diff --git a/frontend/components/admin/AdminNav.tsx b/frontend/components/admin/AdminNav.tsx index 0a6926c4a..5e950ca0a 100644 --- a/frontend/components/admin/AdminNav.tsx +++ b/frontend/components/admin/AdminNav.tsx @@ -30,6 +30,7 @@ import Diversity3Icon from '@mui/icons-material/Diversity3' import CategoryIcon from '@mui/icons-material/Category' import TerminalIcon from '@mui/icons-material/Terminal' import ListAltIcon from '@mui/icons-material/ListAlt' +import HubIcon from '@mui/icons-material/Hub' import {editMenuItemButtonSx} from '~/config/menuItems' @@ -106,6 +107,12 @@ export const adminPages = { icon: , path: '/admin/mentions', }, + remote_rsd: { + title: 'Remotes', + subtitle: '', + icon: , + path: '/admin/remote-rsd', + }, logs:{ title: 'Error logs', subtitle: '', diff --git a/frontend/components/admin/remote-rsd/NoRemotesAlert.tsx b/frontend/components/admin/remote-rsd/NoRemotesAlert.tsx new file mode 100644 index 000000000..3b8609ed1 --- /dev/null +++ b/frontend/components/admin/remote-rsd/NoRemotesAlert.tsx @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import Alert from '@mui/material/Alert' +import AlertTitle from '@mui/material/AlertTitle' + +export default function NoRemotesAlert() { + return ( + + No remote RSD instances defined + To add remote RSD instance use Add button on the right. + + ) +} diff --git a/frontend/components/admin/remote-rsd/RemoteRsdList.tsx b/frontend/components/admin/remote-rsd/RemoteRsdList.tsx new file mode 100644 index 000000000..aad6c8b14 --- /dev/null +++ b/frontend/components/admin/remote-rsd/RemoteRsdList.tsx @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 dv4all +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useContext, useState} from 'react' +import List from '@mui/material/List' + +import ContentLoader from '~/components/layout/ContentLoader' +import PaginationContext from '~/components/pagination/PaginationContext' +import RemoteRsdListItem from './RemoteRsdListItem' +import NoRemotesAlert from './NoRemotesAlert' +import RemoveRemoteRsdModal, {RemoteRsdModalProps} from './RemoveRemoteRsdModal' +import {RemoteRsd} from './apiRemoteRsd' + + +type OrganisationsAdminListProps = Readonly<{ + remoteRsd: RemoteRsd[] + loading: boolean + onDelete: (id:string)=>void + onEdit: (item:RemoteRsd)=>void +}> + +export default function RemoteRsdList({remoteRsd,loading,onDelete,onEdit}:OrganisationsAdminListProps) { + const {pagination:{page}} = useContext(PaginationContext) + const [modal, setModal] = useState({ + open: false + }) + + if (loading && !page) return
+ + if (remoteRsd.length===0) return
+ + return ( + <> + + { + remoteRsd.map(item => { + return ( + setModal({ + open: true, + item + })} + onEdit={()=>onEdit(item)} + /> + ) + }) + } + + { + modal.open && modal.item ? + { + setModal({ + open: false + }) + }} + onDelete={() => { + // call remove method if id present + if (modal.item && modal.item?.id) onDelete(modal.item?.id) + setModal({ + open: false + }) + }} + /> + : null + } + + ) +} diff --git a/frontend/components/admin/remote-rsd/RemoteRsdListItem.tsx b/frontend/components/admin/remote-rsd/RemoteRsdListItem.tsx new file mode 100644 index 000000000..7e5036808 --- /dev/null +++ b/frontend/components/admin/remote-rsd/RemoteRsdListItem.tsx @@ -0,0 +1,109 @@ +// 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 IconButton from '@mui/material/IconButton' +import ListItem from '@mui/material/ListItem' +import DeleteIcon from '@mui/icons-material/Delete' +import EditIcon from '@mui/icons-material/Edit' +import ListItemText from '@mui/material/ListItemText' +import ListItemAvatar from '@mui/material/ListItemAvatar' +import Avatar from '@mui/material/Avatar' + +import {RemoteRsd} from './apiRemoteRsd' + +type RemoteRsdItemProps = Readonly<{ + item: RemoteRsd, + onDelete: () => void + onEdit: () => void +}> + +export default function RemoteRsdListItem({item, onEdit, onDelete}: RemoteRsdItemProps) { + const logo = `${item.domain}/favicon.ico` + return ( + + { + onEdit() + }} + sx={{marginRight: '1rem'}} + > + + + + { + onDelete() + }} + sx={{margin: '0rem'}} + > + + + + } + className="transition shadow-sm border bg-base-100 rounded hover:shadow-lg" + sx={{ + // this makes space for buttons + padding:'0.5rem 4.5rem 0.5rem 1rem', + margin: '0.5rem 0rem' + }} + > + + + {item.label.slice(0,3)} + + + + {item.domain} +
+ + {item.active ? + Update every {item.scrape_interval_minutes ?? 0} minutes. + : + Not active. + } + {item.active ? + Last update: {item.scraped_at ? new Date(item.scraped_at).toLocaleString() : 'never'} + : null + } + {item.last_err_msg ? + Last error: {item.last_err_msg ?? 'no errors'} + : null + } + + + } + /> + +
+ ) +} diff --git a/frontend/components/admin/remote-rsd/RemoteRsdModal.tsx b/frontend/components/admin/remote-rsd/RemoteRsdModal.tsx new file mode 100644 index 000000000..b505fcc68 --- /dev/null +++ b/frontend/components/admin/remote-rsd/RemoteRsdModal.tsx @@ -0,0 +1,212 @@ +// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2022 - 2023 dv4all +// SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) (dv4all) +// SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useEffect, useState} from 'react' + +import Button from '@mui/material/Button' +import useMediaQuery from '@mui/material/useMediaQuery' +import Dialog from '@mui/material/Dialog' +import DialogTitle from '@mui/material/DialogTitle' +import DialogContent from '@mui/material/DialogContent' +import DialogActions from '@mui/material/DialogActions' +import CircularProgress from '@mui/material/CircularProgress' + +import {useForm} from 'react-hook-form' + +import {useDebounce} from '~/utils/useDebounce' +import TextFieldWithCounter from '~/components/form/TextFieldWithCounter' +import SubmitButtonWithListener from '~/components/form/SubmitButtonWithListener' +import ControlledSwitch from '~/components/form/ControlledSwitch' +import config from './config' +import {EditRemoteRsd, isValidRemoteRsdUrl} from './apiRemoteRsd' + +type RemoteRsdModalProps = Readonly<{ + remoteRsd?: EditRemoteRsd + onCancel: () => void, + onSubmit: (item: EditRemoteRsd) => void +}> + +const formId='add-remote-rsd-form' + +export default function RemoteRsdModal({remoteRsd,onCancel,onSubmit}:RemoteRsdModalProps) { + const smallScreen = useMediaQuery('(max-width:600px)') + const [validating, setValidating]=useState(false) + const {register, handleSubmit, watch, formState, control, setError} = useForm({ + mode: 'onChange', + defaultValues: remoteRsd + }) + const {errors, isValid, isDirty} = formState + // watch for data change in the form + const [label,domain] = watch(['label','domain']) + // bounce domain value for async validation + const bouncedDomain = useDebounce(domain,700) + + // console.group('RemoteRsdModal') + // console.log('isValid...', isValid) + // console.log('isDirty...', isDirty) + // console.log('errors...', errors) + // console.log('domain...', domain) + // console.log('bouncedDomain...', bouncedDomain) + // console.groupEnd() + + /** + * When bouncedDomain value is changed by debounce we check if domain is valid remote RSD + */ + useEffect(() => { + let abort = false + async function validateRsd() { + const isValid = await isValidRemoteRsdUrl(bouncedDomain) + if (abort) return + if (isValid === false) { + const message = `Failed to connect to ${bouncedDomain} remote endpoint` + setError('domain',{type:'validate',message}) + } + setValidating(false) + } + if (bouncedDomain && bouncedDomain === domain && !errors?.domain) { + validateRsd() + }else if (!domain){ + // fix: remove validating/spinner when no slug + setValidating(false) + } + return ()=>{abort=true} + },[bouncedDomain,domain,errors?.domain,setError]) + + useEffect(()=>{ + // As soon as the domain value start changing we signal to user that we need to validate new domain. + // New domain is "debounced" into variable bouncedDomain after the user stops typing. + // Another useEffect monitors bouncedDomain value and performs the validation. + // Validating flag disables Save button from the moment the slug value is changed until the validation is completed. + if (domain && !errors?.domain){ + // debugger + setValidating(true) + } + },[domain,errors?.domain]) + + function handleCancel(e:any,reason: 'backdropClick' | 'escapeKeyDown') { + // close only on escape, not if user clicks outside of the modal + if (reason==='escapeKeyDown') onCancel() + } + + return ( + + + {config.modalTitle} + +
+ + {/* hidden inputs */} + + + + +
+ : undefined + }} + register={register('domain', { + ...config.domain.validation + })} + /> + +
+ + + +
+ + + + + + + +
+ ) + + function isSubmitDisabled(){ + if (isValid===false) return true + // we need additional check on errors object + // due to custom validation of domain + if (Object.keys(errors).length > 0) return true + if (isDirty===false) return true + if (validating) return true + return false + } +} diff --git a/frontend/components/admin/remote-rsd/RemoveRemoteRsdModal.tsx b/frontend/components/admin/remote-rsd/RemoveRemoteRsdModal.tsx new file mode 100644 index 000000000..de8d12b8f --- /dev/null +++ b/frontend/components/admin/remote-rsd/RemoveRemoteRsdModal.tsx @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useState} from 'react' +import TextField from '@mui/material/TextField' + +import ConfirmDeleteModal from '~/components/layout/ConfirmDeleteModal' +import {RemoteRsd} from './apiRemoteRsd' + +export type RemoteRsdModalProps={ + open: boolean + item?: RemoteRsd +} + +type RemoveRemoteRsdModalProps=Readonly<{ + item: RemoteRsd + onCancel: ()=>void + onDelete: ()=>void +}> + +export default function RemoveRemoteRsdModal({item,onCancel,onDelete}:RemoveRemoteRsdModalProps) { + const [confirmation,setConfirmation] = useState('') + return ( + +

+ Are you sure you want to delete {item.label}? +

+

+ {item.domain} +

+ Type the url to remote RSD exactly as shown above. + } + value = {confirmation} + onChange={({target})=>setConfirmation(target.value)} + sx={{ + width: '100%', + margin: '1rem 0rem' + }} + /> +

