diff --git a/database/005-create-relations-for-software.sql b/database/005-create-relations-for-software.sql index ce5ee2722..2006cc96c 100644 --- a/database/005-create-relations-for-software.sql +++ b/database/005-create-relations-for-software.sql @@ -1,5 +1,5 @@ --- SPDX-FileCopyrightText: 2021 - 2024 Ewan Cahen (Netherlands eScience Center) --- SPDX-FileCopyrightText: 2021 - 2024 Netherlands eScience Center +-- SPDX-FileCopyrightText: 2021 - 2025 Ewan Cahen (Netherlands eScience Center) +-- SPDX-FileCopyrightText: 2021 - 2025 Netherlands eScience Center -- SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) -- SPDX-FileCopyrightText: 2022 - 2024 dv4all -- SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ) @@ -49,6 +49,7 @@ CREATE TYPE package_manager_type AS ENUM ( 'crates', 'debian', 'dockerhub', + 'ghcr', 'github', 'gitlab', 'golang', diff --git a/frontend/components/software/edit/package-managers/EditPackageManagerModal.tsx b/frontend/components/software/edit/package-managers/EditPackageManagerModal.tsx index e7e0e402b..d812139c3 100644 --- a/frontend/components/software/edit/package-managers/EditPackageManagerModal.tsx +++ b/frontend/components/software/edit/package-managers/EditPackageManagerModal.tsx @@ -3,8 +3,9 @@ // 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: 2024 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 @@ -19,6 +20,8 @@ import useMediaQuery from '@mui/material/useMediaQuery' import {useForm} from 'react-hook-form' +import {useDebounce} from '~/utils/useDebounce' +import ControlledSelect from '~/components/form/ControlledSelect' import ControlledTextField from '~/components/form/ControlledTextField' import SubmitButtonWithListener from '~/components/form/SubmitButtonWithListener' import { @@ -28,7 +31,6 @@ import { } from './apiPackageManager' import PackageManagerInfo from './PackageManagerInfo' import {config} from './config' -import ControlledSelect from '~/components/form/ControlledSelect' type EditPackageManagerModalProps = Readonly<{ open: boolean, @@ -56,9 +58,9 @@ export default function EditPackageManagerModal({open, onCancel, onSubmit, packa ...package_manager } }) - // extract + // extract form states and possible errors const {isValid, isDirty, errors} = formState - // const formData = watch() + // watch for value changes in the form const [ url, package_manager_form, @@ -70,32 +72,34 @@ export default function EditPackageManagerModal({open, onCancel, onSubmit, packa 'download_count_scraping_disabled_reason', 'reverse_dependency_count_scraping_disabled_reason' ]) + // take the last url value + const bouncedUrl = useDebounce(url, 700) + const packageManagerServices = getPackageManagerServices(package_manager_form) useEffect(() => { async function fetchPackageManagerType(){ // extract manager type from url - const pm_key = await getPackageManagerTypeFromUrl(url) + const pm_key = await getPackageManagerTypeFromUrl(bouncedUrl) // save value setValue('package_manager', pm_key as PackageManagerTypes, { shouldValidate: true, shouldDirty: true }) } - if (typeof errors['url'] === 'undefined' && - url?.length > 5 && + if (!errors?.url && url?.length > 5 && + bouncedUrl === url && // only for new items package_manager?.id === null ) { fetchPackageManagerType() } - },[url,setValue,errors,package_manager?.id]) - - const packageManagerServices = getPackageManagerServices(package_manager_form) + },[bouncedUrl,url,setValue,errors?.url,package_manager?.id]) // console.group('EditPackageManagerModal') // console.log('isValid...', isValid) // console.log('isDirty...', isDirty) // console.log('url...', url) + // console.log('bouncedUrl...', bouncedUrl) // console.log('package_manager...', package_manager) // console.log('package_manager_form...', package_manager_form) // console.log('packageManagerOptions...', packageManagerOptions) @@ -236,6 +240,7 @@ export default function EditPackageManagerModal({open, onCancel, onSubmit, packa function isSaveDisabled() { if (isValid === false) return true if (isDirty === false) return true + if (url !== bouncedUrl) return true return false } } diff --git a/frontend/components/software/edit/package-managers/apiPackageManager.ts b/frontend/components/software/edit/package-managers/apiPackageManager.ts index 9f7d0e369..8dd066994 100644 --- a/frontend/components/software/edit/package-managers/apiPackageManager.ts +++ b/frontend/components/software/edit/package-managers/apiPackageManager.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2024 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 dv4all // SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) @@ -11,9 +11,9 @@ import logger from '~/utils/logger' import {createJsonHeaders, extractErrorMessages, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' -export type PackageManagerSettings={ +export type PackageManagerSettings = { name: string, - icon: string|null, + icon: string | null, hostname: string[], services: string[] } @@ -43,7 +43,7 @@ export const packageManagerSettings = { hostname: ['community.chocolatey.org'], services: [] }, - debian:{ + debian: { name: 'Debian', icon: '/images/debian-logo.svg', hostname: ['packages.debian.org'], @@ -55,6 +55,12 @@ export const packageManagerSettings = { hostname: ['hub.docker.com'], services: ['downloads'] }, + ghcr: { + name: 'Github Container Registry', + icon: '/images/github-logo.svg', + hostname: ['ghcr.io'], + services: [] + }, github: { name: 'Github', icon: '/images/github-logo.svg', @@ -64,7 +70,7 @@ export const packageManagerSettings = { gitlab: { name: 'Gitlab', icon: '/images/gitlab-icon-rgb.svg', - hostname: ['gitlab.com','registry.gitlab.com'], + hostname: ['gitlab.com', 'registry.gitlab.com'], services: [] }, golang: { @@ -82,7 +88,7 @@ export const packageManagerSettings = { npm: { name: 'NPM', icon: '/images/npm-logo-64.png', - hostname: ['www.npmjs.com','npmjs.com'], + hostname: ['www.npmjs.com', 'npmjs.com'], services: ['dependents'] }, pypi: { @@ -91,13 +97,13 @@ export const packageManagerSettings = { hostname: ['pypi.org'], services: ['dependents'] }, - sonatype:{ + sonatype: { name: 'Sonatype', icon: '/images/sonatype-logo.svg', hostname: ['central.sonatype.com'], services: ['dependents'] }, - snapcraft:{ + snapcraft: { name: 'Snapcraft', icon: '/images/snapcraft-logo.svg', hostname: ['snapcraft.io'], @@ -115,7 +121,7 @@ export type PackageManagerTypes = keyof typeof packageManagerSettings export type NewPackageManager = { - id: string|null + id: string | null software: string, url: string, package_manager: PackageManagerTypes | null, @@ -133,13 +139,16 @@ export type PackageManager = NewPackageManager & { reverse_dependency_count_scraping_disabled_reason: string | null, } -export async function getPackageManagers({software, token}: { software: string, token?: string }): Promise { +export async function getPackageManagers({software, token}: { + software: string, + token?: string +}): Promise { try { const query = `software=eq.${software}&order=position.asc,package_manager.asc` const url = `${getBaseUrl()}/package_manager?${query}` // make request - const resp = await fetch(url,{ + const resp = await fetch(url, { method: 'GET', headers: { ...createJsonHeaders(token), @@ -149,20 +158,20 @@ export async function getPackageManagers({software, token}: { software: string, if (resp.status === 200) { return await resp.json() } - logger(`getPackageManagers...${resp.status} ${resp.statusText}`,'warn') + logger(`getPackageManagers...${resp.status} ${resp.statusText}`, 'warn') return [] } catch (e: any) { - logger(`getPackageManagers failed. ${e.message}`,'error') + logger(`getPackageManagers failed. ${e.message}`, 'error') return [] } } -export async function postPackageManager({data, token}: {data: NewPackageManager, token: string }) { +export async function postPackageManager({data, token}: { data: NewPackageManager, token: string }) { try { const url = `${getBaseUrl()}/package_manager` // ELSE add new package manager - const resp = await fetch(url,{ + const resp = await fetch(url, { method: 'POST', headers: { ...createJsonHeaders(token), @@ -187,12 +196,16 @@ type UpdatePackageManager = { reverse_dependency_count_scraping_disabled_reason: string | null, } -export async function patchPackageManager({id, data, token}: {id:string, data: UpdatePackageManager, token: string }) { +export async function patchPackageManager({id, data, token}: { + id: string, + data: UpdatePackageManager, + token: string +}) { try { - const query=`id=eq.${id}` + const query = `id=eq.${id}` const url = `${getBaseUrl()}/package_manager?${query}` // make request - const resp = await fetch(url,{ + const resp = await fetch(url, { method: 'PATCH', headers: { ...createJsonHeaders(token), @@ -243,8 +256,8 @@ export async function patchPackageManagers({items, token}: { items: PackageManag } } -export async function patchPackageManagerItem({id,key,value,token}: - { id:string,key:string,value:any,token:string }) { +export async function patchPackageManagerItem({id, key, value, token}: + { id: string, key: string, value: any, token: string }) { try { const url = `/api/v1/package_manager?id=eq.${id}` const resp = await fetch(url, { @@ -254,7 +267,7 @@ export async function patchPackageManagerItem({id,key,value,token}: }, // just update position! body: JSON.stringify({ - [key]:value + [key]: value }) }) // extract errors @@ -268,7 +281,7 @@ export async function patchPackageManagerItem({id,key,value,token}: } } -export async function deletePackageManager({id,token}:{id: string,token:string}) { +export async function deletePackageManager({id, token}: { id: string, token: string }) { try { const url = `/api/v1/package_manager?id=eq.${id}` const resp = await fetch(url, { @@ -288,14 +301,14 @@ export async function deletePackageManager({id,token}:{id: string,token:string}) } } -export async function getPackageManagerTypeFromUrl(url:string) { +export async function getPackageManagerTypeFromUrl(url: string) { try { const urlObject = new URL(url) const keys = Object.keys(packageManagerSettings) as PackageManagerTypes[] // find first key to match the hostname const pm_key = keys.find(key => { - const manager:PackageManagerSettings = packageManagerSettings[key] + const manager: PackageManagerSettings = packageManagerSettings[key] // match hostname return manager.hostname.includes(urlObject.hostname) }) @@ -315,7 +328,7 @@ export async function getPackageManagerTypeFromUrl(url:string) { } ) if (resp.status === 200) { - const platform_type:PackageManagerTypes = await resp.json() + const platform_type: PackageManagerTypes = await resp.json() if (platform_type !== null) { return platform_type } @@ -327,11 +340,11 @@ export async function getPackageManagerTypeFromUrl(url:string) { } } -export function getPackageManagerServices(pm_key:PackageManagerTypes|null){ +export function getPackageManagerServices(pm_key: PackageManagerTypes | null) { // no services if no key - if (pm_key===null) return [] + if (pm_key === null) return [] // return services if key found - if (Object.hasOwn(packageManagerSettings,pm_key)===true){ + if (Object.hasOwn(packageManagerSettings, pm_key) === true) { return packageManagerSettings[pm_key].services } // no services if key not found