From 198cdb357b3713d65122e4bb881471e3e8081263 Mon Sep 17 00:00:00 2001 From: "Dusan Mijatovic (PC2020)" Date: Mon, 18 Sep 2023 13:36:22 +0200 Subject: [PATCH 1/6] feat: add scraper error logs page to admin section --- database/118-backend-logs-views.sql | 51 +++++++ frontend/components/AppHeader/index.tsx | 2 +- frontend/components/admin/AdminNav.tsx | 7 + .../components/admin/AdminPageWithNav.tsx | 2 + .../admin/logs/DeleteOldLogsBtn.tsx | 28 ++++ .../components/admin/logs/ErrorInfoCell.tsx | 139 ++++++++++++++++++ frontend/components/admin/logs/LogsTable.tsx | 91 ++++++++++++ .../admin/logs/__mocks__/apiLogs.ts | 50 +++++++ frontend/components/admin/logs/apiLogs.ts | 125 ++++++++++++++++ frontend/components/admin/logs/config.tsx | 74 ++++++++++ frontend/components/admin/logs/index.tsx | 86 +++++++++++ frontend/components/admin/logs/useLogs.tsx | 114 ++++++++++++++ .../admin/rsd-contributors/config.tsx | 1 - frontend/components/table/EditableTable.tsx | 2 +- frontend/components/table/TableBody.tsx | 3 + frontend/components/table/TableHeader.tsx | 22 +-- frontend/pages/admin/logs.tsx | 42 ++++++ frontend/styles/rsdMuiTheme.ts | 9 +- frontend/utils/dateFn.ts | 14 ++ 19 files changed, 849 insertions(+), 13 deletions(-) create mode 100644 database/118-backend-logs-views.sql create mode 100644 frontend/components/admin/logs/DeleteOldLogsBtn.tsx create mode 100644 frontend/components/admin/logs/ErrorInfoCell.tsx create mode 100644 frontend/components/admin/logs/LogsTable.tsx create mode 100644 frontend/components/admin/logs/__mocks__/apiLogs.ts create mode 100644 frontend/components/admin/logs/apiLogs.ts create mode 100644 frontend/components/admin/logs/config.tsx create mode 100644 frontend/components/admin/logs/index.tsx create mode 100644 frontend/components/admin/logs/useLogs.tsx create mode 100644 frontend/pages/admin/logs.tsx diff --git a/database/118-backend-logs-views.sql b/database/118-backend-logs-views.sql new file mode 100644 index 000000000..8f9dfd24b --- /dev/null +++ b/database/118-backend-logs-views.sql @@ -0,0 +1,51 @@ +-- SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +-- SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) +-- SPDX-FileCopyrightText: 2023 Netherlands eScience Center +-- +-- SPDX-License-Identifier: Apache-2.0 + +CREATE FUNCTION slug_from_log_reference( + table_name VARCHAR, + reference_id UUID +) +RETURNS TABLE ( + slug VARCHAR +) +LANGUAGE sql STABLE AS +$$ +SELECT CASE + WHEN table_name = 'repository_url' THEN ( + SELECT + CONCAT('/software/',slug,'/edit/information') as slug + FROM + software WHERE id = reference_id + ) + WHEN table_name = 'package_manager' THEN ( + SELECT + CONCAT('/software/',slug,'/edit/package-managers') as slug + FROM + software + WHERE id = (SELECT software FROM package_manager WHERE id = reference_id)) + END +$$; + +CREATE FUNCTION backend_log_view() RETURNS TABLE ( + id UUID, + service_name VARCHAR, + table_name VARCHAR, + reference_id UUID, + message VARCHAR, + stack_trace VARCHAR, + other_data JSONB, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ, + slug VARCHAR +) +LANGUAGE sql STABLE AS +$$ +SELECT + *, + slug_from_log_reference(backend_log.table_name, backend_log.reference_id) +FROM + backend_log +$$; diff --git a/frontend/components/AppHeader/index.tsx b/frontend/components/AppHeader/index.tsx index 7e3e3767e..c6a89b3c1 100644 --- a/frontend/components/AppHeader/index.tsx +++ b/frontend/components/AppHeader/index.tsx @@ -75,7 +75,7 @@ export default function AppHeader() { className="2xl:hidden" loading='eager' // lighthouse audit requires explicit width and height - width="100%" + width="7rem" height="1.5rem" /> diff --git a/frontend/components/admin/AdminNav.tsx b/frontend/components/admin/AdminNav.tsx index afe1d6971..990482114 100644 --- a/frontend/components/admin/AdminNav.tsx +++ b/frontend/components/admin/AdminNav.tsx @@ -22,6 +22,7 @@ import DomainAddIcon from '@mui/icons-material/DomainAdd' import AccountCircleIcon from '@mui/icons-material/AccountCircle' import FluorescentIcon from '@mui/icons-material/Fluorescent' import CampaignIcon from '@mui/icons-material/Campaign' +import BugReportIcon from '@mui/icons-material/BugReport' export const adminPages = { pages:{ @@ -66,6 +67,12 @@ export const adminPages = { icon: , path: '/admin/keywords', }, + logs:{ + title: 'Error logs', + subtitle: 'From background services', + icon: , + path: '/admin/logs', + }, announcements: { title: 'Announcement', subtitle: 'Notification to all users', diff --git a/frontend/components/admin/AdminPageWithNav.tsx b/frontend/components/admin/AdminPageWithNav.tsx index 81a8058e0..7000f37cb 100644 --- a/frontend/components/admin/AdminPageWithNav.tsx +++ b/frontend/components/admin/AdminPageWithNav.tsx @@ -1,4 +1,6 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (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 diff --git a/frontend/components/admin/logs/DeleteOldLogsBtn.tsx b/frontend/components/admin/logs/DeleteOldLogsBtn.tsx new file mode 100644 index 000000000..db3390e3d --- /dev/null +++ b/frontend/components/admin/logs/DeleteOldLogsBtn.tsx @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import IconButton from '@mui/material/IconButton' +import AutoDeleteIcon from '@mui/icons-material/AutoDelete' + +type DeleteOldLogs={ + disabled: boolean + onDelete:()=>void +} + +export default function DeleteOldLogsBtn({disabled,onDelete}:DeleteOldLogs) { + return ( + + + + ) +} diff --git a/frontend/components/admin/logs/ErrorInfoCell.tsx b/frontend/components/admin/logs/ErrorInfoCell.tsx new file mode 100644 index 000000000..2b319669b --- /dev/null +++ b/frontend/components/admin/logs/ErrorInfoCell.tsx @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useState} from 'react' +import Tooltip from '@mui/material/Tooltip' +import IconButton from '@mui/material/IconButton' +import KeyIcon from '@mui/icons-material/Key' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' +import DataObjectIcon from '@mui/icons-material/DataObject' +import OpenInNewIcon from '@mui/icons-material/OpenInNew' +import ContentCopyIcon from '@mui/icons-material/ContentCopy' +import copyToClipboard from '~/utils/copyToClipboard' + +type LogMessageCellProps={ + message: string + other_data: string + stack_trace: string + id: string + slug: string | null +} + +export default function ErrorInfoCell({message,stack_trace,other_data,id,slug}:LogMessageCellProps) { + const [action, setAction] = useState() + + function toggleButton(value:string){ + if (action===value){ + setAction(undefined) + } else { + setAction(value) + } + } + + return ( + <> +
{message}
+
+ {/* Other data */} + +
+ Error data + { + copyToClipboard(JSON.stringify(other_data,null,2)) + toggleButton('other_data') + }}> + + +
+
+                {JSON.stringify(other_data,null,2)}
+              
+ + } + open={action === 'other_data'} + // onClose={()=>toggleButton('other_data')} + arrow + > + toggleButton('other_data')}> + + +
+ + {/* Stack trace */} + +
+ Stack trace + { + copyToClipboard(stack_trace) + toggleButton('stack_trace') + }}> + + +
+
+                {stack_trace}
+              
+ + } + open={action === 'stack_trace'} + arrow + > + toggleButton('stack_trace')}> + + +
+ + {/* Log id */} + +
+ Log id + { + copyToClipboard(id) + toggleButton('log_id') + }}> + + +
+
+                {id}
+              
+ + } + open={action === 'log_id'} + arrow + > + toggleButton('log_id')}> + + +
+ {slug ? + + + + : null + } +
+ + ) +} diff --git a/frontend/components/admin/logs/LogsTable.tsx b/frontend/components/admin/logs/LogsTable.tsx new file mode 100644 index 000000000..227449455 --- /dev/null +++ b/frontend/components/admin/logs/LogsTable.tsx @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (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 + +import Alert from '@mui/material/Alert' +import AlertTitle from '@mui/material/AlertTitle' + +import EditableTable, {Column, OrderByProps} from '~/components/table/EditableTable' +import ContentLoader from '~/components/layout/ContentLoader' +import {BackendLog} from './useLogs' + +type LogTableProps={ + loading: boolean + columns: Column[], + logs: BackendLog[], + orderBy: OrderByProps + setOrderBy: (orderBy:OrderByProps)=>void +} + +export default function LogsTable({loading,columns,logs,orderBy,setOrderBy}:LogTableProps) { + // const [orderBy, setOrderBy] = useState>(initalOrder) + // const {loading, columns, logs} = useLogs({orderBy}) + + // console.group('LogsTable') + // console.log('loading...', loading) + // console.log('columns...', columns) + // console.log('logs...', logs) + // console.groupEnd() + + const styles = { + flex: 1, + overflow: 'auto', + padding: '0.5rem 0rem', + cursor: 'default', + minHeight: logs.length > 7 ?'80vh':'50vh' + } + + if (logs.length === 0 && loading===false) { + return ( +
+ + No error logs. Nice job! + +
+ ) + } + + function onSortColumn(column:keyof BackendLog) { + if (orderBy && orderBy.column === column) { + if (orderBy.direction === 'asc') { + setOrderBy({ + column, + direction: 'desc' + }) + } else { + setOrderBy({ + column, + direction: 'asc' + }) + } + } else { + setOrderBy({ + column, + direction: 'asc' + }) + } + } + + return ( +
+ { + loading ? + + : + + } +
+ ) +} diff --git a/frontend/components/admin/logs/__mocks__/apiLogs.ts b/frontend/components/admin/logs/__mocks__/apiLogs.ts new file mode 100644 index 000000000..963d44b55 --- /dev/null +++ b/frontend/components/admin/logs/__mocks__/apiLogs.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (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 + +import {ApiParams} from '~/utils/postgrestUrl' +import {BackendLog} from '../useLogs' + +export async function getLogs({page, rows, token, searchFor, orderBy}: ApiParams) { + try { + return { + count: 0, + logs: [] + } + } catch (e: any) { + return { + count: 0, + logs: [] + } + } +} + +export async function deleteLogById({id,token}:{id:string, token:string}){ + try{ + return { + status:200, + message: 'OK' + } + }catch(e:any){ + return { + status: 500, + message: 'Server error' + } + } +} + +/** + * Deletes logs older than specified number of days. + * Based on created_at value. Default days is 30 and the limit of 1000 records to remove. + * @param @object{days?,limit?,token} + * @returns + */ +export async function deleteLogsOlderThan({days=30,limit=1000,token}:{token:string,days?:number,limit?:number}){ + return { + status: 200, + message: 0, + } +} diff --git a/frontend/components/admin/logs/apiLogs.ts b/frontend/components/admin/logs/apiLogs.ts new file mode 100644 index 000000000..f6f3c3892 --- /dev/null +++ b/frontend/components/admin/logs/apiLogs.ts @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (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 + +import logger from '~/utils/logger' +import {extractCountFromHeader} from '~/utils/extractCountFromHeader' +import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' +import {ApiParams, paginationUrlParams} from '~/utils/postgrestUrl' +import {BackendLog} from './useLogs' +import {getDateFromNow} from '~/utils/dateFn' + +export async function getLogs({page, rows, token, searchFor, orderBy}: ApiParams) { + try { + let query = paginationUrlParams({rows, page}) + if (searchFor) { + query+=`&or=(service_name.ilike.*${searchFor}*,table_name.ilike.*${searchFor}*,message.ilike.*${searchFor}*,stack_trace.ilike.*${searchFor}*)` + } + if (orderBy) { + query+=`&order=${orderBy.column}.${orderBy.direction}` + } + // complete url + const url = `${getBaseUrl()}/rpc/backend_log_view?${query}` + + // make request + const resp = await fetch(url,{ + method: 'GET', + headers: { + ...createJsonHeaders(token), + // request record count to be returned + // note: it's returned in the header + 'Prefer': 'count=exact' + }, + }) + + if ([200,206].includes(resp.status)) { + const logs: BackendLog[] = await resp.json() + return { + count: extractCountFromHeader(resp.headers) ?? 0, + logs + } + } + logger(`getLogs: ${resp.status}: ${resp.statusText}`,'warn') + return { + count: 0, + logs: [] + } + } catch (e: any) { + logger(`getLogs: ${e.message}`,'error') + return { + count: 0, + logs: [] + } + } +} + +export async function deleteLogById({id,token}:{id:string, token:string}){ + const url = `${getBaseUrl()}/backend_log?id=eq.${id}` + try{ + // make request + const resp = await fetch(url,{ + method: 'DELETE', + headers: { + ...createJsonHeaders(token), + // request record count to be returned + // note: it's returned in the header + // 'Prefer': 'count=exact' + }, + }) + + return extractReturnMessage(resp) + }catch(e:any){ + logger(`getLogs: ${e.message}`,'error') + return { + status: 500, + message: e.message + } + } +} + +/** + * Deletes logs older than specified number of days. + * Based on created_at value. Default days is 30 and the limit of 1000 records to remove. + * @param @object{days?,limit?,token} + * @returns + */ +export async function deleteLogsOlderThan({days=30,limit=1000,token}:{token:string,days?:number,limit?:number}){ + // use negative days value + const targetDate = getDateFromNow(-days) + // convert to iso string to pass in query + const isoStrDate = targetDate.toISOString() + // delete records where created_at < isoStrDate + // use limit to set max of 1000 deleted records + const url = `${getBaseUrl()}/backend_log?created_at=lt.${isoStrDate}&select=id` + try{ + // make request + const resp = await fetch(url,{ + method: 'DELETE', + headers: { + ...createJsonHeaders(token), + // request deleted records to be returned AND the record count in the response header + 'Prefer': 'return=representation,count=exact' + }, + }) + + if ([200,204,206].includes(resp.status)){ + // const ids:string[] = await resp.json() + return { + status: 200, + message: extractCountFromHeader(resp.headers) ?? 0, + } + }else{ + return extractReturnMessage(resp) + } + + }catch(e:any){ + logger(`getLogs: ${e.message}`,'error') + return { + status: 500, + message: e.message + } + } +} diff --git a/frontend/components/admin/logs/config.tsx b/frontend/components/admin/logs/config.tsx new file mode 100644 index 000000000..378023cf5 --- /dev/null +++ b/frontend/components/admin/logs/config.tsx @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 + +import {Column} from '~/components/table/EditableTable' +import {BackendLog} from './useLogs' +import ErrorInfoCell from './ErrorInfoCell' +import IconButton from '@mui/material/IconButton' +import DeleteIcon from '@mui/icons-material/Delete' + +export function createColumns(onDelete:(id:string)=>Promise) { + const columns: Column[] = [{ + key: 'created_at', + label: 'Created at', + type: 'datetime', + sx: { + padding: '0.5rem', + width: '7rem', + }, + }, { + key: 'service_name', + label: 'Service', + type: 'string', + }, { + key: 'table_name', + label: 'Table', + type: 'string' + }, { + key: 'message', + label: 'Error info', + type: 'custom', + sx: { + // needs scrollers + overflow: 'auto' + }, + renderFn: (data) => { + return ( + + ) + } + }, { + key: 'delete', + label: 'Action', + type: 'custom', + renderFn: (data) => { + return ( + { + onDelete(data.id) + }} + > + + + ) + } + }] + + return columns +} + diff --git a/frontend/components/admin/logs/index.tsx b/frontend/components/admin/logs/index.tsx new file mode 100644 index 000000000..1b1c75105 --- /dev/null +++ b/frontend/components/admin/logs/index.tsx @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useEffect, useState} from 'react' +import Pagination from '~/components/pagination/Pagination' +import Searchbox from '~/components/search/Searchbox' +import {Column, OrderByProps} from '~/components/table/EditableTable' +import LogsTable from './LogsTable' +import DeleteOldLogsBtn from './DeleteOldLogsBtn' +import useLogs, {BackendLog} from './useLogs' +import {createColumns} from './config' +import IconButton from '@mui/material/IconButton' +import RefreshIcon from '@mui/icons-material/Refresh' + +// inital logs order is on family names +const initalOrder:OrderByProps = { + column: 'created_at', + direction: 'desc' +} + +export default function ScraperErrorLogs() { + const [orderBy, setOrderBy] = useState>(initalOrder) + const [columns, setColumns] = useState[]>([]) + const {loading, logs, loadLogs, deleteLog, deleteOldLogs} = useLogs({orderBy}) + + // update columns order + if (orderBy) { + columns.forEach(col => { + if (col.key === orderBy.column) { + col.order = { + active: true, + direction: orderBy.direction + } + } else { + col.order = { + active: false, + direction: 'asc' + } + } + }) + } + + useEffect(()=>{ + // here we pass deleteLog method to columns + const cols = createColumns(deleteLog) + setColumns(cols) + // do not include deleteLog to avoid loop + // eslint-disable-next-line react-hooks/exhaustive-deps + },[deleteLog]) + + return ( +
+
+
+ deleteOldLogs()} + disabled={logs.length===0} + /> + +
+
+ + + + +
+
+ +
+ ) +} diff --git a/frontend/components/admin/logs/useLogs.tsx b/frontend/components/admin/logs/useLogs.tsx new file mode 100644 index 000000000..258dfd833 --- /dev/null +++ b/frontend/components/admin/logs/useLogs.tsx @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useCallback, useEffect, useState} from 'react' +import usePaginationWithSearch from '~/utils/usePaginationWithSearch' + +import {useSession} from '~/auth' +import useSnackbar from '~/components/snackbar/useSnackbar' +import {OrderByProps} from '~/components/table/EditableTable' +import {getLogs,deleteLogById, deleteLogsOlderThan} from './apiLogs' + +export type BackendLog = { + id: string, + service_name: string + table_name: string + reference_id: string + message: string + stack_trace: string + other_data: string + created_at: string, + slug: string, + delete?: boolean +} + +type useContributorsProps = { + orderBy?: OrderByProps +} + +export default function useLogs({orderBy}:useContributorsProps) { + const {token} = useSession() + const {showErrorMessage,showSuccessMessage} = useSnackbar() + const {searchFor, page, rows, setCount} = usePaginationWithSearch('Find log') + const [logs, setLogs] = useState([]) + const [loading, setLoading] = useState(true) + + const loadLogs = useCallback(async () => { + let abort = false + setLoading(true) + + // console.group('loadLogs') + // console.log('page...', page) + // console.log('rows...', rows) + // console.log('orderBy...', orderBy) + // console.groupEnd() + + const {logs, count} = await getLogs({ + token, + searchFor, + page, + rows, + orderBy + }) + + if (abort === false) { + setLogs(logs) + setCount(count) + setLoading(false) + } + + return ()=>{abort=true} + // do not include setCount in order to avoid loop + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token, searchFor, page, rows, orderBy]) + + /** + * Delete log and reload table. + */ + const deleteLog = useCallback(async(id: string)=>{ + const resp = await deleteLogById({ + id, + token + }) + if (resp.status===200) { + // load logs again + await loadLogs() + } else { + showErrorMessage(`Failed to remove log ${id}. ${resp.message}`) + } + // we exclude showErrorMessage to avoid extra reloads + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token,loadLogs]) + + const deleteOldLogs = useCallback(async(days?:number)=>{ + const resp = await deleteLogsOlderThan({ + days, + token, + }) + if (resp.status===200) { + showSuccessMessage(`Removed ${resp.message} log items`) + // load logs again if some records are deleted + if (parseInt(resp.message) > 0) loadLogs() + } else { + showErrorMessage(`Failed to remove old logs. ${resp.message}`) + } + // we exclude showErrorMessage,showSuccessMessage to avoid extra reloads + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token,loadLogs]) + + + useEffect(() => { + if (token) loadLogs() + },[token,loadLogs]) + + return { + loading, + logs, + loadLogs, + deleteLog, + deleteOldLogs + } +} + diff --git a/frontend/components/admin/rsd-contributors/config.tsx b/frontend/components/admin/rsd-contributors/config.tsx index 8125a5a84..a3b99c2b3 100644 --- a/frontend/components/admin/rsd-contributors/config.tsx +++ b/frontend/components/admin/rsd-contributors/config.tsx @@ -126,7 +126,6 @@ export function createColumns(token: string) { return ( - {data.origin === 'contributor' ? 'Software' : 'Project'} ) } diff --git a/frontend/components/table/EditableTable.tsx b/frontend/components/table/EditableTable.tsx index b64029fc9..57ed2e8ba 100644 --- a/frontend/components/table/EditableTable.tsx +++ b/frontend/components/table/EditableTable.tsx @@ -23,7 +23,7 @@ export type OrderProps = { export type Column = { key: K label: string - type: 'string' | 'date' | 'boolean' | 'custom' + type: 'string' | 'date' | 'datetime' | 'boolean' | 'custom' align?: 'left'|'right'|'center'|'justify' className?: string sx?: SxProps diff --git a/frontend/components/table/TableBody.tsx b/frontend/components/table/TableBody.tsx index 25b46fd48..910649351 100644 --- a/frontend/components/table/TableBody.tsx +++ b/frontend/components/table/TableBody.tsx @@ -24,6 +24,9 @@ function formatValue(col: Column, value: any) { case 'date': formatedValue = new Date(value).toLocaleDateString() return formatedValue + case 'datetime': + formatedValue = new Date(value).toLocaleString() + return formatedValue default: // formatedValue = value.toString() return value diff --git a/frontend/components/table/TableHeader.tsx b/frontend/components/table/TableHeader.tsx index 973fdd9a3..91d99e6a1 100644 --- a/frontend/components/table/TableHeader.tsx +++ b/frontend/components/table/TableHeader.tsx @@ -20,19 +20,23 @@ export default function TableHeader({columns, onSort}: {columns.map((col, i) => { return ( onSort(col.key)} + onClick={() => col.type!=='custom' ? onSort(col.key) : null} sx={col?.sx} > - onSort(col.key)} - > - {col.label} - + {col.type!=='custom' ? + onSort(col.key)} + > + {col.label} + + : + col.label + } ) })} diff --git a/frontend/pages/admin/logs.tsx b/frontend/pages/admin/logs.tsx new file mode 100644 index 000000000..341cd2b1a --- /dev/null +++ b/frontend/pages/admin/logs.tsx @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (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 + +import Head from 'next/head' +import {app} from '../../config/app' +import {adminPages} from '~/components/admin/AdminNav' +import DefaultLayout from '~/components/layout/DefaultLayout' +import AdminPageWithNav from '~/components/admin/AdminPageWithNav' +import {SearchProvider} from '~/components/search/SearchContext' +import {PaginationProvider} from '~/components/pagination/PaginationContext' +import AdminLogs from '~/components/admin/logs' + +const pageTitle = `${adminPages['logs'].title} | Admin page | ${app.title}` + +const pagination = { + count: 0, + page: 0, + rows: 12, + rowsOptions: [12,24,48], + labelRowsPerPage:'Per page' +} + +export default function AdminLogsPage() { + return ( + + + {pageTitle} + + + + + + + + + + ) +} diff --git a/frontend/styles/rsdMuiTheme.ts b/frontend/styles/rsdMuiTheme.ts index 3b377ce84..1e4b15b6e 100644 --- a/frontend/styles/rsdMuiTheme.ts +++ b/frontend/styles/rsdMuiTheme.ts @@ -280,7 +280,14 @@ function applyThemeConfig({colors, action, typography}: ThemeConfig) { backgroundColor: colors['base-600'] } } - } + }, + MuiTooltip: { + styleOverrides:{ + tooltip: { + maxWidth: '40rem' + }, + } + }, }, }) } diff --git a/frontend/utils/dateFn.ts b/frontend/utils/dateFn.ts index 81697dbd2..631c470ec 100644 --- a/frontend/utils/dateFn.ts +++ b/frontend/utils/dateFn.ts @@ -1,5 +1,7 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -127,3 +129,15 @@ export function getMonthYearDate(date: string, locale = 'en-us') { return null } } + +/** + * Calculates the date from now for number of days passed. + * Pass positive value for dates in the future and negative values for dates in the past. + */ +export function getDateFromNow(days:number){ + const newDate = new Date() + // change date + newDate.setDate(newDate.getDate() + days) + // return changed date + return newDate +} From 03bbcbcc9aba993692bf4eae05117132dce95f10 Mon Sep 17 00:00:00 2001 From: "Dusan Mijatovic (PC2020)" Date: Tue, 19 Sep 2023 11:57:22 +0200 Subject: [PATCH 2/6] fix: prevent blurry logo image in the organisation card --- .../components/organisation/overview/card/OrganisationCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/organisation/overview/card/OrganisationCard.tsx b/frontend/components/organisation/overview/card/OrganisationCard.tsx index 22a81d714..e0fb7f463 100644 --- a/frontend/components/organisation/overview/card/OrganisationCard.tsx +++ b/frontend/components/organisation/overview/card/OrganisationCard.tsx @@ -45,7 +45,7 @@ export default function OrganisationCard({organisation}: { organisation: Organis alt={`Logo for ${organisation.name}`} type="gradient" className={`w-full text-base-content-disabled ${organisation.logo_id ? 'p-4':''}`} - bgSize={'contain'} + bgSize={'scale-down'} /> From 32648890cf580127930ae4cf21b1001f39d19278 Mon Sep 17 00:00:00 2001 From: "Dusan Mijatovic (PC2020)" Date: Tue, 19 Sep 2023 17:14:57 +0200 Subject: [PATCH 3/6] refactor: split related topics into related software and projects sections on software and project edit pages tests: update e2e and unit tests --- e2e/helpers/utils.ts | 98 ------- e2e/tests/project.spec.ts | 16 +- e2e/tests/software.spec.ts | 17 +- .../projects/edit/editProjectPages.tsx | 19 +- .../EditRelatedProjectsIndex.test.tsx | 107 +------ .../FindRelatedProject.tsx | 6 +- .../RelatedProjectList.tsx | 4 +- .../__mocks__/relatedProjectsForProject.json | 0 .../relatedProjectsForProject.json.license | 2 + .../projects/edit/related-projects/config.ts | 18 ++ .../index.tsx} | 69 +++-- .../EditRelatedSoftwareIndex.test.tsx | 156 ++++++++++ .../FindRelatedSoftware.tsx | 6 +- .../RelatedSoftwareList.tsx | 4 +- .../__mocks__/relatedSoftwareForProject.json | 0 .../relatedSoftwareForProject.json.license | 2 + .../projects/edit/related-software/config.ts | 18 ++ .../index.tsx} | 74 ++--- .../projects/edit/related/config.ts | 27 -- .../projects/edit/related/index.tsx | 25 -- .../software/RelatedSoftwareSection.tsx | 2 +- .../software/edit/editSoftwarePages.tsx | 18 +- .../__mocks__/relatedProjectsForSoftware.json | 0 .../relatedProjectsForSoftware.json.license | 2 + .../software/edit/related-projects/config.ts | 18 ++ .../index.tsx} | 80 +++--- .../__mocks__/relatedSoftwareForSoftware.json | 0 .../relatedSoftwareForSoftware.json.license | 2 + .../software/edit/related-software/config.ts | 18 ++ .../index.tsx} | 77 ++--- .../related/EditRelatedSoftwareIndex.test.tsx | 268 ------------------ .../software/edit/related/config.ts | 27 -- .../software/edit/related/index.tsx | 29 -- 33 files changed, 478 insertions(+), 731 deletions(-) rename frontend/components/projects/edit/{related => related-projects}/EditRelatedProjectsIndex.test.tsx (60%) rename frontend/components/projects/edit/{related => related-projects}/FindRelatedProject.tsx (94%) rename frontend/components/projects/edit/{related => related-projects}/RelatedProjectList.tsx (95%) rename frontend/components/projects/edit/{related => related-projects}/__mocks__/relatedProjectsForProject.json (100%) rename frontend/components/projects/edit/{related => related-projects}/__mocks__/relatedProjectsForProject.json.license (59%) create mode 100644 frontend/components/projects/edit/related-projects/config.ts rename frontend/components/projects/edit/{related/RelatedProjectsForProject.tsx => related-projects/index.tsx} (79%) create mode 100644 frontend/components/projects/edit/related-software/EditRelatedSoftwareIndex.test.tsx rename frontend/components/projects/edit/{related => related-software}/FindRelatedSoftware.tsx (94%) rename frontend/components/projects/edit/{related => related-software}/RelatedSoftwareList.tsx (95%) rename frontend/components/projects/edit/{related => related-software}/__mocks__/relatedSoftwareForProject.json (100%) rename frontend/components/projects/edit/{related => related-software}/__mocks__/relatedSoftwareForProject.json.license (50%) create mode 100644 frontend/components/projects/edit/related-software/config.ts rename frontend/components/projects/edit/{related/RelatedSoftwareForProject.tsx => related-software/index.tsx} (73%) delete mode 100644 frontend/components/projects/edit/related/config.ts delete mode 100644 frontend/components/projects/edit/related/index.tsx rename frontend/components/software/edit/{related => related-projects}/__mocks__/relatedProjectsForSoftware.json (100%) rename frontend/components/software/edit/{related => related-projects}/__mocks__/relatedProjectsForSoftware.json.license (50%) create mode 100644 frontend/components/software/edit/related-projects/config.ts rename frontend/components/software/edit/{related/RelatedProjectsForSoftware.tsx => related-projects/index.tsx} (70%) rename frontend/components/software/edit/{related => related-software}/__mocks__/relatedSoftwareForSoftware.json (100%) rename frontend/components/software/edit/{related => related-software}/__mocks__/relatedSoftwareForSoftware.json.license (50%) create mode 100644 frontend/components/software/edit/related-software/config.ts rename frontend/components/software/edit/{related/RelatedSoftwareForSoftware.tsx => related-software/index.tsx} (71%) delete mode 100644 frontend/components/software/edit/related/EditRelatedSoftwareIndex.test.tsx delete mode 100644 frontend/components/software/edit/related/config.ts delete mode 100644 frontend/components/software/edit/related/index.tsx diff --git a/e2e/helpers/utils.ts b/e2e/helpers/utils.ts index d0e4541e3..19c674ac1 100755 --- a/e2e/helpers/utils.ts +++ b/e2e/helpers/utils.ts @@ -103,104 +103,6 @@ export async function openEditSection(page:Page,name:string) { } -// TODO! REMOVE THIS FUNCTION LATER -// export async function addOrganisation(page, organisation: Organisation, apiUrl) { - -// // const findOrganisation = page.getByLabel('Find or add organisation') -// const findOrganisation = page.getByRole('combobox', {name: 'Find or add organisation'}) - -// // check if no organisation message is present -// const alert = await page.getByRole('alert') -// .filter({ -// hasText: /No participating organisations/ -// }).count() > 0 - -// // set breakpoint -// // console.log('alert...', alert) -// // await page.pause() - -// if (alert === false) { -// //check if organisation already present -// const organisations = page.getByTestId('organisation-list-item') -// const [count] = await Promise.all([ -// organisations.count(), -// page.waitForLoadState('networkidle') -// ]) - -// if (count > 0) { -// // check if organisation existis -// const found = await organisations -// .filter({hasText: RegExp(organisation.name)}) -// .count() -// // console.log('found...', found) -// // if already exists we return false -// if (found > 0) return false -// } -// } - -// // if not exists we search -// await Promise.all([ -// page.waitForResponse(/api.ror.org\/organizations/), -// page.waitForLoadState('networkidle'), -// findOrganisation.fill(organisation.name) -// ]) - -// const options = page.getByTestId('find-organisation-option') -// const option = await options -// .filter({ -// hasText:RegExp(organisation.name,'i') -// }) -// .first() - -// // get source information -// const source = await option.getByTestId('organisation-list-item').getAttribute('data-source') - -// if (source === 'RSD') { -// await Promise.all([ -// page.waitForResponse(RegExp(apiUrl)), -// option.click() -// ]) -// // if rsd we just add it to list (no modal popup) -// return true -// } -// if (source === 'ROR') { -// // for ROR we can upload logo -// // in the modal/dialog so -// // we wait on modal to appear -// await Promise.all([ -// page.waitForSelector('[role="dialog"]'), -// option.click() -// ]) - -// // console.log('logo...', organisation.logo) -// // upload logo if logo provided -// if (organisation.logo) { -// await uploadFile( -// page, '#upload-avatar-image', -// organisation.logo, -// 'img' -// ) -// } -// // save new organisation -// const saveBtn = page.getByRole('button', { -// name: 'Save' -// }) -// await Promise.all([ -// page.waitForResponse(/organisation/), -// page.waitForResponse(RegExp(apiUrl)), -// saveBtn.click() -// ]) -// } - -// // validate item is added to list -// const lastItem = await page.getByTestId('organisation-list-item').last() -// const lastText = await lastItem.getByTestId('organisation-list-item-text').textContent() -// // console.log('lastText...', lastText) -// expect(lastText).toContain(organisation.name) - -// return true -// } - export async function addRelatedSoftware(page: Page, waitForResponse:string) { const findSoftware = page .getByTestId('find-related-software') diff --git a/e2e/tests/project.spec.ts b/e2e/tests/project.spec.ts index dfa0f1a2e..7cc068b36 100755 --- a/e2e/tests/project.spec.ts +++ b/e2e/tests/project.spec.ts @@ -164,7 +164,7 @@ test.describe.serial('Project', async () => { expect(count).toBeGreaterThanOrEqual(organisations.length) }) - test('Related items', async ({page}, {project: {name}}) => { + test('Related projects', async ({page}, {project: {name}}) => { const project = mockProject[name] // directly open edit software page @@ -173,9 +173,21 @@ test.describe.serial('Project', async () => { // await page.pause() // navigate to organisations section - await openEditSection(page, 'Related topics') + await openEditSection(page, 'Related projects') await addRelatedProject(page, 'project_for_project') + }) + + test('Related software', async ({page}, {project: {name}}) => { + const project = mockProject[name] + + // directly open edit software page + const url = `/projects/${project.slug}` + await openEditPage(page, url, project.title) + + // await page.pause() + // navigate to organisations section + await openEditSection(page, 'Related software') // add related software only if not added const relatedSoftware = page.getByTestId('related-software-item') diff --git a/e2e/tests/software.spec.ts b/e2e/tests/software.spec.ts index a00567d8b..529cce26c 100755 --- a/e2e/tests/software.spec.ts +++ b/e2e/tests/software.spec.ts @@ -163,17 +163,26 @@ test.describe.serial('Software', async()=> { // We test related items as last because we // need some items to be created and published - test('Related items', async ({page}, {project}) => { + test('Related software', async ({page}, {project}) => { const software = mockSoftware[project.name] - // directly open edit software page const url = `/software/${software.slug}` await openEditPage(page, url, software.title) - // navigate to related topics section - await openEditSection(page, 'Related topics') + await openEditSection(page, 'Related software') // add some related software randomly await addRelatedSoftware(page, 'software_for_software') + }) + + // We test related items as last because we + // need some items to be created and published + test('Related projects', async ({page}, {project}) => { + const software = mockSoftware[project.name] + // directly open edit software page + const url = `/software/${software.slug}` + await openEditPage(page, url, software.title) + // navigate to related topics section + await openEditSection(page, 'Related projects') // add some related projects randomly await addRelatedProject(page, 'software_for_project') }) diff --git a/frontend/components/projects/edit/editProjectPages.tsx b/frontend/components/projects/edit/editProjectPages.tsx index 777cf869b..f99868e6b 100644 --- a/frontend/components/projects/edit/editProjectPages.tsx +++ b/frontend/components/projects/edit/editProjectPages.tsx @@ -15,6 +15,7 @@ import OutboundIcon from '@mui/icons-material/Outbound' import AccessibilityNewIcon from '@mui/icons-material/AccessibilityNew' import ShareIcon from '@mui/icons-material/Share' import PersonAddIcon from '@mui/icons-material/PersonAdd' +import TerminalIcon from '@mui/icons-material/Terminal' import ContentLoader from '~/components/layout/ContentLoader' @@ -42,7 +43,10 @@ const ProjectImpact = dynamic(() => import('./impact'),{ const ProjectOutput = dynamic(() => import('./output'),{ loading: ()=> }) -const RelatedTopics = dynamic(() => import('./related'),{ +const RelatedProjects = dynamic(() => import('./related-projects'),{ + loading: ()=> +}) +const RelatedSoftware = dynamic(() => import('./related-software'),{ loading: ()=> }) const ProjectMaintainers = dynamic(() => import('./maintainers'),{ @@ -94,10 +98,17 @@ export const editProjectPage: EditProjectPageProps[] = [ status: 'Optional information' }, { - id: 'related-topics', - label: 'Related topics', + id: 'related-projects', + label: 'Related projects', icon: , - render: () => , + render: () => , + status: 'Optional information' + }, + { + id: 'related-software', + label: 'Related software', + icon: , + render: () => , status: 'Optional information' }, { diff --git a/frontend/components/projects/edit/related/EditRelatedProjectsIndex.test.tsx b/frontend/components/projects/edit/related-projects/EditRelatedProjectsIndex.test.tsx similarity index 60% rename from frontend/components/projects/edit/related/EditRelatedProjectsIndex.test.tsx rename to frontend/components/projects/edit/related-projects/EditRelatedProjectsIndex.test.tsx index 79ad63d59..c45d277f8 100644 --- a/frontend/components/projects/edit/related/EditRelatedProjectsIndex.test.tsx +++ b/frontend/components/projects/edit/related-projects/EditRelatedProjectsIndex.test.tsx @@ -1,5 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 @@ -14,15 +16,12 @@ import RelatedItems from './index' // MOCKS import editProjectState from '../__mocks__/editProjectState' import mockRelatedProjects from './__mocks__/relatedProjectsForProject.json' -import mockRelatedSoftware from './__mocks__/relatedSoftwareForProject.json' // MOCK getRelatedProjectsForProject, getRelatedSoftwareForProject const mockGetRelatedProjectsForProject = jest.fn(props => Promise.resolve([] as any)) -const mockGetRelatedSoftwareForProject = jest.fn(props => Promise.resolve([] as any)) const mockSearchForRelatedProjectByTitle = jest.fn(props => Promise.resolve([] as any)) jest.mock('~/utils/getProjects', () => ({ getRelatedProjectsForProject: jest.fn(props => mockGetRelatedProjectsForProject(props)), - getRelatedSoftwareForProject: jest.fn(props => mockGetRelatedSoftwareForProject(props)), searchForRelatedProjectByTitle: jest.fn(props => mockSearchForRelatedProjectByTitle(props)), })) @@ -44,15 +43,15 @@ jest.mock('~/utils/editRelatedSoftware', () => ({ })) -describe('frontend/components/projects/edit/related/index.tsx', () => { +describe('frontend/components/projects/edit/related-projects/index.tsx', () => { beforeEach(() => { jest.clearAllMocks() }) - it('renders related project and software items', async() => { + it('renders related project items', async() => { // return mocked values mockGetRelatedProjectsForProject.mockResolvedValueOnce(mockRelatedProjects) - mockGetRelatedSoftwareForProject.mockResolvedValueOnce(mockRelatedSoftware) + // mockGetRelatedSoftwareForProject.mockResolvedValueOnce(mockRelatedSoftware) // render render( @@ -67,9 +66,6 @@ describe('frontend/components/projects/edit/related/index.tsx', () => { const relatedProjects = await screen.findAllByTestId('related-project-item') expect(relatedProjects.length).toEqual(mockRelatedProjects.length) - - const relatedSoftware = await screen.findAllByTestId('related-software-item') - expect(relatedSoftware.length).toEqual(mockRelatedSoftware.length) }) it('can add related project to project', async() => { @@ -80,7 +76,6 @@ describe('frontend/components/projects/edit/related/index.tsx', () => { ] // return mocked values mockGetRelatedProjectsForProject.mockResolvedValueOnce([]) - mockGetRelatedSoftwareForProject.mockResolvedValueOnce([]) mockSearchForRelatedProjectByTitle.mockResolvedValueOnce(relatedProjectsFound) mockAddRelatedProject.mockResolvedValueOnce({ status: 200, @@ -122,61 +117,9 @@ describe('frontend/components/projects/edit/related/index.tsx', () => { expect(relatedProjects.length).toEqual(1) }) - it('can add related software to project', async() => { - const searchFor = 'Search for software' - const relatedSoftwareFound = [ - {id:'test-id-1',slug:'test-slug-1',brand_name:'Test title 1',short_statement:'Test subtitle 1',status: 'approved'}, - {id:'test-id-2',slug:'test-slug-2',brand_name:'Test title 2',short_statement:'Test subtitle 2',status:'approved'} - ] - // return mocked values - mockGetRelatedProjectsForProject.mockResolvedValueOnce([]) - mockGetRelatedSoftwareForProject.mockResolvedValueOnce([]) - mockSearchForRelatedSoftware.mockResolvedValueOnce(relatedSoftwareFound) - mockAddRelatedSoftware.mockResolvedValueOnce({ - status: 200, - message: 'OK' - }) - - // render - render( - - - - - - ) - - // search for related project - const findRelated = screen.getByTestId('find-related-software') - const search = within(findRelated).getByRole('combobox') - fireEvent.change(search, {target: {value: searchFor}}) - - // validate mocked options returned - const options = await screen.findAllByTestId('related-software-option') - expect(options.length).toEqual(relatedSoftwareFound.length) - - // add first item - fireEvent.click(options[0]) - - // validate api calls - expect(mockAddRelatedSoftware).toBeCalledTimes(1) - expect(mockAddRelatedSoftware).toBeCalledWith({ - 'project': editProjectState.project.id, - 'software': relatedSoftwareFound[0].id, - 'status': 'approved', - 'token': mockSession.token, - }) - - // validate 1 item added to list - const relatedSoftware = await screen.findAllByTestId('related-software-item') - expect(relatedSoftware.length).toEqual(1) - }) - - it('can remove related project for project', async() => { // return mocked values mockGetRelatedProjectsForProject.mockResolvedValueOnce(mockRelatedProjects) - mockGetRelatedSoftwareForProject.mockResolvedValueOnce([]) mockDeleteRelatedProject .mockResolvedValueOnce({ status: 200, @@ -222,44 +165,4 @@ describe('frontend/components/projects/edit/related/index.tsx', () => { }) }) - it('can remove related software', async() => { - // return mocked values - mockGetRelatedProjectsForProject.mockResolvedValueOnce([]) - mockGetRelatedSoftwareForProject.mockResolvedValueOnce(mockRelatedSoftware) - mockDeleteRelatedSoftware.mockResolvedValueOnce({ - status: 200, - message:'OK' - }) - // render - render( - - - - - - ) - - // get list - const relatedSoftware = await screen.findAllByTestId('related-software-item') - // get delete btn of first item - const delBtn = within(relatedSoftware[0]).getByRole('button', { - name: 'delete' - }) - // delete item - fireEvent.click(delBtn) - - await waitFor(() => { - expect(mockDeleteRelatedSoftware).toBeCalledTimes(1) - expect(mockDeleteRelatedSoftware).toBeCalledWith({ - 'project': editProjectState.project.id, - 'software': mockRelatedSoftware[0].id, - 'token': mockSession.token, - }) - // validate remaining items - const remainedSoftware = screen.getAllByTestId('related-software-item') - expect(remainedSoftware.length).toEqual(mockRelatedSoftware.length - 1) - }) - }) - - }) diff --git a/frontend/components/projects/edit/related/FindRelatedProject.tsx b/frontend/components/projects/edit/related-projects/FindRelatedProject.tsx similarity index 94% rename from frontend/components/projects/edit/related/FindRelatedProject.tsx rename to frontend/components/projects/edit/related-projects/FindRelatedProject.tsx index 1e1915a7f..a2ce0c75b 100644 --- a/frontend/components/projects/edit/related/FindRelatedProject.tsx +++ b/frontend/components/projects/edit/related-projects/FindRelatedProject.tsx @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -61,7 +63,7 @@ export default function FindRelatedProject({project,config,token,onAdd, onCreate } function onAddSelected(selected:AutocompleteOption) { - if (selected && selected.data) { + if (selected?.data) { onAdd(selected.data) } } @@ -131,7 +133,7 @@ export default function FindRelatedProject({project,config,token,onAdd, onCreate // if onCreate fn is not provided // we do not allow free solo text // eg. only selection of found items - freeSolo: onCreate ? true : false + freeSolo: typeof(onCreate) !== 'undefined' }} /> diff --git a/frontend/components/projects/edit/related/RelatedProjectList.tsx b/frontend/components/projects/edit/related-projects/RelatedProjectList.tsx similarity index 95% rename from frontend/components/projects/edit/related/RelatedProjectList.tsx rename to frontend/components/projects/edit/related-projects/RelatedProjectList.tsx index c6c374fd7..5973c2ccd 100644 --- a/frontend/components/projects/edit/related/RelatedProjectList.tsx +++ b/frontend/components/projects/edit/related-projects/RelatedProjectList.tsx @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) (dv4all) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -58,7 +60,7 @@ export default function RelatedProjectList({projects,onRemove}:ProjectListProps) return projects.map((item,pos) => { return ( onRemove(pos)} /> diff --git a/frontend/components/projects/edit/related/__mocks__/relatedProjectsForProject.json b/frontend/components/projects/edit/related-projects/__mocks__/relatedProjectsForProject.json similarity index 100% rename from frontend/components/projects/edit/related/__mocks__/relatedProjectsForProject.json rename to frontend/components/projects/edit/related-projects/__mocks__/relatedProjectsForProject.json diff --git a/frontend/components/projects/edit/related/__mocks__/relatedProjectsForProject.json.license b/frontend/components/projects/edit/related-projects/__mocks__/relatedProjectsForProject.json.license similarity index 59% rename from frontend/components/projects/edit/related/__mocks__/relatedProjectsForProject.json.license rename to frontend/components/projects/edit/related-projects/__mocks__/relatedProjectsForProject.json.license index 1a59b7b7e..fc3eacc64 100644 --- a/frontend/components/projects/edit/related/__mocks__/relatedProjectsForProject.json.license +++ b/frontend/components/projects/edit/related-projects/__mocks__/relatedProjectsForProject.json.license @@ -1,5 +1,7 @@ +SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) +SPDX-FileCopyrightText: 2023 Netherlands eScience Center SPDX-FileCopyrightText: 2023 dv4all SPDX-License-Identifier: Apache-2.0 diff --git a/frontend/components/projects/edit/related-projects/config.ts b/frontend/components/projects/edit/related-projects/config.ts new file mode 100644 index 000000000..2d819825b --- /dev/null +++ b/frontend/components/projects/edit/related-projects/config.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +export const relatedProject = { + title: 'Related projects', + subtitle: 'This project is related to these projects registered in RSD', + findTitle: 'Find project', + label: 'Find related projects in RSD', + help: 'Start typing for suggestions', + validation: { + //custom validation rule, not in used by react-hook-form + minLength: 1, + } +} diff --git a/frontend/components/projects/edit/related/RelatedProjectsForProject.tsx b/frontend/components/projects/edit/related-projects/index.tsx similarity index 79% rename from frontend/components/projects/edit/related/RelatedProjectsForProject.tsx rename to frontend/components/projects/edit/related-projects/index.tsx index 899894f66..3d0884040 100644 --- a/frontend/components/projects/edit/related/RelatedProjectsForProject.tsx +++ b/frontend/components/projects/edit/related-projects/index.tsx @@ -8,18 +8,19 @@ import {useEffect, useState} from 'react' import {useSession} from '~/auth' -import {cfgRelatedItems as config} from './config' import {getRelatedProjectsForProject} from '~/utils/getProjects' import {addRelatedProject, deleteRelatedProject} from '~/utils/editProject' -import useSnackbar from '~/components/snackbar/useSnackbar' import {sortOnStrProp} from '~/utils/sortFn' +import {extractErrorMessages} from '~/utils/fetchHelpers' import {ProjectStatusKey, RelatedProjectForProject, SearchProject} from '~/types/Project' +import {Status} from '~/types/Organisation' +import useSnackbar from '~/components/snackbar/useSnackbar' +import EditSectionTitle from '~/components/layout/EditSectionTitle' +import EditSection from '~/components/layout/EditSection' +import {relatedProject as config} from './config' import FindRelatedProject from './FindRelatedProject' import useProjectContext from '../useProjectContext' import RelatedProjectList from './RelatedProjectList' -import EditSectionTitle from '~/components/layout/EditSectionTitle' -import {Status} from '~/types/Organisation' -import {extractErrorMessages} from '~/utils/fetchHelpers' export default function RelatedProjectsForProject() { const {token} = useSession() @@ -124,35 +125,41 @@ export default function RelatedProjectsForProject() { } return ( - <> - - {/* add count to title */} - {relatedProject && relatedProject.length > 0 ? -
{relatedProject.length}
- : null - } -
- -
+ +
+ + {/* add count to title */} + {relatedProject && relatedProject.length > 0 ? +
{relatedProject.length}
+ : null + } +
-
- + +
+ + +
+ ) } diff --git a/frontend/components/projects/edit/related-software/EditRelatedSoftwareIndex.test.tsx b/frontend/components/projects/edit/related-software/EditRelatedSoftwareIndex.test.tsx new file mode 100644 index 000000000..e0219e3c5 --- /dev/null +++ b/frontend/components/projects/edit/related-software/EditRelatedSoftwareIndex.test.tsx @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 + +import {fireEvent, render, screen, waitFor, within} from '@testing-library/react' + +import {WithAppContext, mockSession} from '~/utils/jest/WithAppContext' +import {WithProjectContext} from '~/utils/jest/WithProjectContext' + +import RelatedItems from './index' + +// MOCKS +import editProjectState from '../__mocks__/editProjectState' +// import mockRelatedProjects from './__mocks__/relatedProjectsForProject.json' +import mockRelatedSoftware from './__mocks__/relatedSoftwareForProject.json' + +// MOCK getRelatedProjectsForProject, getRelatedSoftwareForProject +const mockGetRelatedSoftwareForProject = jest.fn(props => Promise.resolve([] as any)) +const mockSearchForRelatedProjectByTitle = jest.fn(props => Promise.resolve([] as any)) +jest.mock('~/utils/getProjects', () => ({ + getRelatedSoftwareForProject: jest.fn(props => mockGetRelatedSoftwareForProject(props)), + searchForRelatedProjectByTitle: jest.fn(props => mockSearchForRelatedProjectByTitle(props)), +})) + +// MOCK addRelatedProject, deleteRelatedProject +const mockAddRelatedProject = jest.fn(props => Promise.resolve([] as any)) +const mockDeleteRelatedProject = jest.fn(props => Promise.resolve([] as any)) +const mockAddRelatedSoftware = jest.fn(props => Promise.resolve([] as any)) +const mockDeleteRelatedSoftware = jest.fn(props => Promise.resolve([] as any)) +jest.mock('~/utils/editProject', () => ({ + addRelatedProject: jest.fn(props => mockAddRelatedProject(props)), + deleteRelatedProject: jest.fn(props => mockDeleteRelatedProject(props)), + addRelatedSoftware: jest.fn(props => mockAddRelatedSoftware(props)), + deleteRelatedSoftware: jest.fn(props => mockDeleteRelatedSoftware(props)), +})) + +const mockSearchForRelatedSoftware = jest.fn(props => Promise.resolve([] as any)) +jest.mock('~/utils/editRelatedSoftware', () => ({ + searchForRelatedSoftware: jest.fn(props=>mockSearchForRelatedSoftware(props)) +})) + + +describe('frontend/components/projects/edit/related-software/index.tsx', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('renders related software items', async() => { + // return mocked values + mockGetRelatedSoftwareForProject.mockResolvedValueOnce(mockRelatedSoftware) + + // render + render( + + + + + + ) + + const relatedSoftware = await screen.findAllByTestId('related-software-item') + expect(relatedSoftware.length).toEqual(mockRelatedSoftware.length) + }) + + it('can add related software to project', async() => { + const searchFor = 'Search for software' + const relatedSoftwareFound = [ + {id:'test-id-1',slug:'test-slug-1',brand_name:'Test title 1',short_statement:'Test subtitle 1',status: 'approved'}, + {id:'test-id-2',slug:'test-slug-2',brand_name:'Test title 2',short_statement:'Test subtitle 2',status:'approved'} + ] + // return mocked values + mockGetRelatedSoftwareForProject.mockResolvedValueOnce([]) + mockSearchForRelatedSoftware.mockResolvedValueOnce(relatedSoftwareFound) + mockAddRelatedSoftware.mockResolvedValueOnce({ + status: 200, + message: 'OK' + }) + + // render + render( + + + + + + ) + + // search for related project + const findRelated = screen.getByTestId('find-related-software') + const search = within(findRelated).getByRole('combobox') + fireEvent.change(search, {target: {value: searchFor}}) + + // validate mocked options returned + const options = await screen.findAllByTestId('related-software-option') + expect(options.length).toEqual(relatedSoftwareFound.length) + + // add first item + fireEvent.click(options[0]) + + // validate api calls + expect(mockAddRelatedSoftware).toBeCalledTimes(1) + expect(mockAddRelatedSoftware).toBeCalledWith({ + 'project': editProjectState.project.id, + 'software': relatedSoftwareFound[0].id, + 'status': 'approved', + 'token': mockSession.token, + }) + + // validate 1 item added to list + const relatedSoftware = await screen.findAllByTestId('related-software-item') + expect(relatedSoftware.length).toEqual(1) + }) + + it('can remove related software', async() => { + // return mocked values + mockGetRelatedSoftwareForProject.mockResolvedValueOnce(mockRelatedSoftware) + mockDeleteRelatedSoftware.mockResolvedValueOnce({ + status: 200, + message:'OK' + }) + // render + render( + + + + + + ) + + // get list + const relatedSoftware = await screen.findAllByTestId('related-software-item') + // get delete btn of first item + const delBtn = within(relatedSoftware[0]).getByRole('button', { + name: 'delete' + }) + // delete item + fireEvent.click(delBtn) + + await waitFor(() => { + expect(mockDeleteRelatedSoftware).toBeCalledTimes(1) + expect(mockDeleteRelatedSoftware).toBeCalledWith({ + 'project': editProjectState.project.id, + 'software': mockRelatedSoftware[0].id, + 'token': mockSession.token, + }) + // validate remaining items + const remainedSoftware = screen.getAllByTestId('related-software-item') + expect(remainedSoftware.length).toEqual(mockRelatedSoftware.length - 1) + }) + }) + +}) diff --git a/frontend/components/projects/edit/related/FindRelatedSoftware.tsx b/frontend/components/projects/edit/related-software/FindRelatedSoftware.tsx similarity index 94% rename from frontend/components/projects/edit/related/FindRelatedSoftware.tsx rename to frontend/components/projects/edit/related-software/FindRelatedSoftware.tsx index a807a7117..c940a9345 100644 --- a/frontend/components/projects/edit/related/FindRelatedSoftware.tsx +++ b/frontend/components/projects/edit/related-software/FindRelatedSoftware.tsx @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -57,7 +59,7 @@ export default function FindRelatedSoftware({software,config,token,onAdd, onCrea } function onAddSelected(selected:AutocompleteOption) { - if (selected && selected.data) { + if (selected?.data) { onAdd(selected.data) } } @@ -127,7 +129,7 @@ export default function FindRelatedSoftware({software,config,token,onAdd, onCrea // if onCreate fn is not provided // we do not allow free solo text // eg. only selection of found items - freeSolo: onCreate ? true : false + freeSolo: typeof(onCreate) !== 'undefined' }} /> diff --git a/frontend/components/projects/edit/related/RelatedSoftwareList.tsx b/frontend/components/projects/edit/related-software/RelatedSoftwareList.tsx similarity index 95% rename from frontend/components/projects/edit/related/RelatedSoftwareList.tsx rename to frontend/components/projects/edit/related-software/RelatedSoftwareList.tsx index 7cec5997f..9e25ecdf8 100644 --- a/frontend/components/projects/edit/related/RelatedSoftwareList.tsx +++ b/frontend/components/projects/edit/related-software/RelatedSoftwareList.tsx @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) (dv4all) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -55,7 +57,7 @@ export default function RelatedSoftwareList({software,onRemove}:SoftwareListProp return software.map((item,pos) => { return ( onRemove(pos)} /> diff --git a/frontend/components/projects/edit/related/__mocks__/relatedSoftwareForProject.json b/frontend/components/projects/edit/related-software/__mocks__/relatedSoftwareForProject.json similarity index 100% rename from frontend/components/projects/edit/related/__mocks__/relatedSoftwareForProject.json rename to frontend/components/projects/edit/related-software/__mocks__/relatedSoftwareForProject.json diff --git a/frontend/components/projects/edit/related/__mocks__/relatedSoftwareForProject.json.license b/frontend/components/projects/edit/related-software/__mocks__/relatedSoftwareForProject.json.license similarity index 50% rename from frontend/components/projects/edit/related/__mocks__/relatedSoftwareForProject.json.license rename to frontend/components/projects/edit/related-software/__mocks__/relatedSoftwareForProject.json.license index 1dd52fcb6..b1cf00429 100644 --- a/frontend/components/projects/edit/related/__mocks__/relatedSoftwareForProject.json.license +++ b/frontend/components/projects/edit/related-software/__mocks__/relatedSoftwareForProject.json.license @@ -1,4 +1,6 @@ +SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) +SPDX-FileCopyrightText: 2023 Netherlands eScience Center SPDX-FileCopyrightText: 2023 dv4all SPDX-License-Identifier: Apache-2.0 diff --git a/frontend/components/projects/edit/related-software/config.ts b/frontend/components/projects/edit/related-software/config.ts new file mode 100644 index 000000000..4fb8fb5d6 --- /dev/null +++ b/frontend/components/projects/edit/related-software/config.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +export const relatedSoftware = { + title: 'Related software', + subtitle: 'This project uses the following software registered in RSD', + findTitle: 'Find software', + label: 'Search for related software in RSD', + help: 'Start typing for suggestions', + validation: { + //custom validation rule, not in used by react-hook-form + minLength: 1, + } +} diff --git a/frontend/components/projects/edit/related/RelatedSoftwareForProject.tsx b/frontend/components/projects/edit/related-software/index.tsx similarity index 73% rename from frontend/components/projects/edit/related/RelatedSoftwareForProject.tsx rename to frontend/components/projects/edit/related-software/index.tsx index 95256b488..d978b7c5f 100644 --- a/frontend/components/projects/edit/related/RelatedSoftwareForProject.tsx +++ b/frontend/components/projects/edit/related-software/index.tsx @@ -1,22 +1,25 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 import {useEffect, useState} from 'react' import {useSession} from '~/auth' -import {RelatedSoftwareOfProject, SearchSoftware} from '~/types/SoftwareTypes' -import FindRelatedSoftware from './FindRelatedSoftware' -import {cfgRelatedItems as config} from './config' import {getRelatedSoftwareForProject} from '~/utils/getProjects' import {addRelatedSoftware, deleteRelatedSoftware} from '~/utils/editProject' -import useSnackbar from '~/components/snackbar/useSnackbar' import {sortOnStrProp} from '~/utils/sortFn' +import {RelatedSoftwareOfProject, SearchSoftware} from '~/types/SoftwareTypes' +import {Status} from '~/types/Organisation' +import useSnackbar from '~/components/snackbar/useSnackbar' +import EditSectionTitle from '~/components/layout/EditSectionTitle' +import EditSection from '~/components/layout/EditSection' +import FindRelatedSoftware from './FindRelatedSoftware' +import {relatedSoftware as config} from './config' import useProjectContext from '../useProjectContext' import RelatedSoftwareList from './RelatedSoftwareList' -import EditSectionTitle from '~/components/layout/EditSectionTitle' -import {Status} from '~/types/Organisation' export default function RelatedSoftwareForProject() { const {token} = useSession() @@ -35,6 +38,7 @@ export default function RelatedSoftwareForProject() { frontend: true, approved: false }) + if (abort) return null setRelatedSoftware(software) setLoadedProject(project.id) // setLoading(false) @@ -103,35 +107,41 @@ export default function RelatedSoftwareForProject() { } return ( - <> - - {/* add count to title */} - {relatedSoftware && relatedSoftware.length > 0 ? -
{relatedSoftware.length}
- : null - } -
- -
+ +
+ + {/* add count to title */} + {relatedSoftware && relatedSoftware.length > 0 ? +
{relatedSoftware.length}
+ : null + } +
-
- + +
+ + +
+ ) } diff --git a/frontend/components/projects/edit/related/config.ts b/frontend/components/projects/edit/related/config.ts deleted file mode 100644 index 095ef2e47..000000000 --- a/frontend/components/projects/edit/related/config.ts +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 dv4all -// -// SPDX-License-Identifier: Apache-2.0 - -export const cfgRelatedItems = { - relatedProject: { - title: 'Related projects', - subtitle: 'This project is related to these projects registered in RSD', - label: 'Find related projects', - help: 'Start typing for suggestions', - validation: { - //custom validation rule, not in used by react-hook-form - minLength: 1, - } - }, - relatedSoftware: { - title: 'Related software', - subtitle: 'This project uses the following software registered in RSD', - label: 'Find related software', - help: 'Start typing for suggestions', - validation: { - //custom validation rule, not in used by react-hook-form - minLength: 1, - } - } -} diff --git a/frontend/components/projects/edit/related/index.tsx b/frontend/components/projects/edit/related/index.tsx deleted file mode 100644 index 0e50ce0ca..000000000 --- a/frontend/components/projects/edit/related/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) -// -// SPDX-License-Identifier: Apache-2.0 - -import EditSection from '~/components/layout/EditSection' -import RelatedSoftwareForProject from './RelatedSoftwareForProject' -import RelatedProjectsForProject from './RelatedProjectsForProject' - -export default function RelatedProjectItems() { - - return ( - <> - -
- -
-
- -
-
- - ) -} diff --git a/frontend/components/software/RelatedSoftwareSection.tsx b/frontend/components/software/RelatedSoftwareSection.tsx index d3dc79946..fc658f570 100644 --- a/frontend/components/software/RelatedSoftwareSection.tsx +++ b/frontend/components/software/RelatedSoftwareSection.tsx @@ -23,7 +23,7 @@ export default function RelatedSoftwareSection({relatedSoftware = []}: { related

- Related tools + Related software

import('./mentions'),{ const SoftwareTestimonials = dynamic(() => import('./testimonials'),{ loading: ()=> }) -const RelatedTopics = dynamic(() => import('./related'),{ +const RelatedSoftware = dynamic(() => import('./related-software'),{ + loading: ()=> +}) +const RelatedProjects = dynamic(() => import('./related-projects'),{ loading: ()=> }) const SoftwareMaintainers = dynamic(() => import('./maintainers'),{ @@ -97,10 +101,16 @@ export const editSoftwarePage:EditSoftwarePageProps[] = [{ render: () => , status: 'Optional information' },{ - id: 'related-topics', - label: 'Related topics', + id: 'related-software', + label: 'Related software', icon: , - render: () => , + render: () => , + status: 'Optional information' +},{ + id: 'related-projects', + label: 'Related projects', + icon: , + render: () => , status: 'Optional information' },{ id: 'maintainers', diff --git a/frontend/components/software/edit/related/__mocks__/relatedProjectsForSoftware.json b/frontend/components/software/edit/related-projects/__mocks__/relatedProjectsForSoftware.json similarity index 100% rename from frontend/components/software/edit/related/__mocks__/relatedProjectsForSoftware.json rename to frontend/components/software/edit/related-projects/__mocks__/relatedProjectsForSoftware.json diff --git a/frontend/components/software/edit/related/__mocks__/relatedProjectsForSoftware.json.license b/frontend/components/software/edit/related-projects/__mocks__/relatedProjectsForSoftware.json.license similarity index 50% rename from frontend/components/software/edit/related/__mocks__/relatedProjectsForSoftware.json.license rename to frontend/components/software/edit/related-projects/__mocks__/relatedProjectsForSoftware.json.license index 1dd52fcb6..b1cf00429 100644 --- a/frontend/components/software/edit/related/__mocks__/relatedProjectsForSoftware.json.license +++ b/frontend/components/software/edit/related-projects/__mocks__/relatedProjectsForSoftware.json.license @@ -1,4 +1,6 @@ +SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) +SPDX-FileCopyrightText: 2023 Netherlands eScience Center SPDX-FileCopyrightText: 2023 dv4all SPDX-License-Identifier: Apache-2.0 diff --git a/frontend/components/software/edit/related-projects/config.ts b/frontend/components/software/edit/related-projects/config.ts new file mode 100644 index 000000000..2d819825b --- /dev/null +++ b/frontend/components/software/edit/related-projects/config.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +export const relatedProject = { + title: 'Related projects', + subtitle: 'This project is related to these projects registered in RSD', + findTitle: 'Find project', + label: 'Find related projects in RSD', + help: 'Start typing for suggestions', + validation: { + //custom validation rule, not in used by react-hook-form + minLength: 1, + } +} diff --git a/frontend/components/software/edit/related/RelatedProjectsForSoftware.tsx b/frontend/components/software/edit/related-projects/index.tsx similarity index 70% rename from frontend/components/software/edit/related/RelatedProjectsForSoftware.tsx rename to frontend/components/software/edit/related-projects/index.tsx index 7b10a8e0e..6357800a4 100644 --- a/frontend/components/software/edit/related/RelatedProjectsForSoftware.tsx +++ b/frontend/components/software/edit/related-projects/index.tsx @@ -1,22 +1,25 @@ // 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-License-Identifier: Apache-2.0 import {useEffect, useState} from 'react' import {useSession} from '~/auth' -import {cfgRelatedItems as config} from './config' import {getRelatedProjectsForSoftware} from '~/utils/getSoftware' import {addRelatedSoftware, deleteRelatedSoftware} from '~/utils/editProject' -import useSnackbar from '~/components/snackbar/useSnackbar' import {sortOnStrProp} from '~/utils/sortFn' +import {Status} from '~/types/Organisation' import {SearchProject} from '~/types/Project' -import useSoftwareContext from '../useSoftwareContext' -import FindRelatedProject from '~/components/projects/edit/related/FindRelatedProject' +import useSnackbar from '~/components/snackbar/useSnackbar' +import FindRelatedProject from '~/components/projects/edit/related-projects/FindRelatedProject' import EditSectionTitle from '~/components/layout/EditSectionTitle' -import RelatedProjectList from '~/components/projects/edit/related/RelatedProjectList' -import {Status} from '~/types/Organisation' +import RelatedProjectList from '~/components/projects/edit/related-projects/RelatedProjectList' +import EditSection from '~/components/layout/EditSection' +import useSoftwareContext from '../useSoftwareContext' +import {relatedProject as config} from './config' export default function RelatedProjectsForSoftware() { const {token} = useSession() @@ -34,12 +37,11 @@ export default function RelatedProjectsForSoftware() { frontend: true, approved: false }) - // extract software object - const projects = resp - .sort((a, b) => sortOnStrProp(a, b, 'title')) + // order on title + resp.sort((a, b) => sortOnStrProp(a, b, 'title')) if (abort) return null // debugger - setRelatedProject(projects) + setRelatedProject(resp) setLoadedSoftware(software?.id ?? '') } if (software.id && token && @@ -102,35 +104,41 @@ export default function RelatedProjectsForSoftware() { } return ( - <> - - {/* add count to title */} - {relatedProject && relatedProject.length > 0 ? -
{relatedProject.length}
- : null - } -
- -
+ +
+ + {/* add count to title */} + {relatedProject && relatedProject.length > 0 ? +
{relatedProject.length}
+ : null + } +
-
- + +
+ + +
+ ) } diff --git a/frontend/components/software/edit/related/__mocks__/relatedSoftwareForSoftware.json b/frontend/components/software/edit/related-software/__mocks__/relatedSoftwareForSoftware.json similarity index 100% rename from frontend/components/software/edit/related/__mocks__/relatedSoftwareForSoftware.json rename to frontend/components/software/edit/related-software/__mocks__/relatedSoftwareForSoftware.json diff --git a/frontend/components/software/edit/related/__mocks__/relatedSoftwareForSoftware.json.license b/frontend/components/software/edit/related-software/__mocks__/relatedSoftwareForSoftware.json.license similarity index 50% rename from frontend/components/software/edit/related/__mocks__/relatedSoftwareForSoftware.json.license rename to frontend/components/software/edit/related-software/__mocks__/relatedSoftwareForSoftware.json.license index 1dd52fcb6..b1cf00429 100644 --- a/frontend/components/software/edit/related/__mocks__/relatedSoftwareForSoftware.json.license +++ b/frontend/components/software/edit/related-software/__mocks__/relatedSoftwareForSoftware.json.license @@ -1,4 +1,6 @@ +SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) +SPDX-FileCopyrightText: 2023 Netherlands eScience Center SPDX-FileCopyrightText: 2023 dv4all SPDX-License-Identifier: Apache-2.0 diff --git a/frontend/components/software/edit/related-software/config.ts b/frontend/components/software/edit/related-software/config.ts new file mode 100644 index 000000000..4fb8fb5d6 --- /dev/null +++ b/frontend/components/software/edit/related-software/config.ts @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +export const relatedSoftware = { + title: 'Related software', + subtitle: 'This project uses the following software registered in RSD', + findTitle: 'Find software', + label: 'Search for related software in RSD', + help: 'Start typing for suggestions', + validation: { + //custom validation rule, not in used by react-hook-form + minLength: 1, + } +} diff --git a/frontend/components/software/edit/related/RelatedSoftwareForSoftware.tsx b/frontend/components/software/edit/related-software/index.tsx similarity index 71% rename from frontend/components/software/edit/related/RelatedSoftwareForSoftware.tsx rename to frontend/components/software/edit/related-software/index.tsx index d0eb626c1..5d06bd5e2 100644 --- a/frontend/components/software/edit/related/RelatedSoftwareForSoftware.tsx +++ b/frontend/components/software/edit/related-software/index.tsx @@ -1,23 +1,24 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 import {useEffect, useState} from 'react' import {useSession} from '~/auth' -import {RelatedSoftwareOfSoftware, SearchSoftware} from '~/types/SoftwareTypes' - -import {cfgRelatedItems as config} from './config' -import useSnackbar from '~/components/snackbar/useSnackbar' import {sortOnStrProp} from '~/utils/sortFn' -import useSoftwareContext from '../useSoftwareContext' -import FindRelatedSoftware from '~/components/projects/edit/related/FindRelatedSoftware' import {addRelatedSoftware, deleteRelatedSoftware, getRelatedSoftwareForSoftware} from '~/utils/editRelatedSoftware' - -import RelatedSoftwareList from '../../../projects/edit/related/RelatedSoftwareList' -import EditSectionTitle from '~/components/layout/EditSectionTitle' +import {RelatedSoftwareOfSoftware, SearchSoftware} from '~/types/SoftwareTypes' import {Status} from '~/types/Organisation' +import useSnackbar from '~/components/snackbar/useSnackbar' +import FindRelatedSoftware from '~/components/projects/edit/related-software/FindRelatedSoftware' +import RelatedSoftwareList from '~/components/projects/edit/related-software/RelatedSoftwareList' +import EditSectionTitle from '~/components/layout/EditSectionTitle' +import EditSection from '~/components/layout/EditSection' +import useSoftwareContext from '../useSoftwareContext' +import {relatedSoftware as config} from './config' export default function RelatedSoftwareForSoftware() { @@ -109,35 +110,41 @@ export default function RelatedSoftwareForSoftware() { } return ( - <> - - {/* add count to title */} - {relatedSoftware && relatedSoftware.length > 0 ? -
{relatedSoftware.length}
- : null - } -
- -
+ +
+ + {/* add count to title */} + {relatedSoftware && relatedSoftware.length > 0 ? +
{relatedSoftware.length}
+ : null + } +
-
- + +
+ + +
+ ) } diff --git a/frontend/components/software/edit/related/EditRelatedSoftwareIndex.test.tsx b/frontend/components/software/edit/related/EditRelatedSoftwareIndex.test.tsx deleted file mode 100644 index 95a40cfd2..000000000 --- a/frontend/components/software/edit/related/EditRelatedSoftwareIndex.test.tsx +++ /dev/null @@ -1,268 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) -// SPDX-FileCopyrightText: 2023 dv4all -// -// SPDX-License-Identifier: Apache-2.0 - -import {fireEvent, render, screen, waitFor, within} from '@testing-library/react' - -import {WithAppContext, mockSession} from '~/utils/jest/WithAppContext' -import {WithSoftwareContext} from '~/utils/jest/WithSoftwareContext' -import {initialState as softwareState} from '~/components/software/edit/editSoftwareContext' - -import RelatedSoftwareItems from './index' - -// MOCKS -import mockRelatedSoftware from './__mocks__/relatedSoftwareForSoftware.json' -import mockRelatedProjects from './__mocks__/relatedProjectsForSoftware.json' - -// MOCK editRelatedSoftware methods -const mockGetRelatedSoftwareForSoftware = jest.fn(props => Promise.resolve(mockRelatedSoftware)) -const mockSearchForRelatedSoftware = jest.fn(props => Promise.resolve([] as any)) -const mockAddRelatedSoftware = jest.fn(props => Promise.resolve([] as any)) -const mockDeleteRelatedSoftware = jest.fn(props => Promise.resolve([] as any)) -jest.mock('~/utils/editRelatedSoftware', () => ({ - getRelatedSoftwareForSoftware: jest.fn(props => mockGetRelatedSoftwareForSoftware(props)), - searchForRelatedSoftware: jest.fn(props => mockSearchForRelatedSoftware(props)), - addRelatedSoftware: jest.fn(props => mockAddRelatedSoftware(props)), - deleteRelatedSoftware: jest.fn(props => mockDeleteRelatedSoftware(props)), -})) - -const mockGetRelatedProjectsForSoftware = jest.fn(props => Promise.resolve(mockRelatedProjects)) -jest.mock('~/utils/getSoftware', () => ({ - getRelatedProjectsForSoftware: jest.fn(props=>mockGetRelatedProjectsForSoftware(props)) -})) - -// MOCK addRelatedProject, deleteRelatedProject -const mockAddRelatedProjectForSoftware = jest.fn(props => Promise.resolve([] as any)) -const mockDeleteRelatedProjectForSoftware = jest.fn(props => Promise.resolve([] as any)) -jest.mock('~/utils/editProject', () => ({ - addRelatedSoftware: jest.fn(props => mockAddRelatedProjectForSoftware(props)), - deleteRelatedSoftware: jest.fn(props => mockDeleteRelatedProjectForSoftware(props)), -})) - -// MOCK searchForRelatedProjectByTitle -const mockSearchForRelatedProjectByTitle = jest.fn(props => Promise.resolve([] as any)) -jest.mock('~/utils/getProjects', () => ({ - searchForRelatedProjectByTitle: jest.fn(props => mockSearchForRelatedProjectByTitle(props)), -})) - - -describe('frontend/components/software/edit/related/index.tsx', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - - it('renders related software and project items', async () => { - // required prop - softwareState.software.id = 'test-software-id' - - // mock api responses - mockGetRelatedSoftwareForSoftware.mockResolvedValueOnce(mockRelatedSoftware) - mockGetRelatedProjectsForSoftware.mockResolvedValueOnce(mockRelatedProjects) - - render( - - - - - - ) - - // validate related software - const relatedSoftware = await screen.findAllByTestId('related-software-item') - expect(relatedSoftware.length).toEqual(mockRelatedSoftware.length) - - // validate related projects - const relatedProjects = await screen.findAllByTestId('related-project-item') - expect(relatedProjects.length).toEqual(mockRelatedProjects.length) - }) - - it('can add related software to software', async() => { - // required prop - softwareState.software.id = 'test-software-id' - - const searchFor = 'Search for software' - const relatedSoftwareFound = [ - {id:'test-id-1',slug:'test-slug-1',brand_name:'Test title 1',short_statement:'Test subtitle 1',status: 'approved'}, - {id:'test-id-2',slug:'test-slug-2',brand_name:'Test title 2',short_statement:'Test subtitle 2',status:'approved'} - ] - - // mock api responses - mockGetRelatedSoftwareForSoftware.mockResolvedValueOnce([]) - mockGetRelatedProjectsForSoftware.mockResolvedValueOnce([]) - // mock search response - mockSearchForRelatedSoftware.mockResolvedValueOnce(relatedSoftwareFound) - // mock add response - mockAddRelatedSoftware.mockResolvedValueOnce({ - status: 200, - message: 'OK' - }) - - render( - - - - - - ) - - // search for related project - const findRelated = screen.getByTestId('find-related-software') - const search = within(findRelated).getByRole('combobox') - fireEvent.change(search, {target: {value: searchFor}}) - - // validate mocked options returned - const options = await screen.findAllByTestId('related-software-option') - expect(options.length).toEqual(relatedSoftwareFound.length) - - // add first item - fireEvent.click(options[0]) - - // validate api calls - expect(mockAddRelatedSoftware).toBeCalledTimes(1) - expect(mockAddRelatedSoftware).toBeCalledWith({ - 'origin': softwareState.software.id, - 'relation': relatedSoftwareFound[0].id, - 'token': mockSession.token, - }) - - // validate 1 item added to list - const relatedSoftware = await screen.findAllByTestId('related-software-item') - expect(relatedSoftware.length).toEqual(1) - }) - - it('can REMOVE related software for software', async () => { - // required prop - softwareState.software.id = 'test-software-id' - - // mock api responses - mockGetRelatedSoftwareForSoftware.mockResolvedValueOnce(mockRelatedSoftware) - mockGetRelatedProjectsForSoftware.mockResolvedValueOnce([]) - // mock delete response as OK - mockDeleteRelatedSoftware.mockResolvedValueOnce({ - status: 200, - message:'OK' - }) - - render( - - - - - - ) - - // get list - const relatedSoftware = await screen.findAllByTestId('related-software-item') - // get delete btn of first item - const delBtn = within(relatedSoftware[0]).getByRole('button', { - name: 'delete' - }) - // delete item - fireEvent.click(delBtn) - - await waitFor(() => { - expect(mockDeleteRelatedSoftware).toBeCalledTimes(1) - expect(mockDeleteRelatedSoftware).toBeCalledWith({ - 'origin': softwareState.software.id, - 'relation': mockRelatedSoftware[0].id, - 'token': mockSession.token, - }) - // validate remaining items - const remainedSoftware = screen.getAllByTestId('related-software-item') - expect(remainedSoftware.length).toEqual(mockRelatedSoftware.length - 1) - }) - }) - - it('can add related project to software', async () => { - // required prop - softwareState.software.id = 'test-software-id' - const searchFor = 'Search for project' - const relatedProjectsFound = [ - {id:'test-id-1', slug:'test-slug-1', title: 'Test title 1', subtitle: 'Test subtitle 1', status: 'approved'}, - {id:'test-id-2',slug:'test-slug-2',title:'Test title 2',subtitle:'Test subtitle 2',status:'approved'} - ] - - // mock api responses - mockGetRelatedSoftwareForSoftware.mockResolvedValueOnce([]) - mockGetRelatedProjectsForSoftware.mockResolvedValueOnce([]) - - mockSearchForRelatedProjectByTitle.mockResolvedValueOnce(relatedProjectsFound) - mockAddRelatedProjectForSoftware.mockResolvedValueOnce({ - status: 200, - message: 'OK' - }) - - render( - - - - - - ) - - // search for related project - const findRelated = screen.getByTestId('find-related-project') - const search = within(findRelated).getByRole('combobox') - fireEvent.change(search, {target: {value: searchFor}}) - - // validate mocked options returned - const options = await screen.findAllByTestId('related-project-option') - expect(options.length).toEqual(relatedProjectsFound.length) - - // add first item - fireEvent.click(options[0]) - - // validate api calls - expect(mockAddRelatedProjectForSoftware).toBeCalledTimes(1) - expect(mockAddRelatedProjectForSoftware).toBeCalledWith({ - 'software': softwareState.software.id, - 'project': relatedProjectsFound[0].id, - 'status': 'approved', - 'token': mockSession.token, - }) - }) - - it('can REMOVE related project for software', async () => { - // required prop - softwareState.software.id = 'test-software-id' - - // mock api responses - mockGetRelatedSoftwareForSoftware.mockResolvedValueOnce([]) - mockGetRelatedProjectsForSoftware.mockResolvedValueOnce(mockRelatedProjects) - // mock delete response as OK - mockDeleteRelatedProjectForSoftware.mockResolvedValueOnce({ - status: 200, - message:'OK' - }) - - render( - - - - - - ) - - // get list - const relatedSoftware = await screen.findAllByTestId('related-project-item') - // get delete btn of first item - const delBtn = within(relatedSoftware[0]).getByRole('button', { - name: 'delete' - }) - // delete item - fireEvent.click(delBtn) - - await waitFor(() => { - expect(mockDeleteRelatedProjectForSoftware).toBeCalledTimes(1) - expect(mockDeleteRelatedProjectForSoftware).toBeCalledWith({ - 'software': softwareState.software.id, - 'project': mockRelatedProjects[0].id, - 'token': mockSession.token, - }) - // validate remaining items - const remainedSoftware = screen.getAllByTestId('related-project-item') - expect(remainedSoftware.length).toEqual(mockRelatedProjects.length - 1) - }) - }) -}) diff --git a/frontend/components/software/edit/related/config.ts b/frontend/components/software/edit/related/config.ts deleted file mode 100644 index c8fdaca57..000000000 --- a/frontend/components/software/edit/related/config.ts +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 dv4all -// -// SPDX-License-Identifier: Apache-2.0 - -export const cfgRelatedItems = { - relatedProject: { - title: 'Related projects', - subtitle: 'This software is related to these projects registered in RSD', - label: 'Find related projects', - help: 'Start typing for suggestions', - validation: { - //custom validation rule, not in used by react-hook-form - minLength: 1, - } - }, - relatedSoftware: { - title: 'Related software', - subtitle: 'This software is related to following software registered in RSD', - label: 'Find related software', - help: 'Start typing for suggestions', - validation: { - //custom validation rule, not in used by react-hook-form - minLength: 1, - } - } -} diff --git a/frontend/components/software/edit/related/index.tsx b/frontend/components/software/edit/related/index.tsx deleted file mode 100644 index 62130b601..000000000 --- a/frontend/components/software/edit/related/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) -// -// SPDX-License-Identifier: Apache-2.0 - -import {Suspense} from 'react' - -import ContentLoader from '~/components/layout/ContentLoader' -import EditSection from '~/components/layout/EditSection' -import RelatedSoftwareForSoftware from './RelatedSoftwareForSoftware' -import RelatedProjectsForProject from './RelatedProjectsForSoftware' - -export default function RelatedSoftwareItems() { - - return ( - // Not sure if Suspense works in this context - }> - -
- -
-
- -
-
-
- ) -} From ec6b3a785fc3fec54b211976643eac4ac1cf2d5a Mon Sep 17 00:00:00 2001 From: "Dusan Mijatovic (PC2020)" Date: Thu, 21 Sep 2023 22:02:29 +0200 Subject: [PATCH 4/6] refactor: remove embeding feature from rsd --- frontend/components/AppFooter/index.tsx | 3 +-- frontend/components/AppHeader/index.tsx | 9 +------- frontend/components/GoBackLink.test.tsx | 13 ----------- frontend/components/GoBackLink.tsx | 22 ------------------- frontend/components/layout/ContentHeader.tsx | 8 ++----- frontend/config/getSettingsServerSide.test.ts | 3 ++- frontend/config/getSettingsServerSide.ts | 3 --- frontend/config/rsdSettingsReducer.ts | 8 ------- frontend/config/useRsdSettings.tsx | 11 ++-------- frontend/next.headers.js | 10 +++++---- frontend/public/embed_example.html | 19 ---------------- 11 files changed, 14 insertions(+), 95 deletions(-) delete mode 100644 frontend/components/GoBackLink.test.tsx delete mode 100644 frontend/components/GoBackLink.tsx delete mode 100644 frontend/public/embed_example.html diff --git a/frontend/components/AppFooter/index.tsx b/frontend/components/AppFooter/index.tsx index d07eb1b8b..824c42eca 100644 --- a/frontend/components/AppFooter/index.tsx +++ b/frontend/components/AppFooter/index.tsx @@ -15,8 +15,7 @@ import OrganisationLogo from './OrganisationLogo' import ContactEmail from './ContactEmail' export default function AppFooter () { - const {pages,links,embedMode,host} = useRsdSettings() - if (embedMode === true) return null + const {pages,links,host} = useRsdSettings() return (