+ This will remove remote RSD and all related entries too! +

+ + } + onCancel={onCancel} + onDelete={onDelete} + /> + ) +} diff --git a/frontend/components/admin/remote-rsd/apiRemoteRsd.ts b/frontend/components/admin/remote-rsd/apiRemoteRsd.ts new file mode 100644 index 000000000..19a6705b1 --- /dev/null +++ b/frontend/components/admin/remote-rsd/apiRemoteRsd.ts @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {extractCountFromHeader} from '~/utils/extractCountFromHeader' +import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' +import logger from '~/utils/logger' +import {paginationUrlParams} from '~/utils/postgrestUrl' + +export type EditRemoteRsd={ + id: string + label: string, + domain: string, + active: boolean, + scrape_interval_minutes: number +} + +export type RemoteRsd = EditRemoteRsd & { + scraped_at: string | null + last_err_msg: string | null + created_at: string + updated_at: string +} + +export type NewRemoteRsd=Omit + +type GetRemoteRsdParams={ + page: number, + rows: number, + token?: string + searchFor?:string, + orderBy?:string, +} +export async function getRemoteRsd({page,rows,token,searchFor,orderBy}:GetRemoteRsdParams){ + try{ + let query = paginationUrlParams({rows, page}) + if (searchFor) { + // search in name and short description + query+=`&or=(label.ilike.*${searchFor}*,domain.ilike.*${searchFor}*)` + } + if (orderBy) { + query+=`&order=${orderBy}` + } else { + query+='&order=label.asc' + } + // complete url + const url = `${getBaseUrl()}/remote_rsd?${query}` + + // get community + 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 remoteRsd: RemoteRsd[] = await resp.json() + return { + count: extractCountFromHeader(resp.headers) ?? 0, + remoteRsd + } + } + logger(`getRemoteRsd: ${resp.status}: ${resp.statusText}`,'warn') + return { + count: 0, + remoteRsd: [] + } + }catch(e:any){ + logger(`getRemoteRsd: ${e?.message}`, 'error') + return { + count: 0, + remoteRsd: [] + } + } +} + +export async function addRemoteRsd({data,token}:{data:NewRemoteRsd,token:string}){ + try{ + const url = `${getBaseUrl()}/remote_rsd` + + const resp = await fetch(url,{ + method: 'POST', + headers: { + ...createJsonHeaders(token), + // 'Prefer': 'return=representation' + }, + body: JSON.stringify(data) + }) + + return extractReturnMessage(resp, '') + + }catch(e:any){ + logger(`addRemoteRsd: ${e?.message}`, 'error') + return { + status: 500, + message: e?.message + } + } +} + +export async function patchRemoteRsd({id,data,token}:{id:string,data:Partial,token:string}){ + try{ + const query = `remote_rsd?id=eq.${id}` + const url = `${getBaseUrl()}/${query}` + + const resp = await fetch(url,{ + method: 'PATCH', + headers: { + ...createJsonHeaders(token), + // 'Prefer': 'return=representation' + }, + body: JSON.stringify(data) + }) + + return extractReturnMessage(resp, '') + + }catch(e:any){ + logger(`patchRemoteRsd: ${e?.message}`, 'error') + return { + status: 500, + message: e?.message + } + } +} + +export async function deleteRemoteRsdById({id,token}:{id:string,token:string}){ + try{ + const url = `${getBaseUrl()}/remote_rsd?id=eq.${id}` + + const resp = await fetch(url,{ + method: 'DELETE', + headers: { + ...createJsonHeaders(token) + } + }) + return extractReturnMessage(resp, '') + }catch(e:any){ + logger(`deleteRemoteRsdById: ${e?.message}`, 'error') + return { + status: 500, + message: e?.message + } + } +} + +export async function deleteRemoteSoftwareByRemoteRsdId({id,token}:{id:string,token:string}){ + try{ + const url = `${getBaseUrl()}/remote_software?remote_rsd_id=eq.${id}` + + const resp = await fetch(url,{ + method: 'DELETE', + headers: { + ...createJsonHeaders(token) + }, + }) + return extractReturnMessage(resp, '') + }catch(e:any){ + logger(`deleteRemoteSoftwareByRemoteRsdId: ${e?.message}`, 'error') + return { + status: 500, + message: e?.message + } + } +} + + +export async function isValidRemoteRsdUrl(domain:string){ + try{ + const url = `${domain}/api/v1/rpc/software_overview?limit=1&offset=0` + // basic request + const resp = await fetch(url,{ + // wait max. of 5 seconds + signal: AbortSignal.timeout(5000) + }) + + if (resp.ok){ + return true + } + return false + }catch(e:any){ + logger(`isValidRemoteRsdUrl: ${e?.message}`, 'error') + return false + } +} diff --git a/frontend/components/admin/remote-rsd/config.ts b/frontend/components/admin/remote-rsd/config.ts new file mode 100644 index 000000000..4caae0244 --- /dev/null +++ b/frontend/components/admin/remote-rsd/config.ts @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2022 - 2023 dv4all +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {isProperUrl} from '~/utils/fetchHelpers' + +async function isValidUrl(url:string){ + // validate url syntax first + if (isProperUrl(url)===false){ + return 'Invalid url. Please improve your input' + } + return true +} + +const config = { + modalTitle:'Remote RSD', + label: { + label: 'Name', + help: 'Remote RSD name shown in the card.', + // react-hook-form validation rules + validation: { + required: 'Name is required', + minLength: {value: 3, message: 'Minimum length is 3'}, + maxLength: {value: 50, message: 'Maximum length is 50'}, + } + }, + domain: { + label: 'Remote RSD homepage', + help: 'URL to remote RSD homepage (eq. https://research-software-directory.org/)', + validation: { + required: 'Valid URL is required.', + // we do not show error message for this one, we use only maxLength value + maxLength: {value: 200, message: 'Maximum length is 200'}, + validate: isValidUrl + } + }, + active: { + label: 'Active', + help: 'Include this remote RSD in software search', + validation: { + required: 'Active flag is required.', + } + }, + scrape_interval_minutes: { + label: 'Update interval (in minutes)', + help: 'How often to pull the data?', + validation: { + required: 'Update interval is required', + min:{ + value: 5, + message: 'Minimum value is 5 minutes' + } + } + }, +} + +export default config diff --git a/frontend/components/admin/remote-rsd/index.tsx b/frontend/components/admin/remote-rsd/index.tsx new file mode 100644 index 000000000..46d345dc8 --- /dev/null +++ b/frontend/components/admin/remote-rsd/index.tsx @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useState} from 'react' +import Button from '@mui/material/Button' +import AddIcon from '@mui/icons-material/Add' + +import Searchbox from '~/components/search/Searchbox' +import Pagination from '~/components/pagination/Pagination' +import RemoteRsdModal from './RemoteRsdModal' +import useRemoteRsd from './useRemoteRsd' +import {EditRemoteRsd} from './apiRemoteRsd' +import RemoteRsdList from './RemoteRsdList' + +type ModalProps={ + open: boolean + remoteRsd?: EditRemoteRsd +} + +export default function AdminRemoteRsd() { + const [modal, setModal] = useState() + const {loading, remoteRsd, addRemote,patchRemote, deleteRemote} = useRemoteRsd() + + // console.group('AdminRemoteRsd') + // console.log('loading...',loading) + // console.log('remoteRsd...', remoteRsd) + // console.groupEnd() + + async function submitRemote(data:EditRemoteRsd){ + let done:boolean + if (data.id){ + done = await patchRemote({ + id: data.id, + data + }) + }else{ + done = await addRemote(data) + } + // close modal on success + if (done===true){ + setModal({open:false}) + } + } + + return ( + <> +
+
+
+ + +
+
+ { + setModal({ + open: true, + remoteRsd: item + }) + }} + /> +
+
+
+ +
+
+ { + modal?.open ? + setModal({open:false})} + onSubmit={submitRemote} + /> + : null + } + + ) +} diff --git a/frontend/components/admin/remote-rsd/useRemoteRsd.tsx b/frontend/components/admin/remote-rsd/useRemoteRsd.tsx new file mode 100644 index 000000000..2af77362b --- /dev/null +++ b/frontend/components/admin/remote-rsd/useRemoteRsd.tsx @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useCallback, useEffect, useState} from 'react' + +import {useSession} from '~/auth' +import useSnackbar from '~/components/snackbar/useSnackbar' +import usePaginationWithSearch from '~/utils/usePaginationWithSearch' +import { + addRemoteRsd, deleteRemoteRsdById, deleteRemoteSoftwareByRemoteRsdId, + EditRemoteRsd, getRemoteRsd, patchRemoteRsd, RemoteRsd +} from './apiRemoteRsd' + +export default function useRemoteRsd() { + const {token} = useSession() + const {showErrorMessage} = useSnackbar() + const {searchFor, page, rows, setCount} = usePaginationWithSearch('Find remote by name or url') + const [loading, setLoading] = useState(true) + const [remoteRsd, setRemoteRsd] = useState([]) + + + const loadRemoteRsd = useCallback(async()=>{ + const {remoteRsd,count} = await getRemoteRsd({ + token, + searchFor, + page, + rows + }) + setRemoteRsd(remoteRsd) + setCount(count ?? 0) + setLoading(false) + // we do not include setCount in order to avoid loop + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token, searchFor, page, rows]) + + useEffect(()=>{ + if(token){ + loadRemoteRsd() + } + // we do not include setCount in order to avoid loop + // eslint-disable-next-line react-hooks/exhaustive-deps + },[token, searchFor, page, rows]) + + async function addRemote(data:EditRemoteRsd){ + // console.log('Implement...addRemoteRsd...',data) + const newRemote={ + label: data.label, + domain: data.domain, + active: data.active, + scrape_interval_minutes: data.scrape_interval_minutes + } + const resp = await addRemoteRsd({data:newRemote,token}) + // debugger + if (resp.status!==200){ + showErrorMessage(`Failed to add remote RSD. ${resp.message}`) + return false + }else{ + loadRemoteRsd() + return true + } + } + + async function patchRemote({id,data}:{id:string,data:Partial}){ + // console.log('Implement...patchRemoteRsd...', data) + const resp = await patchRemoteRsd({id,data,token}) + // debugger + if (resp.status!==200){ + showErrorMessage(`Failed to update remote RSD. ${resp.message}`) + return false + }else{ + loadRemoteRsd() + return true + } + } + + async function deleteRemote(id:string){ + // console.log('Implement...deleteRemoteRsd...', id) + let resp + // delete scraped software items first + resp = await deleteRemoteSoftwareByRemoteRsdId({id,token}) + if (resp.status!==200){ + showErrorMessage(`Failed to delete remote RSD. ${resp.message}`) + return false + } + // delete remote rsd entry + resp = await deleteRemoteRsdById({id,token}) + if (resp.status!==200){ + showErrorMessage(`Failed to delete remote RSD. ${resp.message}`) + return false + }else{ + loadRemoteRsd() + return true + } + } + + return { + loading, + remoteRsd, + addRemote, + patchRemote, + deleteRemote + } +} diff --git a/frontend/components/form/TextFieldWithCounter.tsx b/frontend/components/form/TextFieldWithCounter.tsx index 71829ed87..dd1780381 100644 --- a/frontend/components/form/TextFieldWithCounter.tsx +++ b/frontend/components/form/TextFieldWithCounter.tsx @@ -1,11 +1,14 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 import {useEffect, useRef} from 'react' import TextField from '@mui/material/TextField' import HelperTextWithCounter from './HelperTextWithCounter' +import InputAdornment from '@mui/material/InputAdornment' type TextFieldWidthCounterType = { autofocus?:boolean @@ -22,10 +25,12 @@ type TextFieldWidthCounterType = { helperTextMessage?: string helperTextCnt?: string disabled?: boolean + startAdornment?: string | JSX.Element + endAdornment?: string | JSX.Element } export default function TextFieldWithCounter({options, register}: - { options: TextFieldWidthCounterType, register: any }) { + { options: TextFieldWidthCounterType, register: any}) { const inputRef = useRef(null) useEffect(() => { @@ -61,6 +66,10 @@ export default function TextFieldWithCounter({options, register}: count={options?.helperTextCnt ?? ''} /> } + InputProps={{ + startAdornment: options?.startAdornment ? {options?.startAdornment} : undefined, + endAdornment: options?.endAdornment ? {options?.endAdornment} : undefined + }} {...register} /> ) diff --git a/frontend/pages/admin/remote-rsd.tsx b/frontend/pages/admin/remote-rsd.tsx new file mode 100644 index 000000000..3ac37ee8e --- /dev/null +++ b/frontend/pages/admin/remote-rsd.tsx @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import Head from 'next/head' + +import {app} from '~/config/app' +import {rowsPerPageOptions} from '~/config/pagination' +import {useUserSettings} from '~/config/UserSettingsContext' +import DefaultLayout from '~/components/layout/DefaultLayout' +import AdminPageWithNav from '~/components/admin/AdminPageWithNav' +import {adminPages} from '~/components/admin/AdminNav' +import {SearchProvider} from '~/components/search/SearchContext' +import {PaginationProvider} from '~/components/pagination/PaginationContext' +import AdminRemoteRsd from '~/components/admin/remote-rsd' + +const pageTitle = `${adminPages['remote_rsd'].title} | Admin page | ${app.title}` + +const pagination = { + count: 0, + page: 0, + rows: 12, + rowsOptions: rowsPerPageOptions, + labelRowsPerPage:'Per page' +} + + +export default function AdminRemoteRsdPage() { + // use page rows from user settings + const {rsd_page_rows} = useUserSettings() + pagination.rows = rsd_page_rows ?? rowsPerPageOptions[0] + + return ( + + + {pageTitle} + + + + + + + + + + ) +} + +// see documentation https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering +// export async function getServerSideProps(context:GetServerSidePropsContext) { +// 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: { +// count: resp?.count, +// communities: resp.communities +// }, +// } +// }catch(e){ +// return { +// notFound: true, +// } +// } +// }