From 63c96a3be67209df0624f2ae353da50d93c85940 Mon Sep 17 00:00:00 2001 From: "Dusan Mijatovic (PC2020)" Date: Fri, 6 Jun 2025 21:18:40 +0200 Subject: [PATCH] feat: upgrade to v3.4.1 and add news to module of settings.json --- deployment/.env.example | 9 - deployment/docker-compose.yml | 42 ++-- docker-compose.yml | 40 ++-- .../docs/03-rsd-instance/02-configurations.md | 104 +++++++--- .../auth/api/__mocks__/useLoginProviders.tsx | 31 ++- frontend/auth/api/getLoginProviders.tsx | 52 ++--- frontend/auth/api/useLoginProviders.tsx | 32 +--- frontend/auth/loginProvidersContext.tsx | 31 +++ frontend/auth/providers/azure.ts | 64 ------- frontend/auth/providers/helmholtzid.ts | 75 -------- frontend/auth/providers/linkedin.ts | 69 ------- frontend/auth/providers/local.ts | 15 -- frontend/auth/providers/orcid.ts | 68 ------- frontend/auth/providers/surfconext.ts | 71 ------- .../components/AppHeader/AddMenu.test.tsx | 55 +++++- frontend/components/AppHeader/AddMenu.tsx | 68 +++---- .../components/AppHeader/AppHeader.test.tsx | 11 +- .../AppHeader/__mocks__/useAddItemMenu.tsx | 59 ++++++ .../components/AppHeader/useAddItemMenu.tsx | 59 ++++++ .../SearchItemIcon.tsx | 7 +- .../apiGlobalSearch.ts | 5 +- .../GlobalSearchAutocomplete/index.tsx | 5 + frontend/components/admin/logs/apiLogs.ts | 6 +- .../admin/mentions/MentionsOverview.tsx | 6 +- .../admin/organisations/apiOrganisation.tsx | 6 +- .../admin/remote-rsd/apiRemoteRsd.ts | 2 +- .../rsd-contributors/apiRsdContributors.ts | 2 +- .../components/admin/rsd-info/apiRsdInfo.ts | 2 +- .../components/admin/rsd-users/apiRsdUsers.ts | 6 +- .../apiSoftwareHighlights.ts | 6 +- .../components/category/CategoriesDialog.tsx | 2 +- .../components/communities/apiCommunities.ts | 6 +- .../components/form/AsyncAutocompleteSC.tsx | 26 +-- .../form/ControlledSlugTextField.tsx | 30 +-- .../components/form/ControlledTextField.tsx | 20 +- frontend/components/form/SlugTextField.tsx | 28 +-- .../components/form/TextFieldWithCounter.tsx | 22 ++- frontend/components/layout/LogoMenu.tsx | 8 +- frontend/components/login/LoginButton.tsx | 52 ++--- frontend/components/login/LoginDialog.tsx | 3 +- frontend/components/login/LoginProviders.tsx | 10 +- .../components/menu/IconBtnMenuOnAction.tsx | 8 +- frontend/components/news/add/AddNewsCard.tsx | 10 +- frontend/components/news/apiNews.tsx | 6 +- frontend/components/news/edit/index.tsx | 9 +- .../organisation/apiOrganisations.ts | 6 +- .../components/profile/ProfileSearchPanel.tsx | 2 +- frontend/components/profile/apiProfile.ts | 2 +- .../profile/overview/PersonCard.tsx | 77 ++++++++ .../profile/overview/PersonListItem.tsx | 63 ++++++ .../profile/overview/PersonMetrics.tsx | 59 ++++++ .../profile/overview/PersonsGrid.tsx | 27 +++ .../profile/overview/PersonsList.tsx | 24 +++ .../profile/overview/apiPersonsOverview.ts | 77 ++++++++ frontend/components/profile/tabs/index.tsx | 67 ++++--- .../information/AutosaveProjectPeriod.tsx | 20 +- frontend/components/search/SearchInput.tsx | 8 +- .../components/search/useSearchParams.tsx | 8 +- frontend/components/snackbar/MuiSnackbar.tsx | 6 +- .../components/software/ContactPersonCard.tsx | 2 +- .../components/software/ContributorsList.tsx | 2 +- .../edit/links/AutosaveConceptDoi.tsx | 12 +- .../components/user/communities/index.tsx | 2 +- .../organisations/useUserOrganisations.tsx | 2 +- .../user/project/useUserProjects.tsx | 2 +- .../user/settings/profile/ProfileInput.tsx | 2 +- .../user/settings/profile/apiUserProfile.ts | 1 + .../user/software/useUserSoftware.tsx | 2 +- frontend/config/UserSettingsContext.tsx | 4 +- frontend/config/getSettingsServerSide.ts | 32 +++- frontend/config/menuItems.tsx | 12 +- frontend/config/rsdSettingsReducer.ts | 6 +- frontend/config/useModules.ts | 24 --- frontend/next.config.ts | 8 +- frontend/next.rewrites.ts | 12 +- frontend/pages/_app.tsx | 26 ++- frontend/pages/api/fe/auth/index.ts | 179 ------------------ frontend/pages/communities/index.tsx | 3 +- frontend/pages/index.tsx | 11 +- frontend/pages/invite/rsd/[id].tsx | 8 +- frontend/pages/login/index.tsx | 9 +- frontend/pages/news/index.tsx | 8 + .../pages/{profile => persons}/[id]/[tab].tsx | 56 ++++-- frontend/pages/persons/[id]/index.tsx | 49 +++++ frontend/pages/persons/index.tsx | 175 +++++++++++++++++ frontend/public/data/settings.json | 2 +- frontend/utils/editOrganisation.ts | 8 +- frontend/utils/jest/WithAppContext.tsx | 10 +- 88 files changed, 1342 insertions(+), 1021 deletions(-) create mode 100644 frontend/auth/loginProvidersContext.tsx delete mode 100644 frontend/auth/providers/azure.ts delete mode 100644 frontend/auth/providers/helmholtzid.ts delete mode 100644 frontend/auth/providers/linkedin.ts delete mode 100644 frontend/auth/providers/local.ts delete mode 100644 frontend/auth/providers/orcid.ts delete mode 100644 frontend/auth/providers/surfconext.ts create mode 100644 frontend/components/AppHeader/__mocks__/useAddItemMenu.tsx create mode 100644 frontend/components/AppHeader/useAddItemMenu.tsx create mode 100644 frontend/components/profile/overview/PersonCard.tsx create mode 100644 frontend/components/profile/overview/PersonListItem.tsx create mode 100644 frontend/components/profile/overview/PersonMetrics.tsx create mode 100644 frontend/components/profile/overview/PersonsGrid.tsx create mode 100644 frontend/components/profile/overview/PersonsList.tsx create mode 100644 frontend/components/profile/overview/apiPersonsOverview.ts delete mode 100644 frontend/config/useModules.ts delete mode 100644 frontend/pages/api/fe/auth/index.ts rename frontend/pages/{profile => persons}/[id]/[tab].tsx (81%) create mode 100644 frontend/pages/persons/[id]/index.tsx create mode 100644 frontend/pages/persons/index.tsx diff --git a/deployment/.env.example b/deployment/.env.example index 0345568..ff0df34 100644 --- a/deployment/.env.example +++ b/deployment/.env.example @@ -81,10 +81,6 @@ SURFCONEXT_CLIENT_ID=kin-rpd-demo.com SURFCONEXT_REDIRECT=https://ubuntu2204sudo.demo-nlesc.src.surf-hosted.nl/auth/login/surfconext # consumed by: authentication, frontend/utils/loginHelpers SURFCONEXT_WELL_KNOWN_URL=https://connect.test.surfconext.nl/.well-known/openid-configuration -# consumed by: authentication, frontend/utils/loginHelpers -SURFCONEXT_SCOPES=openid -# consumed by: frontend/utils/loginHelpers -SURFCONEXT_RESPONSE_MODE=form_post # ORCID # consumed by: authentication, frontend/utils/loginHelpers @@ -107,11 +103,6 @@ AZURE_CLIENT_ID= AZURE_REDIRECT= # consumed by: authentication, frontend/utils/loginHelpers AZURE_WELL_KNOWN_URL= -# consumed by: authentication, frontend/utils/loginHelpers -AZURE_SCOPES=openid+email+profile -# consumed by: authentication, frontend/utils/loginHelpers -AZURE_LOGIN_PROMPT=select_account -# consumed by: frontend # the name displayed to users when multiple providers are configured AZURE_DISPLAY_NAME="Microsoft Azure AD" # consumed by: frontend diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index b26633b..21e2246 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -5,7 +5,7 @@ services: database: container_name: database - image: ghcr.io/research-software-directory/rsd-saas/database:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/database:v3.4.1 expose: - 5432 environment: @@ -25,7 +25,7 @@ services: backend: container_name: backend - image: ghcr.io/research-software-directory/rsd-saas/backend:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/backend:v3.4.1 expose: - 3500 environment: @@ -43,7 +43,7 @@ services: auth: container_name: auth - image: ghcr.io/research-software-directory/rsd-saas/auth:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/auth:v3.4.1 expose: - 7000 environment: @@ -59,6 +59,7 @@ services: - HELMHOLTZID_REDIRECT - HELMHOLTZID_WELL_KNOWN_URL - HELMHOLTZID_SCOPES + - HELMHOLTZID_ALLOW_EXTERNAL_USERS - ORCID_CLIENT_ID - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE @@ -67,11 +68,12 @@ services: - AZURE_REDIRECT - AZURE_WELL_KNOWN_URL - AZURE_ORGANISATION + - AZURE_DISPLAY_NAME + - AZURE_DESCRIPTION_HTML - LINKEDIN_CLIENT_ID - LINKEDIN_REDIRECT - LINKEDIN_REDIRECT_COUPLE - LINKEDIN_WELL_KNOWN_URL - - HELMHOLTZID_ALLOW_EXTERNAL_USERS - AUTH_SURFCONEXT_CLIENT_SECRET - AUTH_HELMHOLTZID_CLIENT_SECRET - AUTH_ORCID_CLIENT_SECRET @@ -87,7 +89,7 @@ services: scrapers: container_name: scrapers - image: ghcr.io/research-software-directory/rsd-saas/scrapers:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/scrapers:v3.4.1 environment: # it uses values from .env file - POSTGREST_URL @@ -108,7 +110,7 @@ services: restart: unless-stopped background-services: - image: ghcr.io/research-software-directory/rsd-saas/background-services:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/background-services:v3.4.1 environment: # it uses values from .env file - POSTGRES_DB_HOST @@ -125,42 +127,26 @@ services: container_name: frontend image: ghcr.io/research-software-directory/kin-rpd/frontend:latest environment: - # it uses values from .env file + # it uses values from .env file - POSTGREST_URL - PGRST_JWT_SECRET - - RSD_AUTH_URL - - RSD_AUTH_PROVIDERS - - RSD_AUTH_COUPLE_PROVIDERS - RSD_REVERSE_PROXY_URL + - RSD_AUTH_URL + - CROSSREF_CONTACT_EMAIL - MATOMO_URL - MATOMO_ID - - SURFCONEXT_CLIENT_ID - - SURFCONEXT_REDIRECT - - SURFCONEXT_WELL_KNOWN_URL - - SURFCONEXT_SCOPES - - SURFCONEXT_RESPONSE_MODE - - HELMHOLTZID_CLIENT_ID - - HELMHOLTZID_REDIRECT - - HELMHOLTZID_WELL_KNOWN_URL - - HELMHOLTZID_SCOPES - - HELMHOLTZID_RESPONSE_MODE + # link your orcid account + - RSD_AUTH_COUPLE_PROVIDERS - ORCID_CLIENT_ID - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE - ORCID_WELL_KNOWN_URL - ORCID_SCOPES - - AZURE_CLIENT_ID - - AZURE_REDIRECT - - AZURE_WELL_KNOWN_URL - - AZURE_SCOPES - - AZURE_LOGIN_PROMPT - - AZURE_DISPLAY_NAME - - AZURE_DESCRIPTION_HTML + # link your linkedin account - LINKEDIN_CLIENT_ID - LINKEDIN_REDIRECT - LINKEDIN_REDIRECT_COUPLE - LINKEDIN_WELL_KNOWN_URL - - CROSSREF_CONTACT_EMAIL expose: - 3000 depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 80c226a..796fe79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: database: - image: ghcr.io/research-software-directory/rsd-saas/database:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/database:v3.4.1 ports: # enable connection from outside (development mode) - "5432:5432" @@ -23,7 +23,7 @@ services: - net backend: - image: ghcr.io/research-software-directory/rsd-saas/backend:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/backend:v3.4.1 expose: - 3500 environment: @@ -39,7 +39,7 @@ services: - net auth: - image: ghcr.io/research-software-directory/rsd-saas/auth:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/auth:v3.4.1 ports: - 5005:5005 expose: @@ -57,6 +57,7 @@ services: - HELMHOLTZID_REDIRECT - HELMHOLTZID_WELL_KNOWN_URL - HELMHOLTZID_SCOPES + - HELMHOLTZID_ALLOW_EXTERNAL_USERS - ORCID_CLIENT_ID - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE @@ -65,11 +66,12 @@ services: - AZURE_REDIRECT - AZURE_WELL_KNOWN_URL - AZURE_ORGANISATION + - AZURE_DISPLAY_NAME + - AZURE_DESCRIPTION_HTML - LINKEDIN_CLIENT_ID - LINKEDIN_REDIRECT - LINKEDIN_REDIRECT_COUPLE - LINKEDIN_WELL_KNOWN_URL - - HELMHOLTZID_ALLOW_EXTERNAL_USERS - AUTH_SURFCONEXT_CLIENT_SECRET - AUTH_HELMHOLTZID_CLIENT_SECRET - AUTH_ORCID_CLIENT_SECRET @@ -90,7 +92,7 @@ services: ] scrapers: - image: ghcr.io/research-software-directory/rsd-saas/scrapers:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/scrapers:v3.4.1 environment: # it uses values from .env file - POSTGREST_URL @@ -110,7 +112,7 @@ services: - net background-services: - image: ghcr.io/research-software-directory/rsd-saas/background-services:v3.3.0 + image: ghcr.io/research-software-directory/rsd-saas/background-services:v3.4.1 environment: # it uses values from .env file - POSTGRES_DB_HOST @@ -134,39 +136,23 @@ services: # it uses values from .env file - POSTGREST_URL - PGRST_JWT_SECRET - - RSD_AUTH_URL - - RSD_AUTH_PROVIDERS - - RSD_AUTH_COUPLE_PROVIDERS - RSD_REVERSE_PROXY_URL + - RSD_AUTH_URL + - CROSSREF_CONTACT_EMAIL - MATOMO_URL - MATOMO_ID - - SURFCONEXT_CLIENT_ID - - SURFCONEXT_REDIRECT - - SURFCONEXT_WELL_KNOWN_URL - - SURFCONEXT_SCOPES - - SURFCONEXT_RESPONSE_MODE - - HELMHOLTZID_CLIENT_ID - - HELMHOLTZID_REDIRECT - - HELMHOLTZID_WELL_KNOWN_URL - - HELMHOLTZID_SCOPES - - HELMHOLTZID_RESPONSE_MODE + # link your orcid account + - RSD_AUTH_COUPLE_PROVIDERS - ORCID_CLIENT_ID - ORCID_REDIRECT - ORCID_REDIRECT_COUPLE - ORCID_WELL_KNOWN_URL - ORCID_SCOPES - - AZURE_CLIENT_ID - - AZURE_REDIRECT - - AZURE_WELL_KNOWN_URL - - AZURE_SCOPES - - AZURE_LOGIN_PROMPT - - AZURE_DISPLAY_NAME - - AZURE_DESCRIPTION_HTML + # link your linkedin account - LINKEDIN_CLIENT_ID - LINKEDIN_REDIRECT - LINKEDIN_REDIRECT_COUPLE - LINKEDIN_WELL_KNOWN_URL - - CROSSREF_CONTACT_EMAIL expose: - 3000 depends_on: diff --git a/documentation/docs/03-rsd-instance/02-configurations.md b/documentation/docs/03-rsd-instance/02-configurations.md index 6ecc91e..e8f3489 100644 --- a/documentation/docs/03-rsd-instance/02-configurations.md +++ b/documentation/docs/03-rsd-instance/02-configurations.md @@ -10,7 +10,7 @@ RSD offers following customization options: :::tip -- Main locations for customizing RSD are: +- The main locations for customising the RSD are: - `.env` file at the same location as your docker-compose.yml - `settings.json` that should be (volume) mounted into the frontend service in your docker-compose.yml - When configuring your production instance, replace `localhost` and `www.localhost` with the domain of your RSD @@ -25,23 +25,28 @@ The RSD supports the following third party OpenID Connect authentication service - [ORCID](#enable-orcid-authentication-and-coupling) - [SURFconext](#enable-surfconext-authentication) - [Helmholtz AI](#enable-helmholtz-ai-authentication) +- [LinkedIn](#enable-linkedin-authentication) :::warning -RSD requires one of mentioned authentication providers to be used in the production. Please obtain the required information for setting up the authentication service directly from the provider. The required information about the authentication provider is stored in `.env` file (environment variables). After changing any value in the .env file you should restart the RSD instance. +The RSD requires one of the aforementioned authentication providers to be used in production. Please obtain the required information for setting up the authentication service directly from the provider. The required information about the authentication provider is stored in `.env` file (environment variables). After changing any value in the `.env` file, you should restart the RSD instance. ::: +### Two access types + +Each provider can be enabled for everyone, or only to users that have an invite that was generated by RSD admins (see [this section](/rsd-instance/administration/#rsd-invites)). This way, you can restrict the usage of the RSD to only people you trust. See the env variable `RSD_AUTH_PROVIDERS` in the file `.env.example` on how to configure this. + :::tip -You can define multiple providers for authentication in the environment variable by providing a semicolon seprated keys. -RSD_AUTH_PROVIDERS=AZURE;ORCID;HELMHOLTZID;SURFCONEXT;LOCAL +You can define multiple authentication providers in the environment variable by providing keys and values separated by semicolons, e.g. +`RSD_AUTH_PROVIDERS=SURFCONEXT:EVERYONE;AZURE:EVERYONE;ORCID:INVITE_ONLY;LINKEDIN:INVITE_ONLY` ::: ### Enable Microsoft Entra ID (Azure AD) authentication -Please refer to [Microsoft Entra ID documention](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal-setup-oidc-sso) about setting up the application access. In the `.env` file you need to provide following information to enable the authentication service. +Please refer to [Microsoft Entra ID documentation](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/add-application-portal-setup-oidc-sso) about setting up the application access. In the `.env` file you need to provide following information to enable the authentication service. ```bash # Ensure AZURE key is included in the list -RSD_AUTH_PROVIDERS=AZURE +RSD_AUTH_PROVIDERS=AZURE:EVERYONE # AZURE ACTIVE DIRECTORY # consumed by: authentication, frontend/utils/loginHelpers @@ -50,10 +55,6 @@ AZURE_CLIENT_ID= AZURE_REDIRECT=http://localhost/auth/login/azure # consumed by: authentication, frontend/utils/loginHelpers AZURE_WELL_KNOWN_URL= -# consumed by: authentication, frontend/utils/loginHelpers -AZURE_SCOPES=openid+email+profile -# consumed by: authentication, frontend/utils/loginHelpers -AZURE_LOGIN_PROMPT=select_account # consumed by: frontend # the name displayed to users when multiple providers are configured AZURE_DISPLAY_NAME="Microsoft Azure AD" @@ -67,7 +68,7 @@ AZURE_ORGANISATION= ### Enable ORCID authentication and coupling -The RSD offers an integration with ORCID which can be used for login and coupling the user's RSD account with their ORCID. Both integrations can be used independently. +The RSD offers an integration with ORCID which can be used for signing in and coupling the user's RSD account with their ORCID. Both integrations can be used independently. Please refer to the [ORCID OAuth documentation](https://info.orcid.org/documentation/integration-guide/getting-started-with-your-orcid-integration/) in order to set up the ORCID authentication service for the RSD. Specifically, look [here](https://info.orcid.org/documentation/integration-guide/registering-a-public-api-client/) on how to register redirect URLs. @@ -94,16 +95,12 @@ To enable login via ORCID, provide the following information in `.env`: ```bash # Ensure ORCID key is included in the list -RSD_AUTH_PROVIDERS=ORCID +RSD_AUTH_PROVIDERS=ORCID:EVERYONE # consumed by: authentication, frontend/utils/loginHelpers ORCID_REDIRECT=http://www.localhost/auth/login/orcid ``` -:::warning -When using ORCID authentication only, each ORCID user need to be added to ORCID users list. See [ORCID users page](/rsd-instance/administration/#orcid-users) in the administration section. -::: - #### ORCID coupling For ORCID account coupling to be enabled, the following variables need to be set in `.env`: @@ -122,11 +119,11 @@ If ORCID login is disabled and ORCID coupling is enabled, users are added to the ::: - For more [info about public profile page see documentation](/users/user-settings/#public-profile). -- If ORCID login is enabled: after a user links an ORCID to their RSD account they will be able to login using ORCID credentials too. +- If ORCID login is enabled: after a user links an ORCID to their RSD account they will be able to log in using ORCID credentials too. ### Enable SURFconext authentication -Please refer to [SURFconext documention](https://www.surf.nl/en/surfconext-global-access-with-1-set-of-credentials). +Please refer to [SURFconext documentation](https://www.surf.nl/en/surfconext-global-access-with-1-set-of-credentials). :::danger The main RSD instance is already registered with the SURFconext authentication service. We advise you to use our main RSD instance and enable it for your organisation. For more information see [register you organisation](/users/register-organisation/). @@ -136,21 +133,57 @@ The main RSD instance is already registered with the SURFconext authentication s Helmholtz already runs an RSD instance at [https://helmholtz.software/](https://helmholtz.software/) +### Enable LinkedIn authentication + +First, create an app on [https://developer.linkedin.com/](https://developer.linkedin.com/). Follow the steps [here](https://www.linkedin.com/help/linkedin/answer/a1665329) to get your app approved by the company you linked it to. Copy the related environment variables from `.env.example` to your `.env` and fill in the missing values (don't forget to set your custom domain for `LINKEDIN_REDIRECT`). Finally, add `LINKEDIN` to the values in the environment variable `RSD_AUTH_PROVIDERS`. + +## e-Mail service + +The RSD provides a mail service functionality. + +:::info +While the mail service is already implemented, use cases where the service is used are still under development. + +The basic implementation of the mail service (publisher, queue and consumer) allows for custom use cases to be implemented (as further mentioned below). + +::: + +To set up the mail service functionality, the following environment variables are required: + +```shell +MAIL_SMTP_SERVER= # value without https://, e.g. "smtp.server.org" +MAIL_SMTP_PORT= # string value of the port number e.g. "587" +MAIL_SMTP_SECURITY= # value = "SSL" or "STARTTLS" +MAIL_SMTP_LOGIN= # the email address used for login to the SMTP server, e.g. "user@domain.org" +MAIL_SMTP_PASSWORD= # the password used for authentication to the SMTP server +MAIL_FROM_ADDRESS= # the email address that should send the emails from the mail service, e.g. "rsd@domain.org" +MAIL_REPLY_TO= # optional, an email address that should be set for reply-to + +MAIL_QUEUE= # optional, name of the rabbitmq channel used for the mail service, default value: mailq + +PUBLISHER_JWT_SECRET_KEY= +``` + ## Host definitions The `host` section of settings.json defines following settings. **Most of them should be customised**. -- `name`: default value is `rsd`. It is used to load default RSD homepage layout. The other two options are `helmholtz` and `imperial` which will load the homepage layout of these insitutions. -- `email`: the email shown in the footer of the RSD pages. It shoud be used as public contact email. **Change this value to reflect your contact email**. +- `name`: default value is `rsd`. It is used to load default RSD homepage layout. The other two options are `helmholtz` and `imperial` which will load the homepage layout of these institutions. +- `email`: the email shown in the footer of the RSD pages. It should be used as public contact email. **Change this value to reflect your contact email**. - `emailHeaders`: append custom email headers when user tries to contact you. - `logo_url`: the url to footer logo. You can change this value provided you also mount the image - `website`: the url that will be attached to footer logo -- `feedback`: in the header of the RSD there is a feedback button. The feedback button can be enabled. In addition the `url` value is actually the email. In the form there is also link to RSD issues page if the user wants to create an Github issue concerning his/her feedback. +- `feedback`: in the header of the RSD there is a feedback button. + - `enabled`: The feedback button can be enabled. + - `host_label`: Used in the body of message to indicate app name + - `url`: The `url` value is actually the email. + - `issues_page_url`: There is also a link to Github issues page if the user wants to create an GitHub issue concerning his/her feedback. - `login_info_url`: the link to getting access documentation shown in the "Sign in with" modal. It is relevant only if you use more than one authentication provider. If you use only one authentication provider "Sign in with" modal is not used, instead the user is directly redirected to authentication page. - `terms_of_service_url`: the link to your Terms of Service page. Used on the user profile page to let user accept the terms of service - `privacy_statement_url`: the link to your privacy statement page. Used on the user profile page to let user accept the privacy statement. - `software_highlights`: the definitions for the software highlights section on the top of the software overview page. You can specify title and the number of items loaded in the carousel. The default values are shown below. -- `modules`: defines RSD "modules" displayed in the main menu in the page header. Possible values are: software, projects, organisations and communities. +- `orcid_search`: should ORCID api be used when searching for contributors or team members? By default ORCID search api is enabled and the entries from RSD and ORCID are combined in the search result. If you want your users to be able to search only within the existing RSD entries set this value to false. +- `modules`: defines RSD "modules" displayed in the main menu in the page header. Possible values are: software, projects, organisations, communities and persons. ```json ... @@ -162,6 +195,7 @@ The `host` section of settings.json defines following settings. **Most of them s "website": "https://www.esciencecenter.nl", "feedback": { "enabled": true, + "host_label": "RSD", "url": "rsd@esciencecenter.nl", "issues_page_url": "https://github.com/research-software-directory/RSD-as-a-service/issues" }, @@ -173,14 +207,15 @@ The `host` section of settings.json defines following settings. **Most of them s "limit": 5, "description": null }, - "modules":["software","projects","organisations"] + "orcid_search": true, + "modules":["software","projects","organisations","communities","persons"] } ... ``` :::tip host modules -- Note that the communities module is not included in the default settings definitions. +- Note that the communities module is included in the default settings definitions. - Note that disabled module pages still can be accessed when "proper" url is used. - You can enable it by adding "communities" into the modules array and the menu option will appear. ::: @@ -193,7 +228,7 @@ The look and feel of RSD can be customised with desired colors and fonts. In the - The `settings.json` should be mounted into `/app/public/data` folder of frontend service - The `index.css` should be mounted into `/app/public/styles` folder of frontend service -- When customizing RSD styles we advice to mount custom fonts in the styles folder close to index.css +- When customizing RSD styles we advise to mount custom fonts in the styles folder close to index.css - The footer logo should be mounted into `/app/public/images`. Then you can use relative image path `/images/your-logo.svg` in the settings.json - Your [starting point should be our default files](https://github.com/research-software-directory/RSD-as-a-service/tree/main/frontend/public) where you adjust the values you want to be different. @@ -236,12 +271,20 @@ RSD uses default settings.json if alternative is not mounted into `/app/public/d "website": "https://www.esciencecenter.nl", "feedback": { "enabled": true, + "host_label": "RSD", "url": "rsd@esciencecenter.nl", "issues_page_url": "https://github.com/research-software-directory/RSD-as-a-service/issues" }, "login_info_url": "https://research-software-directory.github.io/documentation/getting-access.html", "terms_of_service_url": "/page/terms-of-service/", - "privacy_statement_url": "/page/privacy-statement/" + "privacy_statement_url": "/page/privacy-statement/", + "software_highlights": { + "title": "Software Highlights", + "limit": 5, + "description": null + }, + "orcid_search": true, + "modules": ["software", "projects", "organisations"] }, "links": [ { @@ -251,18 +294,23 @@ RSD uses default settings.json if alternative is not mounted into `/app/public/d }, { "label": "User Documentation", - "url": "/documentation/category/users/", + "url": "/documentation/users/", "target": "_blank" }, { "label": "Technical Documentation", - "url": "/documentation/category/developers/", + "url": "/documentation/rsd-instance/", "target": "_blank" }, { "label": "Netherlands eScienceCenter", "url": "https://www.esciencecenter.nl/", "target": "_blank" + }, + { + "label": "News", + "url": "/news", + "target": "_self" } ], "theme": { diff --git a/frontend/auth/api/__mocks__/useLoginProviders.tsx b/frontend/auth/api/__mocks__/useLoginProviders.tsx index 5fce500..17a0f78 100644 --- a/frontend/auth/api/__mocks__/useLoginProviders.tsx +++ b/frontend/auth/api/__mocks__/useLoginProviders.tsx @@ -1,28 +1,21 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 -import {useEffect, useState} from 'react' - -import {Provider} from 'pages/api/fe/auth' - export default function useLoginProviders() { - const [providers, setProviders] = useState([]) - - useEffect(() => { - let abort = false - - if (abort === false) { - setProviders([{ - name: 'test provider', - redirectUrl: 'https://test-login-redirect.com' - }]) - } - - return () => { abort = true } - }, []) - return providers + return { + providers:[{ + name: 'test provider', + signInUrl: 'https://test-login-redirect.com', + accessType: 'EVERYONE', + openidProvider: 'local' + }], + setProviders: jest.fn() + } } diff --git a/frontend/auth/api/getLoginProviders.tsx b/frontend/auth/api/getLoginProviders.tsx index a1a32e2..ef7b64c 100644 --- a/frontend/auth/api/getLoginProviders.tsx +++ b/frontend/auth/api/getLoginProviders.tsx @@ -1,45 +1,45 @@ // SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 -import {Provider} from 'pages/api/fe/auth' import logger from '~/utils/logger' -// save info after initial call -let loginProviders:Provider[] = [] +/** + * Return a list of valid OpenID providers + * based on provided env. RSD_AUTH_PROVIDERS string, semicolon separated values + * Example! RSD_AUTH_PROVIDERS=surfconext:everyone;helmholtz:invites_only + */ -export async function getLoginProviders(baseUrl?:string) { +export type AccessType = 'INVITE_ONLY' | 'EVERYONE' + +export type Provider = { + openidProvider: 'local' | 'surfconext' | 'helmholtz' | 'orcid' | 'azure' | 'linkedin', + name: string, + signInUrl: string, + accessType: AccessType, + html?: string +} + +export async function getLoginProviders(): Promise { try{ // console.group('getLoginProviders') - // console.log('baseUrl...', baseUrl) // console.log('loginProviders...', loginProviders) // console.groupEnd() - if (loginProviders.length === 0){ - // baseUrl is required for requests from server side - // this is internal url of frontend, by default this - // is http://localhost:3000 - const url = `${baseUrl ?? ''}/api/fe/auth` - // console.log('url...', url) - const resp = await fetch(url) + const url = `${typeof window === 'undefined' ? process.env.RSD_AUTH_URL : '/auth'}/providers` + // console.log('url...', url) + const resp = await fetch(url) - if (resp.status === 200){ - const providers: Provider[] = await resp.json() - // api response is the same once the app is started - // because the info eventually comes from .env file - // to avoid additional api calls we save api response - // into the loginProviders variable and reuse it - loginProviders = [ - ...providers - ] - }else{ - logger(`getLoginProviders: ${resp.status}: ${resp.statusText}`, 'warn') - } + if (resp.status === 200){ + return await resp.json() + }else{ + logger(`getLoginProviders: ${resp.status}: ${resp.statusText}`, 'warn') + return [] } - return loginProviders }catch(e:any){ logger(`getLoginProviders: ${e.message}`, 'error') - return loginProviders + return [] } } diff --git a/frontend/auth/api/useLoginProviders.tsx b/frontend/auth/api/useLoginProviders.tsx index cf330f0..e88e19f 100644 --- a/frontend/auth/api/useLoginProviders.tsx +++ b/frontend/auth/api/useLoginProviders.tsx @@ -2,33 +2,19 @@ // SPDX-FileCopyrightText: 2022 dv4all // SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 -import {useEffect, useState} from 'react' +import {useContext} from 'react' -import {Provider} from 'pages/api/fe/auth' -import {getLoginProviders} from './getLoginProviders' +import {LoginProvidersContext} from '~/auth/loginProvidersContext' -export default function useLoginProviders() { - const [providers, setProviders] = useState([]) +export default function useLoginProviders(){ + const {providers,setProviders} = useContext(LoginProvidersContext) - // console.group('useLoginProviders') - // console.log('providers...', providers) - // console.groupEnd() - - useEffect(() => { - let abort = false - - getLoginProviders() - .then(providers=>{ - if (abort) return - setProviders(providers) - }) - .catch(()=>setProviders([])) - - return () => { abort = true } - }, []) - - return providers + return { + providers, + setProviders + } } diff --git a/frontend/auth/loginProvidersContext.tsx b/frontend/auth/loginProvidersContext.tsx new file mode 100644 index 0000000..322dc9c --- /dev/null +++ b/frontend/auth/loginProvidersContext.tsx @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {createContext, useState} from 'react' +import {Provider} from './api/getLoginProviders' + +export type LoginProvidersContextProps={ + providers: Provider[] + setProviders: (provider:Provider[])=>void +} + +export const LoginProvidersContext = createContext({ + providers: [], + setProviders:()=>{} +}) + +export function LoginProvidersProvider(props:any){ + const [providers,setProviders] = useState(props?.providers ?? []) + + // console.group('LoginProvidersProvider') + // console.log('props?.providers...', props?.providers) + // console.log('providers...', providers) + // console.groupEnd() + + return +} diff --git a/frontend/auth/providers/azure.ts b/frontend/auth/providers/azure.ts deleted file mode 100644 index 05b62b4..0000000 --- a/frontend/auth/providers/azure.ts +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) -// SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Azure Active Directory OpenID info - * It provides frontend with redirect uri for the login button - */ - -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import logger from '~/utils/logger' -import {RedirectToProps, getRedirectUrl} from '~/auth/api/authHelpers' -import {getAuthEndpoint} from '~/auth/api/authEndpoint' - -async function azureRedirectProps() { - // extract wellknown url from env - const wellknownUrl = process.env.AZURE_WELL_KNOWN_URL ?? null - if (wellknownUrl) { - // get (cached) authorisation endpoint from wellknown url - const authorization_endpoint = await getAuthEndpoint(wellknownUrl, 'azure') - if (authorization_endpoint) { - // construct all props needed for redirectUrl - const props: RedirectToProps = { - authorization_endpoint, - redirect_uri: process.env.AZURE_REDIRECT ?? 'https://research-software.nl/auth/login/azure', - client_id: process.env.AZURE_CLIENT_ID ?? 'www.research-software.nl', - scope: process.env.AZURE_SCOPES ?? 'openid', - response_mode: process.env.AZURE_RESPONSE_MODE ?? 'query', - prompt: process.env.AZURE_LOGIN_PROMPT - } - return props - } else { - const message = 'authorization_endpoint is missing' - logger(`api/fe/auth/azure: ${message}`, 'error') - throw new Error(message) - } - } else { - const message = 'AZURE_WELL_KNOWN_URL is missing' - logger(`api/fe/auth/azure: ${message}`, 'error') - throw new Error(message) - } -} - -export async function azureInfo() { - // extract all props from env and wellknown endpoint - const redirectProps = await azureRedirectProps() - if (redirectProps) { - // create return url and the name to use in login button - const redirectUrl = getRedirectUrl(redirectProps) - // provide redirectUrl and name/label - return { - name: process.env.AZURE_DISPLAY_NAME ?? 'Azure Active Directory', - redirectUrl, - html: process.env.AZURE_DESCRIPTION_HTML ?? 'Login with your institutional credentials.' - } - } - return null -} diff --git a/frontend/auth/providers/helmholtzid.ts b/frontend/auth/providers/helmholtzid.ts deleted file mode 100644 index c5a3ac7..0000000 --- a/frontend/auth/providers/helmholtzid.ts +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-FileCopyrightText: 2022 - 2024 Christian Meeßen (GFZ) -// SPDX-FileCopyrightText: 2022 - 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) -// SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center -// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Helmholz ID OpenID info - * It provides frontend with redirect uri for the login button - */ - -import logger from '~/utils/logger' -import {RedirectToProps, getRedirectUrl} from '~/auth/api/authHelpers' -import {getAuthEndpoint} from '~/auth/api/authEndpoint' - -const claims = { - id_token: { - schac_home_organization: null, - name: null, - email: null - } -} - -async function helmholtzRedirectProps() { - // extract wellknow url from env - const wellknownUrl = process.env.HELMHOLTZID_WELL_KNOWN_URL ?? null - if (wellknownUrl) { - // get (cached) authorisation endpoint from wellknown url - const authorization_endpoint = await getAuthEndpoint(wellknownUrl, 'helmholtz') - if (authorization_endpoint) { - // construct all props needed for redirectUrl - // use default values if env not provided - const props: RedirectToProps = { - authorization_endpoint, - client_id: process.env.HELMHOLTZID_CLIENT_ID ?? 'rsd-dev', - redirect_uri: process.env.HELMHOLTZID_REDIRECT ?? 'http://localhost/auth/login/helmholtzid', - scope: process.env.HELMHOLTZID_SCOPES ?? 'openid+profile+email+eduperson_principal_name', - response_mode: process.env.HELMHOLTZID_RESPONSE_MODE ?? 'query', - claims - } - return props - } else { - const message = 'authorization_endpoint is missing' - logger(`api/fe/auth/helmholtzid: ${message}`, 'error') - throw new Error(message) - } - } else { - const message = 'HELMHOLTZID_WELL_KNOWN_URL is missing' - logger(`api/fe/auth/helmholtzid: ${message}`, 'error') - throw new Error(message) - } -} - -export async function helmholtzInfo() { - // extract all props from env and wellknown endpoint - const redirectProps = await helmholtzRedirectProps() - if (redirectProps) { - // create return url and the name to use in login button - const redirectUrl = getRedirectUrl(redirectProps) - // provide redirectUrl and name/label - return { - name: 'Helmholtz ID', - redirectUrl, - html: ` - Sign in with Helmholtz ID is enabled for all members of the Helmholtz Research Foundation. - ` - } - } - return null -} diff --git a/frontend/auth/providers/linkedin.ts b/frontend/auth/providers/linkedin.ts deleted file mode 100644 index c901fb6..0000000 --- a/frontend/auth/providers/linkedin.ts +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2025 Netherlands eScience Center -// -// SPDX-License-Identifier: Apache-2.0 - -import logger from '~/utils/logger' -import {getAuthEndpoint} from '~/auth/api/authEndpoint' -import {getRedirectUrl, RedirectToProps} from '~/auth/api/authHelpers' - -async function linkedinRedirectProps() { - try { - // extract well known url from env - const wellknownUrl = process.env.LINKEDIN_WELL_KNOWN_URL - if (!wellknownUrl) { - const message = 'LINKEDIN_WELL_KNOWN_URL is missing' - logger(`linkedinRedirectProps: ${message}`, 'error') - return null - } - - const authorization_endpoint = await getAuthEndpoint(wellknownUrl, 'linkedin') - if (!authorization_endpoint) { - const message = 'authorization_endpoint is missing' - logger(`linkedinRedirectProps: ${message}`, 'error') - return null - } - - const redirect_uri = process.env.LINKEDIN_REDIRECT - if (!redirect_uri) { - const message = 'LINKEDIN_REDIRECT is missing' - logger(`linkedinRedirectProps: ${message}`, 'error') - return null - } - - const client_id = process.env.LINKEDIN_CLIENT_ID - if (!client_id) { - const message = 'LINKEDIN_CLIENT_ID is missing' - logger(`linkedinRedirectProps: ${message}`, 'error') - return null - } - - const props: RedirectToProps = { - authorization_endpoint, - redirect_uri, - client_id, - scope: process.env.LINKEDIN_SCOPES ?? 'openid%20profile%20email', - response_mode: process.env.LINKEDIN_RESPONSE_MODE ?? 'code' - } - return props - } catch (e: any) { - logger(`orcidRedirectProps: ${e.message}`, 'error') - return null - } -} - -export async function linkedinInfo() { - const redirectProps = await linkedinRedirectProps() - if (!redirectProps) { - return null - } - - const redirectUrl = getRedirectUrl(redirectProps) - - return { - name: 'LinkedIn', - redirectUrl, - html: 'Sign in with your LinkedIn account.' - } -} diff --git a/frontend/auth/providers/local.ts b/frontend/auth/providers/local.ts deleted file mode 100644 index e19b168..0000000 --- a/frontend/auth/providers/local.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center -// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) -// -// SPDX-License-Identifier: Apache-2.0 - -export function localInfo() { - return { - name: 'Local account', - redirectUrl: '/login/local', - html: '

Sign in with local account is for testing purposes only. This option should not be enabled in the production version.

' - - } -} - diff --git a/frontend/auth/providers/orcid.ts b/frontend/auth/providers/orcid.ts deleted file mode 100644 index 509ef52..0000000 --- a/frontend/auth/providers/orcid.ts +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) -// SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * ORCID OpenID endpoint - * It provides frontend with redirect uri for the login button - */ - -import logger from '~/utils/logger' -import {RedirectToProps, getRedirectUrl} from '~/auth/api/authHelpers' -import {getAuthEndpoint} from '~/auth/api/authEndpoint' - -async function orcidRedirectProps() { - try { - // extract well known url from env - const wellknownUrl = process.env.ORCID_WELL_KNOWN_URL ?? null - if (wellknownUrl) { - // get (cached) authorisation endpoint from wellknown url - const authorization_endpoint = await getAuthEndpoint(wellknownUrl, 'orcid') ?? null - if (authorization_endpoint) { - // construct all props needed for redirectUrl - const props: RedirectToProps = { - authorization_endpoint, - redirect_uri: process.env.ORCID_REDIRECT ?? 'https://research-software.nl/auth/login/orcid', - redirect_couple_uri: process.env.ORCID_REDIRECT_COUPLE ?? null, - client_id: process.env.ORCID_CLIENT_ID ?? 'www.research-software.nl', - scope: process.env.ORCID_SCOPES ?? 'openid', - response_mode: process.env.ORCID_RESPONSE_MODE ?? 'query' - } - return props - } else { - const message = 'authorization_endpoint is missing' - logger(`orcidRedirectProps: ${message}`, 'error') - return null - } - } else { - const message = 'ORCID_WELL_KNOWN_URL is missing' - logger(`orcidRedirectProps: ${message}`, 'error') - return null - } - } catch (e: any) { - logger(`orcidRedirectProps: ${e.message}`, 'error') - return null - } -} - -export async function orcidInfo() { - // extract all props from env and wellknown endpoint - const redirectProps = await orcidRedirectProps() - if (redirectProps) { - // create return url and the name to use in login button - const redirectUrl = getRedirectUrl(redirectProps) - // provide redirectUrl and name/label - return { - name: 'ORCID', - redirectUrl, - html: 'Sign in with your ORCID account.' - } - } - return null -} diff --git a/frontend/auth/providers/surfconext.ts b/frontend/auth/providers/surfconext.ts deleted file mode 100644 index 60d8a45..0000000 --- a/frontend/auth/providers/surfconext.ts +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) -// SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * SURFconext OpenID info - * It provides frontend with redirect uri for the login button - */ -import logger from '~/utils/logger' -import {RedirectToProps, getRedirectUrl} from '~/auth/api/authHelpers' -import {getAuthEndpoint} from '~/auth/api/authEndpoint' - -const claims = { - id_token: { - schac_home_organization: null, - name: null, - email: null - } -} - -async function surfconextRedirectProps() { - // extract wellknown url from env - const wellknownUrl = process.env.SURFCONEXT_WELL_KNOWN_URL ?? null - if (wellknownUrl) { - // get (cached) authorisation endpoint from wellknown url - const authorization_endpoint = await getAuthEndpoint(wellknownUrl, 'surfconext') ?? null - if (authorization_endpoint) { - // construct all props needed for redirectUrl - const props: RedirectToProps = { - authorization_endpoint, - redirect_uri: process.env.SURFCONEXT_REDIRECT ?? 'https://research-software.nl/auth/login/surfconext', - client_id: process.env.SURFCONEXT_CLIENT_ID ?? 'www.research-software.nl', - scope: process.env.SURFCONEXT_SCOPES ?? 'openid', - response_mode: process.env.SURFCONEXT_RESPONSE_MODE ?? 'form_post', - claims - } - return props - } else { - const message = 'authorization_endpoint is missing' - logger(`api/fe/auth/surfconext: ${message}`, 'error') - throw new Error(message) - } - } else { - const message = 'SURFCONEXT_WELL_KNOWN_URL is missing' - logger(`api/fe/auth/surfconext: ${message}`, 'error') - throw new Error(message) - } -} - -export async function surfconextInfo() { - // extract all props from env and wellknown endpoint - const redirectProps = await surfconextRedirectProps() - if (redirectProps) { - // create return url and the name to use in login button - const redirectUrl = getRedirectUrl(redirectProps) - // provide redirectUrl and name/label - return { - name: 'SURFconext', - redirectUrl, - html: `Sign in with SURFconext is for Dutch Institutions who enabled the - RSD service in the SURFconext IdP dashboard.` - } - } - return null -} diff --git a/frontend/components/AppHeader/AddMenu.test.tsx b/frontend/components/AppHeader/AddMenu.test.tsx index 824244b..865107f 100644 --- a/frontend/components/AppHeader/AddMenu.test.tsx +++ b/frontend/components/AppHeader/AddMenu.test.tsx @@ -4,25 +4,68 @@ // SPDX-License-Identifier: Apache-2.0 import {render,screen, fireEvent} from '@testing-library/react' -import {WrappedComponentWithProps} from '../../utils/jest/WrappedComponents' -import AddMenu from '../AppHeader/AddMenu' +import {WithAppContext, mockSession} from '~/utils/jest/WithAppContext' +import {defaultRsdSettings, RsdSettingsState} from '~/config/rsdSettingsReducer' +import AddMenu from './AddMenu' + +const mockSettings:RsdSettingsState = { + ...defaultRsdSettings +} it('should render AddMenu',()=>{ - render(WrappedComponentWithProps(AddMenu)) + render( + + + + ) const userMenu = screen.queryByTestId('add-menu-button') expect(userMenu).toBeInTheDocument() // screen.debug() }) -it('should have AddMenu options',async()=>{ - render(WrappedComponentWithProps(AddMenu)) +it('all menu options for rsd-admin',async()=>{ + mockSettings.host.modules = ['software','projects','news'] + // admin should have more items + if (mockSession.user) mockSession.user.role='rsd_admin' + + render( + + + + ) + const menuButton = screen.queryByTestId('add-menu-button') + // click on the button to display menu options + fireEvent.click(menuButton as HTMLElement) + // select all menu options + const menuOptions = screen.queryAllByTestId('add-menu-option') + // assert only 1 item + expect(menuOptions.length).toEqual(mockSettings.host.modules.length) + // screen.debug() +}) + +it('no news option for rsd-user',async()=>{ + mockSettings.host.modules = ['software','projects','news'] + // admin should have more items + if (mockSession.user) mockSession.user.role='rsd_user' + + render( + + + + ) const menuButton = screen.queryByTestId('add-menu-button') // click on the button to display menu options fireEvent.click(menuButton as HTMLElement) // select all menu options const menuOptions = screen.queryAllByTestId('add-menu-option') // assert only 1 item - expect(menuOptions.length).toEqual(1) + expect(menuOptions.length).toEqual(mockSettings.host.modules.length-1) // screen.debug() }) diff --git a/frontend/components/AppHeader/AddMenu.tsx b/frontend/components/AppHeader/AddMenu.tsx index 34e11e7..6c9cc36 100644 --- a/frontend/components/AppHeader/AddMenu.tsx +++ b/frontend/components/AppHeader/AddMenu.tsx @@ -1,7 +1,7 @@ // 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-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -10,25 +10,28 @@ import {useRouter} from 'next/router' import Menu from '@mui/material/Menu' import MenuItem from '@mui/material/MenuItem' import AddIcon from '@mui/icons-material/Add' -// import TerminalIcon from '@mui/icons-material/Terminal' -import ListAltIcon from '@mui/icons-material/ListAlt' import IconButton from '@mui/material/IconButton' import ListItemIcon from '@mui/material/ListItemIcon' -import NewspaperIcon from '@mui/icons-material/Newspaper' -import {useSession} from '~/auth' import CaretIcon from '~/components/icons/caret.svg' import useDisableScrollLock from '~/utils/useDisableScrollLock' +import useAddItemMenu from './useAddItemMenu' export default function AddMenu() { - const {user} = useSession() const router = useRouter() const disable = useDisableScrollLock() const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) + const addItemMenu = useAddItemMenu() function handleClick(event: React.MouseEvent) { - setAnchorEl(event.currentTarget) + // only one option so click direct to action + if (addItemMenu.length===1 && addItemMenu[0]?.path){ + handleClose(addItemMenu[0]?.path) + }else{ + // show menu items + setAnchorEl(event.currentTarget) + } } function handleClose(path?: string) { @@ -39,6 +42,14 @@ export default function AddMenu() { setAnchorEl(null) } + // console.group('AddMenu') + // console.log('open...',open) + // console.log('addItemMenu...',addItemMenu) + // console.groupEnd() + + // if no items to shown hide it + if (addItemMenu.length === 0) return null + return ( <> - handleClose()} - MenuListProps={{'aria-labelledby': 'menu-button'}} transformOrigin={{horizontal: 'right', vertical: 'top'}} anchorOrigin={{horizontal: 'right', vertical: 'bottom'}} // disable adding styles to body (overflow:hidden & padding-right) disableScrollLock={disable} + slotProps={{ + list: {'aria-labelledby': 'menu-button'} + }} > - {/* disable software option, 2024-07-02 */} - {/* handleClose('/add/software')}> - - - - New Software - */} - - handleClose('/add/project')}> - - - - New Project - { - // ADMIN ONLY options - user?.role==='rsd_admin' ? - handleClose('/add/news')}> - - - - Add News - - : null + addItemMenu.map(item=>{ + return ( + handleClose(item.path)}> + + {item.icon} + + {item.label} + + ) + }) } diff --git a/frontend/components/AppHeader/AppHeader.test.tsx b/frontend/components/AppHeader/AppHeader.test.tsx index 4567b59..db5086f 100644 --- a/frontend/components/AppHeader/AppHeader.test.tsx +++ b/frontend/components/AppHeader/AppHeader.test.tsx @@ -1,7 +1,7 @@ // 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-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -15,12 +15,7 @@ import AppHeader from './index' // mocks login providers list const redirectUrl = 'https://test-login-redirect.com' -jest.mock('~/auth/api/useLoginProviders', () => { - return ()=>[{ - name: 'test provider', - redirectUrl - }] -}) +jest.mock('~/auth/api/useLoginProviders') beforeEach(() => { jest.clearAllMocks() diff --git a/frontend/components/AppHeader/__mocks__/useAddItemMenu.tsx b/frontend/components/AppHeader/__mocks__/useAddItemMenu.tsx new file mode 100644 index 0000000..9edb49c --- /dev/null +++ b/frontend/components/AppHeader/__mocks__/useAddItemMenu.tsx @@ -0,0 +1,59 @@ +import {JSX} from 'react' + + +import {useSession} from '~/auth' +import TerminalIcon from '@mui/icons-material/Terminal' +import ListAltIcon from '@mui/icons-material/ListAlt' +import NewspaperIcon from '@mui/icons-material/Newspaper' +import useRsdSettings from '~/config/useRsdSettings' + +type AddItemMenuItem={ + type: 'link' | 'divider' + label: string + icon: JSX.Element, + path?: string, + // used to customize menu items per user/profile + active:(props:any)=>boolean +} + +export const addItemMenu:AddItemMenuItem[]=[{ + type: 'link', + label: 'New Software', + icon: , + path: '/add/software', + active: ({modules})=>modules.includes('software') +},{ + type: 'link', + label: 'New Project', + icon: , + path: '/add/project', + active: ({modules})=>modules.includes('projects') +},{ + type: 'link', + label: 'Add News', + icon: , + path: '/add/news', + active: ({role,modules})=> { + return role ==='rsd_admin' && modules.includes('news') + } +}] + + +function useAddItemMenu(){ + const {user} = useSession() + const {host} = useRsdSettings() + + const menuItems = addItemMenu.filter(item=>{ + return item.active({role: user?.role, modules: host.modules}) ?? false + }) + + // console.group('useAddItemMenu') + // console.log('user...', user) + // console.log('host...', host) + // console.log('menuItems...', menuItems) + // console.groupEnd() + + return menuItems +} + +export default useAddItemMenu diff --git a/frontend/components/AppHeader/useAddItemMenu.tsx b/frontend/components/AppHeader/useAddItemMenu.tsx new file mode 100644 index 0000000..2f6a998 --- /dev/null +++ b/frontend/components/AppHeader/useAddItemMenu.tsx @@ -0,0 +1,59 @@ +import {JSX} from 'react' + + +import {useSession} from '~/auth' +import TerminalIcon from '@mui/icons-material/Terminal' +import ListAltIcon from '@mui/icons-material/ListAlt' +import NewspaperIcon from '@mui/icons-material/Newspaper' +import useRsdSettings from '~/config/useRsdSettings' + +type AddItemMenuItem={ + type: 'link' | 'divider' + label: string + icon: JSX.Element, + path?: string, + // used to customize menu items per user/profile + active:(props:any)=>boolean +} + +const addItemMenu:AddItemMenuItem[]=[{ + type: 'link', + label: 'New Software', + icon: , + path: '/add/software', + active: ({modules})=>modules.includes('software') +},{ + type: 'link', + label: 'New Project', + icon: , + path: '/add/project', + active: ({modules})=>modules.includes('projects') +},{ + type: 'link', + label: 'Add News', + icon: , + path: '/add/news', + active: ({role,modules})=> { + return role ==='rsd_admin' && modules.includes('news') + } +}] + + +function useAddItemMenu(){ + const {user} = useSession() + const {host} = useRsdSettings() + + const menuItems = addItemMenu.filter(item=>{ + return item.active({role: user?.role, modules: host.modules}) ?? false + }) + + // console.group('useAddItemMenu') + // console.log('user...', user) + // console.log('host...', host) + // console.log('menuItems...', menuItems) + // console.groupEnd() + + return menuItems +} + +export default useAddItemMenu diff --git a/frontend/components/GlobalSearchAutocomplete/SearchItemIcon.tsx b/frontend/components/GlobalSearchAutocomplete/SearchItemIcon.tsx index 5562bde..829ddb5 100644 --- a/frontend/components/GlobalSearchAutocomplete/SearchItemIcon.tsx +++ b/frontend/components/GlobalSearchAutocomplete/SearchItemIcon.tsx @@ -7,10 +7,11 @@ import TerminalIcon from '@mui/icons-material/Terminal' import ListAltIcon from '@mui/icons-material/ListAlt' import BusinessIcon from '@mui/icons-material/Business' import Diversity3Icon from '@mui/icons-material/Diversity3' +import AccountCircleIcon from '@mui/icons-material/AccountCircle' -import {GlobalSearchResultsSource} from './apiGlobalSearch' +import {RsdModule} from '~/config/rsdSettingsReducer' -export default function SearchItemIcon({source}:Readonly<{source:GlobalSearchResultsSource}>) { +export default function SearchItemIcon({source}:Readonly<{source:RsdModule}>) { switch (source){ case 'software': return @@ -20,6 +21,8 @@ export default function SearchItemIcon({source}:Readonly<{source:GlobalSearchRes return case 'communities': return + case 'persons': + return default: return null } diff --git a/frontend/components/GlobalSearchAutocomplete/apiGlobalSearch.ts b/frontend/components/GlobalSearchAutocomplete/apiGlobalSearch.ts index b2b4d65..4ee74c2 100644 --- a/frontend/components/GlobalSearchAutocomplete/apiGlobalSearch.ts +++ b/frontend/components/GlobalSearchAutocomplete/apiGlobalSearch.ts @@ -8,15 +8,16 @@ import logger from '~/utils/logger' import {createJsonHeaders, getBaseUrl} from '~/utils/fetchHelpers' +import {RsdModule} from '~/config/rsdSettingsReducer' -export type GlobalSearchResultsSource = 'software' | 'projects' | 'organisations' | 'communities' +// export type GlobalSearchResultsSource = 'software' | 'projects' | 'organisations' | 'communities' | 'persons' export type GlobalSearchResults = { rsd_host: string | null domain: string | null slug: string, name: string, - source: GlobalSearchResultsSource, + source: RsdModule, is_published?: boolean, search_text?: string } diff --git a/frontend/components/GlobalSearchAutocomplete/index.tsx b/frontend/components/GlobalSearchAutocomplete/index.tsx index f8516b0..b1feb10 100644 --- a/frontend/components/GlobalSearchAutocomplete/index.tsx +++ b/frontend/components/GlobalSearchAutocomplete/index.tsx @@ -41,6 +41,7 @@ export default function GlobalSearchAutocomplete(props: Props) { const [searchResults, setSearchResults] = useState([]) const [searchCombo, setSearchCombo] = useState('Ctrl K') const {hasRemotes} = useHasRemotes() + const lastValue = useDebounce(inputValue, 150) const inputRef = useRef(null) const {showErrorMessage} = useSnackbar() @@ -83,6 +84,10 @@ export default function GlobalSearchAutocomplete(props: Props) { if (host.modules?.includes('communities')) { defaultValues.push({name: 'Go to Communities page', slug: '', source: 'communities', domain: null, rsd_host: null}) } + if (host.modules?.includes('persons')) { + defaultValues.push({name: 'Go to Persons page', slug: '', source: 'persons', domain: null, rsd_host: null}) + } + async function fetchData(search: string) { // Fetch api diff --git a/frontend/components/admin/logs/apiLogs.ts b/frontend/components/admin/logs/apiLogs.ts index 01964ec..a2038c1 100644 --- a/frontend/components/admin/logs/apiLogs.ts +++ b/frontend/components/admin/logs/apiLogs.ts @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 dv4all // SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) @@ -17,7 +17,7 @@ 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}*,slug.ilike.*${searchFor}*)` + query+=`&or=(service_name.ilike."*${searchFor}*",table_name.ilike."*${searchFor}*",message.ilike."*${searchFor}*",stack_trace.ilike."*${searchFor}*",slug.ilike."*${searchFor}*")` } if (orderBy) { query+=`&order=${orderBy.column}.${orderBy.direction}` diff --git a/frontend/components/admin/mentions/MentionsOverview.tsx b/frontend/components/admin/mentions/MentionsOverview.tsx index f0e667a..79a7b23 100644 --- a/frontend/components/admin/mentions/MentionsOverview.tsx +++ b/frontend/components/admin/mentions/MentionsOverview.tsx @@ -1,6 +1,6 @@ -// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -55,7 +55,7 @@ export default function MentionsOverview() { case 'openalex': return `openalex_id=eq.${termEscaped}` case 'title': - return `or=(title.ilike.*${termEscaped}*,authors.ilike.*${termEscaped}*,journal.ilike.*${termEscaped}*,url.ilike.*${termEscaped}*,note.ilike.*${termEscaped}*,openalex_id.ilike.*${termEscaped}*)` + return `or=(title.ilike."*${termEscaped}*",authors.ilike."*${termEscaped}*",journal.ilike."*${termEscaped}*",url.ilike."*${termEscaped}*",note.ilike."*${termEscaped}*",openalex_id.ilike."*${termEscaped}*")` } } diff --git a/frontend/components/admin/organisations/apiOrganisation.tsx b/frontend/components/admin/organisations/apiOrganisation.tsx index 7536e49..1f28d08 100644 --- a/frontend/components/admin/organisations/apiOrganisation.tsx +++ b/frontend/components/admin/organisations/apiOrganisation.tsx @@ -1,7 +1,7 @@ // 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-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -38,7 +38,7 @@ async function getOrganisations({page, rows, token, searchFor, orderBy}: getOrga const selectList = 'id,parent,name,website,is_tenant,rsd_path,logo_id,ror_id,software_cnt,project_cnt' let query = paginationUrlParams({rows, page}) if (searchFor) { - query+=`&or=(name.ilike.*${searchFor}*,website.ilike.*${searchFor}*,ror_id.ilike.*${searchFor}*)` + query+=`&or=(name.ilike."*${searchFor}*",website.ilike."*${searchFor}*",ror_id.ilike."*${searchFor}*")` } if (orderBy) { query+=`&order=${orderBy}` diff --git a/frontend/components/admin/remote-rsd/apiRemoteRsd.ts b/frontend/components/admin/remote-rsd/apiRemoteRsd.ts index a69e922..8b6dee5 100644 --- a/frontend/components/admin/remote-rsd/apiRemoteRsd.ts +++ b/frontend/components/admin/remote-rsd/apiRemoteRsd.ts @@ -37,7 +37,7 @@ export async function getRemoteRsd({page,rows,token,searchFor,orderBy}:GetRemote let query = paginationUrlParams({rows, page}) if (searchFor) { // search in name and short description - query+=`&or=(label.ilike.*${searchFor}*,domain.ilike.*${searchFor}*)` + query+=`&or=(label.ilike."*${searchFor}*",domain.ilike."*${searchFor}*")` } if (orderBy) { query+=`&order=${orderBy}` diff --git a/frontend/components/admin/rsd-contributors/apiRsdContributors.ts b/frontend/components/admin/rsd-contributors/apiRsdContributors.ts index 544d11e..3351fdb 100644 --- a/frontend/components/admin/rsd-contributors/apiRsdContributors.ts +++ b/frontend/components/admin/rsd-contributors/apiRsdContributors.ts @@ -20,7 +20,7 @@ export async function getContributors({page, rows, token, searchFor, orderBy}: A try { let query = paginationUrlParams({rows, page}) if (searchFor) { - query+=`&or=(given_names.ilike.*${searchFor}*,family_names.ilike.*${searchFor}*,email_address.ilike.*${searchFor}*,orcid.ilike.*${searchFor}*)` + query+=`&or=(given_names.ilike."*${searchFor}*",family_names.ilike."*${searchFor}*",email_address.ilike."*${searchFor}*",orcid.ilike."*${searchFor}*")` } if (orderBy) { query+=`&order=${orderBy.column}.${orderBy.direction}` diff --git a/frontend/components/admin/rsd-info/apiRsdInfo.ts b/frontend/components/admin/rsd-info/apiRsdInfo.ts index 26212b2..8f27635 100644 --- a/frontend/components/admin/rsd-info/apiRsdInfo.ts +++ b/frontend/components/admin/rsd-info/apiRsdInfo.ts @@ -31,7 +31,7 @@ export async function getRsdInfo({page, rows, token, searchFor, orderBy}: ApiPar try { let query = paginationUrlParams({rows, page}) if (searchFor) { - query+=`&or=(key.ilike.*${searchFor}*,value.ilike.*${searchFor}*)` + query+=`&or=(key.ilike."*${searchFor}*",value.ilike."*${searchFor}*")` } if (orderBy) { query+=`&order=${orderBy.column}.${orderBy.direction}` diff --git a/frontend/components/admin/rsd-users/apiRsdUsers.ts b/frontend/components/admin/rsd-users/apiRsdUsers.ts index 8e3ca56..791de7c 100644 --- a/frontend/components/admin/rsd-users/apiRsdUsers.ts +++ b/frontend/components/admin/rsd-users/apiRsdUsers.ts @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 dv4all // SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) @@ -37,7 +37,7 @@ export async function getRsdAccounts({page,rows,token,searchFor,adminsOnly,inact query += `&id=eq.${searchFor}` } else { // else we search by name, email or organisation - query+=`&login_for_account_text_filter.or=(name.ilike.*${searchFor}*,email.ilike.*${searchFor}*,home_organisation.ilike.*${searchFor}*)` + query+=`&login_for_account_text_filter.or=(name.ilike."*${searchFor}*",email.ilike."*${searchFor}*",home_organisation.ilike."*${searchFor}*")` } } // complete url diff --git a/frontend/components/admin/software-highlights/apiSoftwareHighlights.ts b/frontend/components/admin/software-highlights/apiSoftwareHighlights.ts index e6fd0a2..503b4b5 100644 --- a/frontend/components/admin/software-highlights/apiSoftwareHighlights.ts +++ b/frontend/components/admin/software-highlights/apiSoftwareHighlights.ts @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) // SPDX-FileCopyrightText: 2023 dv4all @@ -32,7 +32,7 @@ export async function getSoftwareHighlights({limit, offset, token, searchFor, or // let query = paginationUrlParams({ rows, page }) let query = '' if (searchFor) { - query+=`&or=(brand_name.ilike.*${searchFor}*,short_statement.ilike.*${searchFor}*)` + query+=`&or=(brand_name.ilike."*${searchFor}*",short_statement.ilike."*${searchFor}*")` } if (orderBy) { query+=`&order=${orderBy}` diff --git a/frontend/components/category/CategoriesDialog.tsx b/frontend/components/category/CategoriesDialog.tsx index 0f7749e..993db8f 100644 --- a/frontend/components/category/CategoriesDialog.tsx +++ b/frontend/components/category/CategoriesDialog.tsx @@ -82,7 +82,7 @@ export default function CategoriesDialog({ placeholder='Find category' onSearch={setSearchFor} defaultValue={searchFor ?? ''} - InputProps={{ + inputProps={{ startAdornment: }} /> diff --git a/frontend/components/communities/apiCommunities.ts b/frontend/components/communities/apiCommunities.ts index 865c344..917743b 100644 --- a/frontend/components/communities/apiCommunities.ts +++ b/frontend/components/communities/apiCommunities.ts @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -44,7 +44,7 @@ export async function getCommunityList({page,rows,token,searchFor,orderBy}:GetCo let query = paginationUrlParams({rows, page}) if (searchFor) { // search in name and short description - query+=`&or=(name.ilike.*${searchFor}*,short_description.ilike.*${searchFor}*)` + query+=`&or=(name.ilike."*${searchFor}*",short_description.ilike."*${searchFor}*")` } if (orderBy) { query+=`&order=${orderBy}` diff --git a/frontend/components/form/AsyncAutocompleteSC.tsx b/frontend/components/form/AsyncAutocompleteSC.tsx index 09c7dab..cc4124d 100644 --- a/frontend/components/form/AsyncAutocompleteSC.tsx +++ b/frontend/components/form/AsyncAutocompleteSC.tsx @@ -1,11 +1,11 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 - 2024 dv4all +// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) // SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (dv4all) (dv4all) -// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 @@ -299,22 +299,24 @@ export default function AsyncAutocompleteSC({status, options, config, error={config?.error ? config.error : false} onKeyDown={(e) => { // console.log('onKeyDown.TextField.AsyncAutocompleteSC') - // dissable enter key when autocomplete menu options closed + // disable enter key when autocomplete menu options closed // it seem to crash component in some configuration (probably when freeSolo===false) if (e.key === 'Enter' && open === false) { // stop propagation e.stopPropagation() } }} - InputProps={{ - ...params.InputProps, - 'aria-label': config.label ?? 'Search', - endAdornment: ( - <> - {loading ? : null} - {params.InputProps.endAdornment} - - ), + slotProps={{ + input: { + ...params.InputProps, + 'aria-label': config.label ?? 'Search', + endAdornment: ( + <> + {loading ? : null} + {params.InputProps.endAdornment} + + ), + } }} /> )} diff --git a/frontend/components/form/ControlledSlugTextField.tsx b/frontend/components/form/ControlledSlugTextField.tsx index f680756..6049ce0 100644 --- a/frontend/components/form/ControlledSlugTextField.tsx +++ b/frontend/components/form/ControlledSlugTextField.tsx @@ -1,5 +1,7 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -64,19 +66,21 @@ export default function ControlledSlugTextField({ label={options.label} variant='outlined' value={value} - InputProps={{ - startAdornment: ( - - {options.baseUrl} - - ), - endAdornment: ( - loading ? -
- -
- : null - ) + slotProps={{ + input:{ + startAdornment: ( + + {options.baseUrl} + + ), + endAdornment: ( + loading ? +
+ +
+ : null + ) + } }} error={error ? true: false} helperText={error?.message ?? options.helperTextMessage} diff --git a/frontend/components/form/ControlledTextField.tsx b/frontend/components/form/ControlledTextField.tsx index 36ca6b7..38c6f6f 100644 --- a/frontend/components/form/ControlledTextField.tsx +++ b/frontend/components/form/ControlledTextField.tsx @@ -80,11 +80,17 @@ export default function ControlledTextField({options, control, rules}:Control // controlled mui input requires "" instead of null // but the value in controller of react-hook-form is null (can be null) value={value ?? ''} - FormHelperTextProps={{ - sx:{ - display: 'flex', - justifyContent:'space-between' - } + slotProps={{ + input:{ + startAdornment: options?.startAdornment ? {options?.startAdornment} : undefined, + endAdornment: options?.endAdornment ? {options?.endAdornment} : undefined + }, + formHelperText:{ + sx:{ + display: 'flex', + justifyContent:'space-between' + } + }, }} helperText={ ({options, control, rules}:Control onChange(target.value) } }} - InputProps={{ - startAdornment: options?.startAdornment ? {options?.startAdornment} : undefined, - endAdornment: options?.endAdornment ? {options?.endAdornment} : undefined - }} {...options.muiProps} /> ) diff --git a/frontend/components/form/SlugTextField.tsx b/frontend/components/form/SlugTextField.tsx index c70f894..941824f 100644 --- a/frontend/components/form/SlugTextField.tsx +++ b/frontend/components/form/SlugTextField.tsx @@ -50,19 +50,21 @@ export default function SlugTextField({ // default options autoComplete='off' variant='outlined' - InputProps={{ - startAdornment: ( - - {baseUrl} - - ), - endAdornment: ( - loading ? -
- -
- : null - ) + slotProps={{ + input:{ + startAdornment: ( + + {baseUrl} + + ), + endAdornment: ( + loading ? +
+ +
+ : null + ) + } }} {...options} {...register} diff --git a/frontend/components/form/TextFieldWithCounter.tsx b/frontend/components/form/TextFieldWithCounter.tsx index ecc88b9..d7c97b6 100644 --- a/frontend/components/form/TextFieldWithCounter.tsx +++ b/frontend/components/form/TextFieldWithCounter.tsx @@ -54,10 +54,20 @@ export default function TextFieldWithCounter({options, register}: fullWidth={options?.fullWidth ?? true } variant={options?.variant ?? 'standard'} defaultValue={options?.defaultValue ?? null} - FormHelperTextProps={{ - sx:{ - display: 'flex', - justifyContent:'space-between' + slotProps={{ + input:{ + startAdornment: options?.startAdornment ? + {options?.startAdornment} + : undefined, + endAdornment: options?.endAdornment ? + {options?.endAdornment} + : undefined + }, + formHelperText:{ + sx:{ + display: 'flex', + justifyContent:'space-between' + } } }} helperText={ @@ -66,10 +76,6 @@ 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/components/layout/LogoMenu.tsx b/frontend/components/layout/LogoMenu.tsx index 0ca5faa..f9fff07 100644 --- a/frontend/components/layout/LogoMenu.tsx +++ b/frontend/components/layout/LogoMenu.tsx @@ -95,11 +95,13 @@ export default function LogoMenu({logo, onAddLogo, onRemoveLogo}: AdminMenuLogoP anchorEl={anchorEl} open={open} onClose={handleClose} - MenuListProps={{ - 'aria-labelledby': 'menu-button', - }} transformOrigin={{horizontal: 'right', vertical: 'top'}} anchorOrigin={{horizontal: 'right', vertical: 'bottom'}} + slotProps={{ + list: { + 'aria-labelledby': 'menu-button', + } + }} > // SPDX-FileCopyrightText: 2022 - 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 Jesús García Gonzalez (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) // SPDX-FileCopyrightText: 2022 dv4all @@ -19,13 +19,13 @@ import UserMenu from '~/components/layout/UserMenu' import LoginDialog from './LoginDialog' export default function LoginButton() { - const providers = useLoginProviders() + const {providers} = useLoginProviders() const {status} = useSession() const [open, setOpen] = useState(false) // console.group('LoginButton') // console.log('status...', status) - // console.log('menuItems...', menuItems) + // console.log('providers...', providers) // console.groupEnd() if (status === 'loading') { @@ -39,31 +39,16 @@ export default function LoginButton() { ) } - // when there are multiple providers - // we show modal with the list of login options - if (providers?.length > 1) { - return ( -
- - setOpen(false)} - /> -
- ) + // if no providers we do not show login button + if (providers.length === 0) { + return null } - if (providers?.length===1){ - // if there is only 1 provider we link the redirect directly to Sign in button + // if there is only 1 provider we link the redirect directly to Sign in button + if (providers.length === 1){ return ( @@ -72,6 +57,21 @@ export default function LoginButton() { ) } - // if no providers we do not shown login button - return null + // when there are multiple providers + // we show modal with the list of login options + return ( +
+ + setOpen(false)} + /> +
+ ) } diff --git a/frontend/components/login/LoginDialog.tsx b/frontend/components/login/LoginDialog.tsx index 27b5a98..5cbfabe 100644 --- a/frontend/components/login/LoginDialog.tsx +++ b/frontend/components/login/LoginDialog.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all // SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -13,7 +14,7 @@ import {useTheme} from '@mui/material/styles' import CloseIcon from '@mui/icons-material/Close' import IconButton from '@mui/material/IconButton' -import {Provider} from 'pages/api/fe/auth' +import {Provider} from '~/auth/api/getLoginProviders' import useRsdSettings from '~/config/useRsdSettings' import LoginProviders from './LoginProviders' diff --git a/frontend/components/login/LoginProviders.tsx b/frontend/components/login/LoginProviders.tsx index 766ca80..3b94335 100644 --- a/frontend/components/login/LoginProviders.tsx +++ b/frontend/components/login/LoginProviders.tsx @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -9,7 +10,7 @@ import ListItem from '@mui/material/ListItem' import ListItemText from '@mui/material/ListItemText' import Chip from '@mui/material/Chip' -import {Provider} from 'pages/api/fe/auth' +import {Provider} from '~/auth/api/getLoginProviders' import useRsdSettings from '~/config/useRsdSettings' type LoginProvidersProps=Readonly<{ @@ -55,11 +56,10 @@ export default function LoginProviders({providers,login_info_url}:LoginProviders let color = 'default' if (provider?.accessType==='EVERYONE') color='success' if (provider?.accessType==='INVITE_ONLY') color='warning' - if (provider?.accessType==='DISABLED') color='error' return ( {login_info_url ?

- You can find more information in our documentation. + You can find more information on signing in to the RSD in our documentation.

: null } diff --git a/frontend/components/menu/IconBtnMenuOnAction.tsx b/frontend/components/menu/IconBtnMenuOnAction.tsx index 8683a6b..1d954ad 100644 --- a/frontend/components/menu/IconBtnMenuOnAction.tsx +++ b/frontend/components/menu/IconBtnMenuOnAction.tsx @@ -96,11 +96,13 @@ export default function IconBtnMenuOnAction({ anchorEl={anchorEl} open={open} onClose={handleClose} - MenuListProps={{ - 'aria-labelledby': 'menu-button', - }} transformOrigin={{horizontal: 'right', vertical: 'top'}} anchorOrigin={{horizontal: 'right', vertical: 'bottom'}} + slotProps={{ + list: { + 'aria-labelledby': 'menu-button', + } + }} > {renderMenuOptions()} diff --git a/frontend/components/news/add/AddNewsCard.tsx b/frontend/components/news/add/AddNewsCard.tsx index f4fd277..347abbf 100644 --- a/frontend/components/news/add/AddNewsCard.tsx +++ b/frontend/components/news/add/AddNewsCard.tsx @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -219,8 +219,10 @@ export default function AddNewsCard() { variant: 'outlined', label: config.publication_date.label, type: 'date', - InputLabelProps:{ - shrink: true + slotProps:{ + inputLabel:{ + shrink: true + } }, sx:{ maxWidth:'13rem' diff --git a/frontend/components/news/apiNews.tsx b/frontend/components/news/apiNews.tsx index e25cdea..bcdabb6 100644 --- a/frontend/components/news/apiNews.tsx +++ b/frontend/components/news/apiNews.tsx @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -191,7 +191,7 @@ export async function getNewsList({page,rows,is_published=true,token,searchFor,o // get published meta pages ordered by position let query = paginationUrlParams({rows, page}) if (searchFor) { - query+=`&or=(title.ilike.*${searchFor}*,summary.ilike.*${searchFor}*,author.ilike.*${searchFor}*)` + query+=`&or=(title.ilike."*${searchFor}*",summary.ilike."*${searchFor}*",author.ilike."*${searchFor}*")` } if (orderBy) { query+=`&order=${orderBy}` diff --git a/frontend/components/news/edit/index.tsx b/frontend/components/news/edit/index.tsx index e5dffcd..1eeeea2 100644 --- a/frontend/components/news/edit/index.tsx +++ b/frontend/components/news/edit/index.tsx @@ -122,15 +122,16 @@ export default function EditNewsItem({item}:{item:NewsItem}) { useNull: true, defaultValue: item.publication_date, helperTextMessage: config.publication_date.help, - // helperTextCnt: `${item?.publication_date?.length || 0}/${config.slug.validation.maxLength.value}`, muiProps:{ autoComplete: 'off', variant: 'standard', label: config.publication_date.label, type: 'date', - InputLabelProps:{ - shrink: true - } + slotProps:{ + inputLabel:{ + shrink: true + } + }, } }} rules={config.slug.validation} diff --git a/frontend/components/organisation/apiOrganisations.ts b/frontend/components/organisation/apiOrganisations.ts index ac3305c..4c520ca 100644 --- a/frontend/components/organisation/apiOrganisations.ts +++ b/frontend/components/organisation/apiOrganisations.ts @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all // SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 @@ -26,10 +26,10 @@ export function organisationListUrl({search, rows = 12, page = 0}: { // NOTE 1! selectList need to include all columns used in filtering // NOTE 2! ensure selectList uses identical props as defined in OrganisationList type const selectList = 'id,parent,name,short_description,country,website,is_tenant,ror_names_string,rsd_path,logo_id,software_cnt,project_cnt,score' - let url = `${getBaseUrl()}/rpc/organisations_overview?parent=is.null&score=gt.0&order=project_cnt.desc.nullslast,name.asc&select=${selectList}` + let url = `${getBaseUrl()}/rpc/organisations_overview?parent=is.null&score=gt.0&order=is_tenant.desc,score.desc.nullslast,name.asc&select=${selectList}` // add search params if (search) { - url += `&or=(name.ilike.*${search}*, website.ilike.*${search}*, ror_names_string.ilike.*${search}*)` + url += `&or=(name.ilike."*${search}*", website.ilike."*${search}*", ror_names_string.ilike."*${search}*")` } // add pagination params url += paginationUrlParams({ diff --git a/frontend/components/profile/ProfileSearchPanel.tsx b/frontend/components/profile/ProfileSearchPanel.tsx index 2147fcb..f3f63e0 100644 --- a/frontend/components/profile/ProfileSearchPanel.tsx +++ b/frontend/components/profile/ProfileSearchPanel.tsx @@ -21,7 +21,7 @@ export default function ProfileSearchPanel({ onSetView,handleQueryChange }:ProfileSearchPanelProps) { return ( -
+
handleQueryChange('search', search)} diff --git a/frontend/components/profile/apiProfile.ts b/frontend/components/profile/apiProfile.ts index f24f5fb..91b3d5c 100644 --- a/frontend/components/profile/apiProfile.ts +++ b/frontend/components/profile/apiProfile.ts @@ -42,7 +42,7 @@ export async function getProfileSoftware({orcid,account,rows=12,page=0,search,to // include search if (search){ const encodedSearch = encodeURIComponent(search) - query+=`&or=(brand_name.ilike.*${encodedSearch}*,short_statement.ilike.*${encodedSearch}*,keywords_text.ilike.*${encodedSearch}*)` + query+=`&or=(brand_name.ilike."*${encodedSearch}*",short_statement.ilike."*${encodedSearch}*",keywords_text.ilike."*${encodedSearch}*")` } // complete url const url = `${getBaseUrl()}/rpc/software_by_public_profile?${query}` diff --git a/frontend/components/profile/overview/PersonCard.tsx b/frontend/components/profile/overview/PersonCard.tsx new file mode 100644 index 0000000..831975b --- /dev/null +++ b/frontend/components/profile/overview/PersonCard.tsx @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import Link from 'next/link' +import Avatar from '@mui/material/Avatar' + +import LogoOrcid from '~/assets/logos/logo-orcid.svg' +import {getImageUrl} from '~/utils/editImage' +import {getDisplayInitials} from '~/utils/getDisplayName' +import CardImageFrame from '~/components/cards/CardImageFrame' +import CardContentFrame from '~/components/cards/CardContentFrame' +import KeywordList from '~/components/cards/KeywordList' +import PersonMetrics from './PersonMetrics' +import {PersonsOverview} from './apiPersonsOverview' + +export default function PersonCard({person}:{person:PersonsOverview}) { + const given_names = person.display_name.split(' ')[0] ?? '' + const family_names = person.display_name.split(' ')[1] ?? '' + const initials = getDisplayInitials({given_names,family_names}) + return ( + +
+ +
+ + {initials} + +
+ +
+
+
+ + {/* personal info */} +

+ {person.display_name} +

+
+ {person?.role ?
{person.role}
: null} + {person?.affiliation ?
{person?.affiliation}
:null} + {person?.orcid ?
+ + {person.orcid} +
: null} +
+ {/* keywords */} +
+ +
+
+
+ + ) +} diff --git a/frontend/components/profile/overview/PersonListItem.tsx b/frontend/components/profile/overview/PersonListItem.tsx new file mode 100644 index 0000000..6437e71 --- /dev/null +++ b/frontend/components/profile/overview/PersonListItem.tsx @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center +// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) +// +// SPDX-License-Identifier: Apache-2.0 + +import Link from 'next/link' +import Avatar from '@mui/material/Avatar' + +import {getImageUrl} from '~/utils/editImage' +import {getDisplayInitials} from '~/utils/getDisplayName' +import OverviewListItem from '~/components/software/overview/list/OverviewListItem' +import {PersonsOverview} from './apiPersonsOverview' +import PersonMetrics from './PersonMetrics' + +export default function CommunityListItem({person}:{person:PersonsOverview}) { + const given_names = person.display_name.split(' ')[0] ?? '' + const family_names = person.display_name.split(' ')[1] ?? '' + const initials = getDisplayInitials({given_names,family_names}) + + return ( + + +
+ + {initials} + +
+
+ {/* basic info */} +
+
+ {person.display_name} +
+
+ {person.role ? `${person.role},` : null} {person.affiliation} +
+
+ {/* software count */} +
+ +
+
+ +
+ ) +} diff --git a/frontend/components/profile/overview/PersonMetrics.tsx b/frontend/components/profile/overview/PersonMetrics.tsx new file mode 100644 index 0000000..d1bf4fe --- /dev/null +++ b/frontend/components/profile/overview/PersonMetrics.tsx @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center +// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) +// +// SPDX-License-Identifier: Apache-2.0 + +import Tooltip from '@mui/material/Tooltip' +import TerminalIcon from '@mui/icons-material/Terminal' +import ListAltIcon from '@mui/icons-material/ListAlt' + +import useRsdSettings from '~/config/useRsdSettings' + +type PersonMetricsProps = { + software_cnt: number | null + project_cnt: number | null +} + +export default function PersonMetrics({software_cnt,project_cnt}:PersonMetricsProps) { + const {host} = useRsdSettings() + + function softwareMessage(){ + if (software_cnt && software_cnt === 1) { + return `${software_cnt} software package` + } + return `${software_cnt ?? 0} software packages` + } + + function projectMessage(){ + if (project_cnt && project_cnt === 1) { + return `${project_cnt} research project` + } + return `${project_cnt ?? 0} research projects` + } + + return ( + <> + { + host.modules?.includes('software') ? + +
+ + {software_cnt ?? 0} +
+
+ :null + } + { + host.modules?.includes('projects') ? + +
+ + {project_cnt} +
+
+ : null + } + + ) +} diff --git a/frontend/components/profile/overview/PersonsGrid.tsx b/frontend/components/profile/overview/PersonsGrid.tsx new file mode 100644 index 0000000..0734444 --- /dev/null +++ b/frontend/components/profile/overview/PersonsGrid.tsx @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import NoContent from '~/components/layout/NoContent' + +import {PersonsOverview} from './apiPersonsOverview' +import PersonCard from './PersonCard' + + +export default function PersonsGrid({items}:{items:PersonsOverview[]}) { + + if (typeof items == 'undefined' || items.length===0){ + return + } + + return ( +
+ {items.map((item) => ( + + ))} +
+ ) +} diff --git a/frontend/components/profile/overview/PersonsList.tsx b/frontend/components/profile/overview/PersonsList.tsx new file mode 100644 index 0000000..cc4beb3 --- /dev/null +++ b/frontend/components/profile/overview/PersonsList.tsx @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import NoContent from '~/components/layout/NoContent' +import {PersonsOverview} from './apiPersonsOverview' +import PersonListItem from './PersonListItem' + + +export default function PersonsList({items}:{items:PersonsOverview[]}) { + if (typeof items == 'undefined' || items.length===0){ + return + } + return ( +
+ {items.map((item) => ( + + ))} +
+ ) +} diff --git a/frontend/components/profile/overview/apiPersonsOverview.ts b/frontend/components/profile/overview/apiPersonsOverview.ts new file mode 100644 index 0000000..27573f5 --- /dev/null +++ b/frontend/components/profile/overview/apiPersonsOverview.ts @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import logger from '~/utils/logger' +import {extractCountFromHeader} from '~/utils/extractCountFromHeader' +import {createJsonHeaders, getBaseUrl} from '~/utils/fetchHelpers' +import {paginationUrlParams} from '~/utils/postgrestUrl' + +export type PersonsOverview = { + account: string + display_name: string + role: string | null + affiliation: string | null + avatar_id: string | null + orcid: string | null + is_public: boolean + software_cnt: number | null + project_cnt: number | null + keywords: string[] | null +} + +type GetPersonsListParams={ + page: number, + rows: number, + token?: string + searchFor?:string, + orderBy?:string, +} + +export async function getPersonsList({page,rows,token,searchFor,orderBy}:GetPersonsListParams){ + try{ + let query = paginationUrlParams({rows, page}) + if (searchFor) { + // search in name and short description + query+=`&or=(display_name.ilike."*${searchFor}*",affiliation.ilike."*${searchFor}*")` + } + if (orderBy) { + query+=`&order=${orderBy}` + } else { + query+='&order=affiliation.asc,display_name.asc' + } + // complete url + const url = encodeURI(`${getBaseUrl()}/rpc/public_persons_overview?${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 persons: PersonsOverview[] = await resp.json() + return { + count: extractCountFromHeader(resp.headers) ?? 0, + persons + } + } + logger(`getPersonsList: ${resp.status}: ${resp.statusText}`,'warn') + return { + count: 0, + persons: [] + } + }catch(e:any){ + logger(`getPersonsList: ${e.message}`,'error') + return { + count: 0, + persons: [] + } + } +} diff --git a/frontend/components/profile/tabs/index.tsx b/frontend/components/profile/tabs/index.tsx index c06d64d..fe19c11 100644 --- a/frontend/components/profile/tabs/index.tsx +++ b/frontend/components/profile/tabs/index.tsx @@ -7,8 +7,10 @@ import {useRouter} from 'next/router' import Tabs from '@mui/material/Tabs' +import useRsdSettings from '~/config/useRsdSettings' import TabAsLink from '~/components/layout/TabAsLink' import {useProfileContext} from '~/components/profile/context/ProfileContext' +import BaseSurfaceRounded from '~/components/layout/BaseSurfaceRounded' import {ProfileTabKey, profileTabItems} from './ProfileTabItems' type ProfileTabsProps={ @@ -21,32 +23,49 @@ const tabItems = Object.keys(profileTabItems) as ProfileTabKey[] export default function ProfileTabs({tab_id, isMaintainer}:ProfileTabsProps) { const router = useRouter() + const {host} = useRsdSettings() const {software_cnt,project_cnt} = useProfileContext() + + // if only one module active we do not show tabs + if ( + host?.modules?.includes('software')===false || + host?.modules?.includes('projects')===false + ){ + return ( +
+ ) + } + return ( - - {tabItems.map(key => { - const item = profileTabItems[key] - if (item.isVisible({isMaintainer})===true){ - return ( - - ) - } - })} - + + {tabItems.map(key => { + const item = profileTabItems[key] + if (item.isVisible({isMaintainer})===true){ + return ( + + ) + } + })} + + ) } diff --git a/frontend/components/projects/edit/information/AutosaveProjectPeriod.tsx b/frontend/components/projects/edit/information/AutosaveProjectPeriod.tsx index b350684..04a2274 100644 --- a/frontend/components/projects/edit/information/AutosaveProjectPeriod.tsx +++ b/frontend/components/projects/edit/information/AutosaveProjectPeriod.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -99,9 +99,11 @@ export default function AutosaveProjectPeriod({date_start, date_end}: variant: 'standard', label: config.date_start.label, type: 'date', - InputLabelProps:{ - shrink: true - } + slotProps:{ + inputLabel:{ + shrink: true + } + }, } }} /> @@ -120,9 +122,11 @@ export default function AutosaveProjectPeriod({date_start, date_end}: variant: 'standard', label: config.date_end.label, type: 'date', - InputLabelProps:{ - shrink: true - } + slotProps:{ + inputLabel:{ + shrink: true + } + }, } }} /> diff --git a/frontend/components/search/SearchInput.tsx b/frontend/components/search/SearchInput.tsx index 9cc5a1b..4a768ea 100644 --- a/frontend/components/search/SearchInput.tsx +++ b/frontend/components/search/SearchInput.tsx @@ -20,7 +20,7 @@ type SearchInputProps = { onSearch: (search:string)=>void, delay?: number, defaultValue?: string, - InputProps?: Partial | Partial | Partial + inputProps?: Partial | Partial | Partial sx?: SxProps } @@ -29,7 +29,7 @@ export default function SearchInput({ onSearch, delay = 700, defaultValue = '', - InputProps, + inputProps, sx }: SearchInputProps) { const [state, setState] = useState({ @@ -79,7 +79,9 @@ export default function SearchInput({ }, ...sx }} - InputProps={InputProps} + slotProps={{ + input: inputProps + }} /> ) } diff --git a/frontend/components/search/useSearchParams.tsx b/frontend/components/search/useSearchParams.tsx index 662e9c0..b0a315a 100644 --- a/frontend/components/search/useSearchParams.tsx +++ b/frontend/components/search/useSearchParams.tsx @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -8,8 +8,8 @@ import {useRouter} from 'next/router' import {ssrBasicParams} from '~/utils/extractQueryParam' import {QueryParams,buildFilterUrl} from '~/utils/postgrestUrl' import {useUserSettings} from '~/config/UserSettingsContext' +import {RsdModule} from '~/config/rsdSettingsReducer' -type RsdViews='organisations'|'communities'|'news' /** * Hook to extract basic query parameters rows, page and search from the url. @@ -17,7 +17,7 @@ type RsdViews='organisations'|'communities'|'news' * @param view the route of the overview page (organisations | communities | news) * @returns handleQueryChange and resetFilters methods. */ -export default function useSearchParams(view:RsdViews){ +export default function useSearchParams(view:RsdModule){ const router = useRouter() const {rsd_page_rows, setPageRows} = useUserSettings() diff --git a/frontend/components/snackbar/MuiSnackbar.tsx b/frontend/components/snackbar/MuiSnackbar.tsx index a9dc71c..82eaf44 100644 --- a/frontend/components/snackbar/MuiSnackbar.tsx +++ b/frontend/components/snackbar/MuiSnackbar.tsx @@ -1,5 +1,7 @@ // SPDX-FileCopyrightText: 2021 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2021 - 2023 dv4all +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -36,7 +38,9 @@ export default function MuiSnackbar({options, setSnackbar}:PageSnackbarType){ anchorOrigin={anchor} autoHideDuration={duration} onClose={handleClose} - TransitionComponent={slideTransition} + slots={{ + transition: slideTransition + }} >

{person?.is_public ? - + {displayName} : diff --git a/frontend/components/software/ContributorsList.tsx b/frontend/components/software/ContributorsList.tsx index 9fc291c..36379c6 100644 --- a/frontend/components/software/ContributorsList.tsx +++ b/frontend/components/software/ContributorsList.tsx @@ -102,7 +102,7 @@ export default function ContributorsList({contributors,section='software'}: { co
{item?.is_public ? - + {displayName} : diff --git a/frontend/components/software/edit/links/AutosaveConceptDoi.tsx b/frontend/components/software/edit/links/AutosaveConceptDoi.tsx index bc29916..fd5a406 100644 --- a/frontend/components/software/edit/links/AutosaveConceptDoi.tsx +++ b/frontend/components/software/edit/links/AutosaveConceptDoi.tsx @@ -1,7 +1,7 @@ // 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-FileCopyrightText: 2024 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -80,8 +80,9 @@ export default function AutosaveConceptDoi() { helperTextCnt: `${concept_doi?.length || 0}/${config.concept_doi.validation.maxLength.value}`, // add validate button as part of the input at the end muiProps:{ - InputProps:{ - endAdornment: + slotProps:{ + input:{ + endAdornment: - } + } + }, } }} rules={config.concept_doi.validation} diff --git a/frontend/components/user/communities/index.tsx b/frontend/components/user/communities/index.tsx index bbf4747..1c38f08 100644 --- a/frontend/components/user/communities/index.tsx +++ b/frontend/components/user/communities/index.tsx @@ -39,7 +39,7 @@ export default function UserCommunities() {
{/* SEARCH */} {is_public === true && account ? - + Enabled :Disabled (Why should I enable it? ) diff --git a/frontend/components/user/settings/profile/apiUserProfile.ts b/frontend/components/user/settings/profile/apiUserProfile.ts index 52e20c9..182391b 100644 --- a/frontend/components/user/settings/profile/apiUserProfile.ts +++ b/frontend/components/user/settings/profile/apiUserProfile.ts @@ -23,6 +23,7 @@ export type UserProfile = { } export type PublicUserProfile = UserProfile & { + display_name: string orcid: string | null } diff --git a/frontend/components/user/software/useUserSoftware.tsx b/frontend/components/user/software/useUserSoftware.tsx index 0c20d3c..6de5678 100644 --- a/frontend/components/user/software/useUserSoftware.tsx +++ b/frontend/components/user/software/useUserSoftware.tsx @@ -43,7 +43,7 @@ export async function getSoftwareForMaintainer({ let url =`/api/v1/rpc/software_by_maintainer?maintainer_id=${account}&order=brand_name` // search if (searchFor) { - url+=`&or=(brand_name.ilike.*${encodeURIComponent(searchFor)}*, short_statement.ilike.*${encodeURIComponent(searchFor)}*)` + url+=`&or=(brand_name.ilike."*${searchFor}*", short_statement.ilike."*${searchFor}*")` } // pagination url += paginationUrlParams({rows, page}) diff --git a/frontend/config/UserSettingsContext.tsx b/frontend/config/UserSettingsContext.tsx index 79d60d7..f9681bc 100644 --- a/frontend/config/UserSettingsContext.tsx +++ b/frontend/config/UserSettingsContext.tsx @@ -56,7 +56,7 @@ export function UserSettingsProvider(props:any){ export function useUserSettings(){ const {user,setUser} = useContext(UserSettingsContext) - function setPageLayout(layout:LayoutType){ + function setPageLayout(layout:LayoutType='grid'){ // save to cookie setDocumentCookie(layout,'rsd_page_layout') // save to state @@ -66,7 +66,7 @@ export function useUserSettings(){ }) } - function setPageRows(rows:number){ + function setPageRows(rows:number=12){ // save to cookie setDocumentCookie(rows.toString(),'rsd_page_rows') // save to state diff --git a/frontend/config/getSettingsServerSide.ts b/frontend/config/getSettingsServerSide.ts index 90bd3e3..858d39b 100644 --- a/frontend/config/getSettingsServerSide.ts +++ b/frontend/config/getSettingsServerSide.ts @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all // SPDX-FileCopyrightText: 2023 - 2024 Christian Meeßen (GFZ) -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 - 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -11,10 +11,13 @@ import {IncomingMessage} from 'http' import logger from '~/utils/logger' import {getPageLinks} from '~/components/admin/pages/useMarkdownPages' -import {defaultRsdSettings, RsdSettingsState} from './rsdSettingsReducer' +import {defaultRsdSettings, RsdModule, RsdSettingsState} from './rsdSettingsReducer' import defaultSettings from '~/config/defaultSettings.json' import {getAnnouncement} from '~/components/admin/announcements/apiAnnouncement' +// cache module list +let modules:RsdModule[]=[] + /** * getThemeSettings from local json file * theme.json can be mounted in the docker image into settings folder. @@ -70,3 +73,26 @@ export async function getSettingsServerSide(req: IncomingMessage | undefined): P // console.groupEnd() return rsdSettings } + +/** + * Get RSD modules from settings.json server side + * @returns + */ +export async function getRsdModules(){ + try{ + if (modules?.length > 0 && process.env.NODE_ENV == 'production'){ + // console.log('cached modules...', modules) + return modules + } + const settings = await getRsdSettings() + + if (settings?.host?.modules){ + modules = settings?.host?.modules + return modules + } + return modules + }catch(e:any){ + logger(`getRsdModules failed: ${e?.message}`,'warn') + return [] + } +} diff --git a/frontend/config/menuItems.tsx b/frontend/config/menuItems.tsx index ba6172f..1088e58 100644 --- a/frontend/config/menuItems.tsx +++ b/frontend/config/menuItems.tsx @@ -43,7 +43,8 @@ export const menuItems:MenuItemType[] = [ {path: '/software', match:'/software', label:'Software', module:'software'}, {path: '/projects', match: '/projects', label: 'Projects', module:'projects'}, {path: '/organisations', match: '/organisations', label: 'Organisations', module:'organisations'}, - {path: '/communities', match: '/communities', label: 'Communities', module:'communities'} + {path: '/communities', match: '/communities', label: 'Communities', module:'communities'}, + {path: '/persons', match: '/persons', label: 'Persons', module:'persons'} ] // ListItemButton styles for menus used on the edit pages @@ -156,15 +157,17 @@ export const userMenuItems: MenuItemType[] = [ module: 'user', type: 'divider', label: 'divider3', - active: ({role})=>['rsd_admin'].includes(role), + // news devider + active: ({role, modules})=>['rsd_admin'].includes(role) && modules.includes('news'), }, { module: 'user', type: 'link', label: 'News', - active: ({role})=>['rsd_admin'].includes(role), + // news menu item + active: ({role,modules})=>['rsd_admin'].includes(role) && modules.includes('news'), path: '/news', icon: , - }, { + },{ module: 'user', type: 'divider', label: 'divider4', @@ -181,3 +184,4 @@ export const userMenuItems: MenuItemType[] = [ } }, ] + diff --git a/frontend/config/rsdSettingsReducer.ts b/frontend/config/rsdSettingsReducer.ts index 8c3424d..2ffca7d 100644 --- a/frontend/config/rsdSettingsReducer.ts +++ b/frontend/config/rsdSettingsReducer.ts @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2025 dv4all // SPDX-FileCopyrightText: 2023 - 2024 Christian Meeßen (GFZ) -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 - 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2025 Dusan Mijatovic (dv4all) (dv4all) // // SPDX-License-Identifier: Apache-2.0 @@ -20,7 +20,7 @@ export type RsdSettingsState = { announcement?: string | null } -export type RsdModule= 'software'| 'projects' | 'organisations' | 'communities' | 'news' | 'user' +export type RsdModule= 'software'| 'projects' | 'organisations' | 'communities' | 'news' | 'user' | 'persons' export type RsdHost = { name: string, diff --git a/frontend/config/useModules.ts b/frontend/config/useModules.ts deleted file mode 100644 index 11db056..0000000 --- a/frontend/config/useModules.ts +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Netherlands eScience Center -// -// SPDX-License-Identifier: Apache-2.0 - -import useRsdSettings from '~/config/useRsdSettings' - -export function useModules() { - const {host} = useRsdSettings() - - // TODO: change the type of the module parameter to RsdModule when the modules news and user are also in the default settings - function isModuleEnabled(module: 'software'| 'projects' | 'organisations' | 'communities'): boolean { - if (host?.modules && host?.modules?.length > 0){ - // include only options defined for this RSD host - return host.modules.includes(module) - } - // else all menuItems are allowed by default - return true - } - - return ({ - isModuleEnabled - }) -} diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 0e5a121..f98fb2d 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -3,7 +3,7 @@ // SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Jesús García Gonzalez (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 @@ -50,10 +50,10 @@ const nextConfig: NextConfig = { destination: '/communities/:slug/software', permanent: true, }, - // profile default page + // forward old links to new location { - source: '/profile/:orcid', - destination: '/profile/:orcid/software', + source: '/profile/:orcid*', + destination: '/persons/:orcid*', permanent: true, }, ] diff --git a/frontend/next.rewrites.ts b/frontend/next.rewrites.ts index 824cd68..e0db0cf 100644 --- a/frontend/next.rewrites.ts +++ b/frontend/next.rewrites.ts @@ -2,8 +2,8 @@ // SPDX-FileCopyrightText: 2022 dv4all // SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 - 2025 Netherlands eScience Center +// SPDX-FileCopyrightText: 2024 - 2025 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) -// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // // SPDX-License-Identifier: Apache-2.0 @@ -16,12 +16,12 @@ import {Rewrite} from 'next/dist/lib/load-custom-routes' // console.log('process.env.NODE_ENV',process.env.NODE_ENV) // console.log('process.env.PWD', process.env.PWD) -let rewritesConfig:Rewrite[] = [] +let rewritesConfig: Rewrite[] = [] // NOSONAR if (process.env.NODE_ENV === 'docker' as any) { // proxies for frontend-dev service // developing using node docker container - rewritesConfig=[ + rewritesConfig = [ { source: '/image/:path*', destination: 'http://nginx/image/:path*', // NOSONAR @@ -35,7 +35,7 @@ if (process.env.NODE_ENV === 'docker' as any) { destination: 'http://nginx/auth/login/local', // NOSONAR } ] -} else if (process.env.NODE_ENV === 'development'){ +} else if (process.env.NODE_ENV === 'development') { rewritesConfig = [ { source: '/image/:path*', @@ -49,6 +49,10 @@ if (process.env.NODE_ENV === 'docker' as any) { source: '/auth/login/local', destination: 'http://localhost/auth/login/local', // NOSONAR }, + { + source: '/auth/providers', + destination: 'http://localhost/auth/providers', // NOSONAR + }, { source: '/documentation/:path*', destination: 'http://localhost/documentation/:path*', // NOSONAR diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index 1489275..09296de 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -23,6 +23,8 @@ import '../styles/global.css' // authentication import {AuthProvider, Session, getSessionSeverSide} from '~/auth' import {saveLocationCookie} from '~/auth/locationCookie' +import {getLoginProviders, Provider} from '~/auth/api/getLoginProviders' +import {LoginProvidersProvider} from '~/auth/loginProvidersContext' // theme import {loadMuiTheme} from '~/styles/rsdMuiTheme' import createEmotionCache from '~/styles/createEmotionCache' @@ -52,7 +54,8 @@ export interface MuiAppProps extends AppProps { settings: RsdSettingsState, matomo: Matomo, userSettings?: UserSettingsProps, - pluginSettings?: PluginConfig[] + pluginSettings?: PluginConfig[], + loginProviders: Provider[] } // define npgrogres setup, no spinner @@ -84,7 +87,7 @@ function RsdApp(props: MuiAppProps) { const { Component, emotionCache = clientSideEmotionCache, pageProps, session, settings, matomo, userSettings, - pluginSettings + pluginSettings, loginProviders } = props const router = useRouter() @@ -100,6 +103,7 @@ function RsdApp(props: MuiAppProps) { const [rsdSettings] = useState(settings) const [rsdUserSettings] = useState(userSettings) const [rsdPluginSettings] = useState(pluginSettings) + const [rsdLoginProviders] = useState(loginProviders) // request theme when options changed const {muiTheme, cssVariables} = useMemo(() => { return loadMuiTheme(rsdSettings.theme) @@ -128,6 +132,8 @@ function RsdApp(props: MuiAppProps) { // console.log('rsdUserSettings...', rsdUserSettings) // console.log('pluginSettings...', pluginSettings) // console.log('rsdPluginSettings...', rsdPluginSettings) + // console.log('loginProviders...', loginProviders) + // console.log('rsdLoginProviders...', rsdLoginProviders) // console.groupEnd() return ( @@ -150,7 +156,10 @@ function RsdApp(props: MuiAppProps) { {/* User settings rows, page layout etc. */} - + {/* Login providers list */} + + + @@ -203,6 +212,8 @@ RsdApp.getInitialProps = async(appContext:AppContext) => { let userSettings:UserSettingsProps|null = null // List of all plugins that can be used by the user let pluginSettings: PluginConfig[] = [] + // list of login providers + let loginProviders: Provider[] = [] // Matomo cached settings passed via getInitialProps // Note! getInitialProps does not always run server side // so we keep the last obtained values in this object @@ -226,16 +237,18 @@ RsdApp.getInitialProps = async(appContext:AppContext) => { userSettings = getUserSettings(req) // get RSD plugins from config endpoint and avatar_id - const [plugins, avatar_id] = await Promise.all([ + const [plugins, avatar_id, providers] = await Promise.all([ getPlugins({ plugins:settings.host.plugins, token:session?.token }), - getUserAvatar(session?.user?.account,session?.token) + getUserAvatar(session?.user?.account,session?.token), + getLoginProviders() ]) // save plugin and avatar values pluginSettings = plugins userSettings.avatar_id = avatar_id + loginProviders = providers // set content security header setContentSecurityPolicyHeader(res) } @@ -253,7 +266,8 @@ RsdApp.getInitialProps = async(appContext:AppContext) => { settings, matomo, userSettings, - pluginSettings + pluginSettings, + loginProviders } } diff --git a/frontend/pages/api/fe/auth/index.ts b/frontend/pages/api/fe/auth/index.ts deleted file mode 100644 index 2bc9987..0000000 --- a/frontend/pages/api/fe/auth/index.ts +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-FileCopyrightText: 2022 - 2025 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center -// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 dv4all -// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) -// SPDX-FileCopyrightText: 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Return a list of valid OpenID providers - * based on provided env. RSD_AUTH_PROVIDERS string, semicolon separated values - * Example! RSD_AUTH_PROVIDERS=surfconext:everyone;helmholtz:invites_only - */ - -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type {NextApiRequest, NextApiResponse} from 'next' - -// import providers methods -import logger from '~/utils/logger' -import {ProviderName} from '~/auth/api/authEndpoint' -import {surfconextInfo} from '~/auth/providers/surfconext' -import {helmholtzInfo} from '~/auth/providers/helmholtzid' -import {localInfo} from '~/auth/providers/local' -import {orcidInfo} from '~/auth/providers/orcid' -import {azureInfo} from '~/auth/providers/azure' -import {linkedinInfo} from '~/auth/providers/linkedin' - -export type ProviderFilter = 'INVITE_ONLY'|'EVERYONE'|'ENABLED' -export type AccessType = 'INVITE_ONLY'|'EVERYONE'|'DISABLED' - -export type ApiError = { - status: number, - message: string -} - -export type Provider = { - name: string, - redirectUrl: string, - accessType?: AccessType, - html?: string -} - -type Data = Provider[] | ApiError - -async function getRedirectInfo(provider: string) { - // split provider string and use only - const providerName = provider.toLocaleLowerCase().split(':')[0] as ProviderName - const accessType = provider.toLocaleUpperCase().split(':')[1] as AccessType ?? 'DISABLED' as AccessType - // select provider - switch (providerName) { - case 'surfconext':{ - const surf = await surfconextInfo() - if (surf){ - return { - ...surf, - accessType - } - } - return null - } - case 'helmholtz':{ - const helm = await helmholtzInfo() - if (helm){ - return { - ...helm, - accessType - } - } - return null - } - case 'orcid':{ - const orcid = await orcidInfo() - if (orcid){ - return { - ...orcid, - accessType - } - } - return null - } - case 'azure':{ - const azure = await azureInfo() - if (azure){ - return { - ...azure, - accessType - } - } - return null - } - case 'linkedin':{ - const link = await linkedinInfo() - if (link){ - return { - ...link, - accessType - } - } - return null - } - case 'local':{ - const loc = localInfo() - if (loc){ - return { - ...loc, - accessType - } - } - return null - } - default:{ - const message = `${providerName} NOT SUPPORTED, check your spelling` - logger(`api/fe/auth/index.ts: ${message}`, 'error') - throw new Error(message) - } - } -} -/** - * ServerSide get list of login providers - * @param filter - * @returns - */ -export async function ssrProvidersInfo(filter:ProviderFilter='ENABLED') { - // extract list of providers, default value surfconext - const strProviders = process.env.RSD_AUTH_PROVIDERS ?? '' - // split providers to array on ; and apply filter - const providers = strProviders.split(';') - .filter(provider=>{ - // filter out DISABLED providers - if (filter==='ENABLED') return !provider.toLowerCase().includes(':disabled') - // always show HELMHOLTZ on invite only page, if present - if (filter==='INVITE_ONLY' && - provider.toLowerCase().includes('helmholtz:')){ - return true - } - // match - return provider.toLowerCase().includes(`:${filter.toLowerCase()}`) - }) - - // add all requests - const promises: Promise[] = [] - providers.forEach(provider => { - promises.push( - getRedirectInfo(provider) - ) - }) - // return providers with redirectUrl - const resp = await Promise.allSettled(promises) - // filter null responses (if any) - const info: Provider[] = [] - resp.forEach(item => { - if (item.status === 'fulfilled' && item.value !== null) { - info.push(item.value) - } - }) - return info -} - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - try { - // check for filtering of providers - const filter = req.query['filter']?.toString() as ProviderFilter ?? 'ENABLED' as ProviderFilter - // extract list of providers from .env file - const providers = await ssrProvidersInfo(filter) - // return only 'valid' providers - res.status(200).json(providers) - } catch (e: any) { - logger(`api/fe/auth/index: ${e?.message}`, 'error') - res.status(500).json({ - status: 500, - message: e?.message - }) - } -} diff --git a/frontend/pages/communities/index.tsx b/frontend/pages/communities/index.tsx index e041798..9a1bd0b 100644 --- a/frontend/pages/communities/index.tsx +++ b/frontend/pages/communities/index.tsx @@ -44,8 +44,7 @@ type CommunitiesOverviewProps={ export default function CommunitiesOverview({count,page,rows,layout,search,communities}:CommunitiesOverviewProps) { const {user} = useSession() - const isAdmin = user?.role === 'rsd_admin' - if (!isAdmin) { + if (user?.role !== 'rsd_admin') { for (const community of communities) { community.pending_cnt = null } diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 788b625..a50d010 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -7,12 +7,13 @@ // SPDX-License-Identifier: Apache-2.0 import {app} from '~/config/app' +import {getRsdModules} from '~/config/getSettingsServerSide' +// import useRsdSettings from '~/config/useRsdSettings' import {getHomepageCounts} from '~/components/home/getHomepageCounts' import KinRpdHome from '~/components/home/kin' import {RsdHomeProps} from '~/components/home/rsd' import PageMeta from '~/components/seo/PageMeta' import CanonicalUrl from '~/components/seo/CanonicalUrl' -// import useRsdSettings from '~/config/useRsdSettings' import {TopNewsProps, getTopNews} from '~/components/news/apiNews' export type HomeProps = { @@ -50,16 +51,18 @@ export default function Home({news}: HomeProps) { // see documentation https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering export async function getServerSideProps() { // get counts for default rsd home page - const [counts,news] = await Promise.all([ + const [counts,news,modules] = await Promise.all([ getHomepageCounts(), // get top 3 (most recent) news items - getTopNews(3) + getTopNews(3), + getRsdModules() ]) // provide props to home component return { props: { counts, - news + // remove top news if news module is not enabled + news: modules.includes('news') ? news : [], }, } } diff --git a/frontend/pages/invite/rsd/[id].tsx b/frontend/pages/invite/rsd/[id].tsx index ec0e37c..8d7a44f 100644 --- a/frontend/pages/invite/rsd/[id].tsx +++ b/frontend/pages/invite/rsd/[id].tsx @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -6,13 +7,13 @@ import {useEffect} from 'react' import {GetServerSidePropsContext} from 'next' +import {getLoginProviders, Provider} from '~/auth/api/getLoginProviders' import useRsdSettings from '~/config/useRsdSettings' import MainContent from '~/components/layout/MainContent' import PageBackground from '~/components/layout/PageBackground' import AppHeader from '~/components/AppHeader' import AppFooter from '~/components/AppFooter' import LoginProviders from '~/components/login/LoginProviders' -import {Provider, ssrProvidersInfo} from 'pages/api/fe/auth' type RsdInvitePageProps=Readonly<{ id: string | null @@ -79,13 +80,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { } // get list of providers that support INVITE_ONLY - const providers = await ssrProvidersInfo('INVITE_ONLY') + const providers = await getLoginProviders() + const inviteOnlyProviders = providers.filter(p => p.accessType === 'INVITE_ONLY' || p.openidProvider === 'helmholtz') return { props: { id, token: token ?? null, - providers + providers: inviteOnlyProviders } } diff --git a/frontend/pages/login/index.tsx b/frontend/pages/login/index.tsx index 9bac791..a06e422 100644 --- a/frontend/pages/login/index.tsx +++ b/frontend/pages/login/index.tsx @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2025 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -6,12 +7,12 @@ import {GetServerSidePropsContext} from 'next' import {getRsdTokenNode, useSession} from '~/auth' +import {Provider,getLoginProviders} from '~/auth/api/getLoginProviders' import MainContent from '~/components/layout/MainContent' import PageBackground from '~/components/layout/PageBackground' import AppHeader from '~/components/AppHeader' import AppFooter from '~/components/AppFooter' import LoginProviders from '~/components/login/LoginProviders' -import {Provider, ssrProvidersInfo} from 'pages/api/fe/auth' export default function LoginPage({providers}:Readonly<{providers:Provider[]}>) { const {status} = useSession() @@ -58,18 +59,18 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { } // get list of available providers - const providers = await ssrProvidersInfo() + const providers = await getLoginProviders() // if no providers we show 404 page if (providers?.length === 0){ return { notFound: true } - }else if (providers?.length === 1 && providers[0]?.redirectUrl){ + }else if (providers?.length === 1 && providers[0]?.signInUrl){ // when only 1 provider we redirect directly return { redirect: { - destination: providers[0]?.redirectUrl, + destination: providers[0]?.signInUrl, permanent: false }, } diff --git a/frontend/pages/news/index.tsx b/frontend/pages/news/index.tsx index 4d543db..c2df0bd 100644 --- a/frontend/pages/news/index.tsx +++ b/frontend/pages/news/index.tsx @@ -9,6 +9,7 @@ import Pagination from '@mui/material/Pagination' import PaginationItem from '@mui/material/PaginationItem' import {app} from '~/config/app' +import {getRsdModules} from '~/config/getSettingsServerSide' import {ssrBasicParams} from '~/utils/extractQueryParam' import {getUserSettings, setDocumentCookie} from '~/utils/userSettings' import PageMeta from '~/components/seo/PageMeta' @@ -142,7 +143,14 @@ export async function getServerSideProps(context:GetServerSidePropsContext) { const {req} = context const {search, rows, page} = ssrBasicParams(context.query) const token = req?.cookies['rsd_token'] + const modules = await getRsdModules() + // show 404 page if module is not enabled + if (modules.includes('news')===false){ + return { + notFound: true, + } + } // extract user settings from cookie const {rsd_page_layout,rsd_page_rows} = getUserSettings(context.req) // use url param if present else user settings diff --git a/frontend/pages/profile/[id]/[tab].tsx b/frontend/pages/persons/[id]/[tab].tsx similarity index 81% rename from frontend/pages/profile/[id]/[tab].tsx rename to frontend/pages/persons/[id]/[tab].tsx index 2ffcc66..5b666df 100644 --- a/frontend/pages/profile/[id]/[tab].tsx +++ b/frontend/pages/persons/[id]/[tab].tsx @@ -12,16 +12,18 @@ import {isOrcid} from '~/utils/getORCID' import PageMeta from '~/components/seo/PageMeta' import CanonicalUrl from '~/components/seo/CanonicalUrl' import BackgroundAndLayout from '~/components/layout/BackgroundAndLayout' -// import BaseSurfaceRounded from '~/components/layout/BaseSurfaceRounded' + import {getProfileProjects,getProfileSoftware} from '~/components/profile/apiProfile' import ProfileMetadata from '~/components/profile/metadata' -// import ProfileTabs from '~/components/profile/tabs' +import ProfileTabs from '~/components/profile/tabs' import ProfileTabContent from '~/components/profile/tabs/ProfileTabContent' import {ProfileContextProvider} from '~/components/profile/context/ProfileContext' import {ProfileTabKey} from '~/components/profile/tabs/ProfileTabItems' import {getPublicUserProfile, PublicUserProfile} from '~/components/user/settings/profile/apiUserProfile' +import {getRsdModules} from '~/config/getSettingsServerSide' +import {RsdModule} from '~/config/rsdSettingsReducer' -type SoftwareByOrcidProps=Readonly<{ +type PublicProfilePageProps=Readonly<{ orcid: string tab: ProfileTabKey publicProfile: PublicUserProfile, @@ -31,17 +33,16 @@ type SoftwareByOrcidProps=Readonly<{ projects: ProjectListItem[] }> -export default function PublicProfileByOrcidPage({ +export default function PublicProfilePage({ tab,publicProfile, software_cnt, software, project_cnt, projects -}:SoftwareByOrcidProps) { +}:PublicProfilePageProps) { // console.group('PublicProfileByOrcidPage') // console.log('orcid...', orcid) // console.log('rsd_page_rows....', rsd_page_rows) // console.log('rsd_page_layout....', rsd_page_layout) // console.log('tab....', tab) - // console.log('profiles....', profiles) // console.log('publicProfile....', publicProfile) // console.log('software....', software) // console.log('software_cnt....', software_cnt) @@ -66,15 +67,10 @@ export default function PublicProfileByOrcidPage({ }}> {/* PROFILE METADATA */} - {/* TABS - DISABLED for KIN 2025-05-16 */} - {/* - - */} + {/* TABS */} + {/* TAB CONTENT */} -
+
@@ -105,7 +101,11 @@ export async function getServerSideProps(context:GetServerSidePropsContext) { } else { account = params?.id.toString() } - const publicProfile = await getPublicUserProfile({orcid,account}) + const [publicProfile, modules] = await Promise.all([ + getPublicUserProfile({orcid,account}), + getRsdModules() + ]) + // 404 if profile is null (not found in public_user_profile) if (publicProfile === null){ return { @@ -120,8 +120,30 @@ export async function getServerSideProps(context:GetServerSidePropsContext) { let page = parseInt(query['page'] as string ?? 0) // api works with 0 page index if (page>0) page = page-1 - // we only have projects in KIN - const tab:string = 'projects' + + // select tab + let tab = '' + if (params?.tab){ + if (modules.includes(params.tab.toString() as RsdModule)===true){ + tab = params?.tab.toString() + } + }else{ + // default tab based no enabled modules + if (modules.includes('software')===true) { + tab='software' + } else if (modules.includes('projects')===true){ + tab='projects' + } + } + + // 404 if we cannot determine tab + if (tab===''){ + return { + notFound: true, + } + } + + // search const search = query?.search as string // get software, projects and public profile diff --git a/frontend/pages/persons/[id]/index.tsx b/frontend/pages/persons/[id]/index.tsx new file mode 100644 index 0000000..8d632ae --- /dev/null +++ b/frontend/pages/persons/[id]/index.tsx @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {GetServerSidePropsContext} from 'next/types' +import {getRsdModules} from '~/config/getSettingsServerSide' + +export default function Profile() { + return ( +

Profile redirect

+ ) +} + +// see documentation https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering +export async function getServerSideProps(context:GetServerSidePropsContext) { + try{ + + const {params} = context + const id = params?.id as string + + // determine active modules + const modules = await getRsdModules() + // default tab is software + let tab = '' + if (modules?.includes('software')){ + tab='software' + } else if (modules?.includes('projects')){ + tab='projects' + } + // if software and projects module are disabled we do not forward + if (tab===''){ + return { + notFound: true, + } + } + // redirect to default tab + return { + redirect: { + destination: `/persons/${id}/${tab}`, + permanent: false + }, + } + }catch{ + return { + notFound: true, + } + } +} diff --git a/frontend/pages/persons/index.tsx b/frontend/pages/persons/index.tsx new file mode 100644 index 0000000..689d725 --- /dev/null +++ b/frontend/pages/persons/index.tsx @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2025 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2025 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {GetServerSidePropsContext} from 'next/types' +import Link from 'next/link' +import Pagination from '@mui/material/Pagination' +import PaginationItem from '@mui/material/PaginationItem' + +import {app} from '~/config/app' +import {getRsdModules} from '~/config/getSettingsServerSide' +import {useUserSettings} from '~/config/UserSettingsContext' +import {getUserSettings} from '~/utils/userSettings' +import {ssrBasicParams} from '~/utils/extractQueryParam' +import PageMeta from '~/components/seo/PageMeta' +import PageBackground from '~/components/layout/PageBackground' +import AppHeader from '~/components/AppHeader' +import MainContent from '~/components/layout/MainContent' +import AppFooter from '~/components/AppFooter' +import SearchInput from '~/components/search/SearchInput' +import useSearchParams from '~/components/search/useSearchParams' +import SelectRows from '~/components/software/overview/search/SelectRows' +import {getPersonsList, PersonsOverview} from '~/components/profile/overview/apiPersonsOverview' +import PersonsGrid from '~/components/profile/overview//PersonsGrid' +import ViewToggleGroup from '~/components/projects/overview/search/ViewToggleGroup' +import PersonsList from '~/components/profile/overview/PersonsList' + +const pageTitle = `Persons | ${app.title}` +const pageDesc = 'List of persons.' + +type PersonsOverviewProps={ + count: number, + page: number, + rows: number, + search?: string, + persons: PersonsOverview[] +} + +export default function PersonsOverviewPage({count,page,rows,search,persons}:PersonsOverviewProps) { + const {handleQueryChange,createUrl} = useSearchParams('persons') + const {rsd_page_layout,setPageLayout} = useUserSettings() + const view = rsd_page_layout === 'masonry' ? 'grid' : rsd_page_layout + const numPages = Math.ceil(count / rows) + + // console.group('PersonsOverviewPage') + // console.log('count...', count) + // console.log('view...', view) + // console.log('rsd_page_layout...', rsd_page_layout) + // console.log('search...', search) + // console.log('persons...', persons) + // console.groupEnd() + + return ( + <> + {/* Page Head meta tags */} + + + + + + + {/* Page title with search and pagination */} +
+

+ Persons +

+
+ handleQueryChange('search', search)} + defaultValue={search ?? ''} + /> + + +
+
+ {/* news cards, grid is default */} + {view === 'list' ? + + : + + } + {/* Pagination */} + {numPages > 1 && +
+ { + if (item.page !== null) { + return ( + + + + ) + } else { + return ( + + ) + } + }} + /> +
+ } +
+ + {/* App footer */} + +
+ + ) +} + +// 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 {search, rows, page} = ssrBasicParams(context.query) + const token = req?.cookies['rsd_token'] + const modules = await getRsdModules() + + // console.log('modules...', modules) + + // show 404 page if module is not enabled + if (modules.includes('persons')===false){ + return { + notFound: true, + } + } + + // extract user settings from cookie + const {rsd_page_rows} = getUserSettings(context.req) + // use url param if present else user settings + const page_rows = rows ?? rsd_page_rows + + // get news items list to all pages server side + const {count,persons} = await getPersonsList({ + // api uses 0 based index + page: page>0 ? page-1 : 0, + rows: page_rows, + searchFor: search, + // default order by affiliation and name + orderBy: 'affiliation,display_name', + token + }) + + return { + // passed to the page component as props + props: { + search, + count, + page, + rows: page_rows, + persons, + }, + } + }catch{ + return { + notFound: true, + } + } +} diff --git a/frontend/public/data/settings.json b/frontend/public/data/settings.json index a456c0e..b093b87 100644 --- a/frontend/public/data/settings.json +++ b/frontend/public/data/settings.json @@ -20,7 +20,7 @@ "description": null }, "orcid_search": false, - "modules":["projects","organisations"] + "modules":["projects","organisations","news","persons"] }, "links": [ { diff --git a/frontend/utils/editOrganisation.ts b/frontend/utils/editOrganisation.ts index ff3b524..b05e6e9 100644 --- a/frontend/utils/editOrganisation.ts +++ b/frontend/utils/editOrganisation.ts @@ -1,10 +1,10 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2022 - 2025 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2022 Matthias Rüster (GFZ) -// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2025 Dusan Mijatovic (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 @@ -60,9 +60,9 @@ export async function findRSDOrganisation({searchFor, token, rorIds}: if (rorIds.length) { const rorIdsCommaSeparated = rorIds.join(',') - query += `&or=(name.ilike.*${searchFor}*,website.ilike.*${searchFor}*,ror_id.in.(${rorIdsCommaSeparated}))&limit=20` + query += `&or=(name.ilike."*${searchFor}*",website.ilike."*${searchFor}*",ror_id.in.(${rorIdsCommaSeparated}))&limit=20` } else { - query += `&or=(name.ilike.*${searchFor}*,website.ilike.*${searchFor}*)&limit=20` + query += `&or=(name.ilike."*${searchFor}*",website.ilike."*${searchFor}*")&limit=20` } const url = `${getBaseUrl()}/rpc/organisations_overview?${query}` diff --git a/frontend/utils/jest/WithAppContext.tsx b/frontend/utils/jest/WithAppContext.tsx index 23620f4..6b32d27 100644 --- a/frontend/utils/jest/WithAppContext.tsx +++ b/frontend/utils/jest/WithAppContext.tsx @@ -12,12 +12,15 @@ import {RsdSettingsState,defaultRsdSettings} from '~/config/rsdSettingsReducer' import {RsdSettingsProvider} from '~/config/RsdSettingsContext' import {UserSettingsProps, UserSettingsProvider} from '~/config/UserSettingsContext' import {LayoutType} from '~/components/software/overview/search/ViewToggleGroup' +import {LoginProvidersProvider} from '~/auth/loginProvidersContext' +import {Provider} from '~/auth/api/getLoginProviders' export type WrapProps = { props?: any session?: Session settings?: RsdSettingsState user?: UserSettingsProps + providers?: Provider[] } type WithAppContextProps = { @@ -59,6 +62,8 @@ export function WithAppContext({children,options}:WithAppContextProps) { const {muiTheme} = loadMuiTheme(settings.theme) // user settings const user = options?.user ?? defaultUserSettings + // providers list + const providers = options?.providers ?? [] // console.group('WithAppContext') // console.log('session...', session) @@ -72,7 +77,10 @@ export function WithAppContext({children,options}:WithAppContextProps) { {/* User settings rows, page layout etc. */} - {children} + {/* Login providers list */} + + {children} +