From 778a41b9ef37db6969dd3588f4505da8ab1aa732 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Tue, 7 Jan 2025 16:14:24 +0100 Subject: [PATCH 1/3] [ResultsTableMUI] added styling on column headers and rows --- package.json | 1 + src/pages/results/ResultsTableMUI.js | 64 ++++++++++++++++++++++------ 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 893d7db..ee45802 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@microlink/react-json-view": "^1.23.1", "@mui/icons-material": "^5.15.21", "@mui/material": "^5.15.21", + "@mui/styles": "^6.3.1", "downloadjs": "^1.4.7", "i18next": "^23.11.5", "i18next-http-backend": "^2.5.2", diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js index 1a7b22b..2e2dc71 100644 --- a/src/pages/results/ResultsTableMUI.js +++ b/src/pages/results/ResultsTableMUI.js @@ -2,24 +2,21 @@ import React, { useEffect, useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import MUIDataTable from 'mui-datatables'; import { createTheme, ThemeProvider } from '@mui/material'; +import { makeStyles } from '@mui/styles'; import { fetchPublicFields } from '../../actions/source'; import { fetchUserFieldsDisplaySettings } from '../../actions/user'; import { buildFieldName } from '../../Utils'; +import { useEuiTheme, transparentize, EuiTitle } from '@elastic/eui'; -const getMuiTheme = () => - createTheme({ - components: { - MuiTableRow: { - styleOverrides: { - root: { - '&:hover': { - cursor: 'pointer', - }, - }, - }, - }, +const useStyles = makeStyles(() => ({ + centeredHeader: { + '& span': { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', }, - }); + }, +})); const ResultsTableMUI = ({ searchResults, @@ -30,6 +27,8 @@ const ResultsTableMUI = ({ setResourceFlyoutDataFromId, }) => { const { t } = useTranslation('results'); + const { euiTheme } = useEuiTheme(); + const classes = useStyles(); const [publicFields, setPublicFields] = useState([]); const [userFieldsIds, setUserFieldsIds] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -64,12 +63,24 @@ const ResultsTableMUI = ({ options: { display: 'excluded' }, }, ]; - publicFields.forEach((publicField) => { + publicFields.forEach((publicField, index) => { dataColumns.push({ name: publicField.field_name, label: buildFieldName(publicField.field_name), options: { display: userFieldsIds.includes(publicField.id), + // Apply styling on columns headers + setCellHeaderProps: () => ({ + className: classes.centeredHeader, + }), + customHeadLabelRender: (columnMeta) => { + return ( + <EuiTitle size={'xxs'}> + <p>{columnMeta.label}</p> + </EuiTitle> + ); + }, + // Truncate text to avoid oversize rows customBodyRenderLite: (dataIndex, rowIndex) => { let value = rows[rowIndex][publicField.field_name]; if (value && value.length >= 150) { @@ -77,6 +88,7 @@ const ResultsTableMUI = ({ } return value; }, + // Apply a max width on table cells setCellProps: () => ({ style: { maxWidth: '350px', @@ -193,6 +205,30 @@ const ResultsTableMUI = ({ }, }; + const getMuiTheme = () => + createTheme({ + components: { + MUIDataTableBodyRow: { + styleOverrides: { + root: { + '&:nth-of-type(odd)': { + backgroundColor: transparentize(euiTheme.colors.lightShade, 0.5), + }, + }, + }, + }, + MuiTableRow: { + styleOverrides: { + root: { + '&:hover': { + cursor: 'pointer', + }, + }, + }, + }, + }, + }); + const tableOptions = { print: false, download: false, -- GitLab From 3a3b9defc46fa98daf8e27e838769ce548d7a575 Mon Sep 17 00:00:00 2001 From: rbisson <remi.bisson@inrae.fr> Date: Tue, 7 Jan 2025 16:15:13 +0100 Subject: [PATCH 2/3] [ResultsTableMUI] added comments --- src/pages/results/ResultsTableMUI.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js index 2e2dc71..0fd0c86 100644 --- a/src/pages/results/ResultsTableMUI.js +++ b/src/pages/results/ResultsTableMUI.js @@ -63,7 +63,7 @@ const ResultsTableMUI = ({ options: { display: 'excluded' }, }, ]; - publicFields.forEach((publicField, index) => { + publicFields.forEach((publicField) => { dataColumns.push({ name: publicField.field_name, label: buildFieldName(publicField.field_name), @@ -73,6 +73,7 @@ const ResultsTableMUI = ({ setCellHeaderProps: () => ({ className: classes.centeredHeader, }), + // Apply styling on columns headers text customHeadLabelRender: (columnMeta) => { return ( <EuiTitle size={'xxs'}> -- GitLab From 0559686b41bbc05054ced09666813d97f7ce21d6 Mon Sep 17 00:00:00 2001 From: Brett Choquet <brett.choquet@inra.fr> Date: Fri, 17 Jan 2025 11:09:38 +0100 Subject: [PATCH 3/3] Feature/authentication --- .dockerignore | 22 ++ .gitlab-ci.yml | 16 ++ Dockerfile | 25 ++ env.sh | 121 ++++++++++ nginx/gzip.conf | 44 ++++ nginx/nginx.conf | 60 +++++ package.json | 7 +- public/env-config.js | 8 + public/index.html | 29 +-- public/locales/en/profile.json | 3 +- public/locales/fr/maps.json | 4 +- public/locales/fr/profile.json | 3 +- src/App.js | 4 +- src/Utils.js | 208 ++--------------- src/actions/index.js | 2 - src/actions/source.js | 61 ----- src/actions/user.js | 216 ------------------ src/components/Header/Header.js | 2 +- src/components/Header/HeaderUserMenu.js | 7 +- src/context/InSylvaGatekeeperClient.js | 138 ----------- src/context/InSylvaKeycloakClient.js | 66 ------ src/context/InSylvaSearchClient.js | 67 ------ src/context/InSylvaSourceManagerClient.js | 34 --- src/context/UserContext.js | 141 ------------ src/contexts/GatekeeperContext.js | 19 ++ src/contexts/TokenContext.js | 52 +++++ src/i18n.js | 2 +- src/index.js | 58 ++--- src/pages/profile/GroupSettings.js | 166 +++++++------- src/pages/profile/MyProfile.js | 13 +- src/pages/profile/Profile.js | 84 +++---- src/pages/profile/RoleSettings.js | 35 +-- .../profile/UserFieldsDisplaySettings.js | 49 ++-- src/pages/results/ResultsTableMUI.js | 140 +++++------- .../search/AdvancedSearch/AdvancedSearch.js | 211 +++++++++-------- src/pages/search/BasicSearch/BasicSearch.js | 26 +-- src/pages/search/Search.js | 93 +++++--- src/services/GatekeeperService.js | 148 ++++++++++++ src/services/TokenService.js | 38 +++ 39 files changed, 1026 insertions(+), 1396 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitlab-ci.yml create mode 100644 Dockerfile create mode 100755 env.sh create mode 100644 nginx/gzip.conf create mode 100644 nginx/nginx.conf create mode 100644 public/env-config.js delete mode 100644 src/actions/index.js delete mode 100644 src/actions/source.js delete mode 100644 src/actions/user.js delete mode 100644 src/context/InSylvaGatekeeperClient.js delete mode 100644 src/context/InSylvaKeycloakClient.js delete mode 100644 src/context/InSylvaSearchClient.js delete mode 100644 src/context/InSylvaSourceManagerClient.js delete mode 100644 src/context/UserContext.js create mode 100644 src/contexts/GatekeeperContext.js create mode 100644 src/contexts/TokenContext.js create mode 100644 src/services/GatekeeperService.js create mode 100644 src/services/TokenService.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7dc10ba --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +# Ignore node_modules directory +node_modules/ + +# Ignore npm debug log +npm-debug.log + +# Ignore build artifacts +dist/ +build/ +out/ + +# Ignore development files +*.log +*.pid +*.lock + +# Ignore version control files +.git/ +.gitignore + +# Ignore certificates +ssl/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..f0674db --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,16 @@ +stages: + - build + +build: + stage: build + image: docker:latest + services: + - docker:dind + script: + - apk add jq + - version=$(jq -r .version package.json) + - cat $ENV_PRODUCTION > .env.production + - docker build -t registry.forgemia.inra.fr/in-sylva-development/in-sylva.search.app:$version . + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker push registry.forgemia.inra.fr/in-sylva-development/in-sylva.search.app:$version + when: manual diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d2c2bcb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:18.20.5 as builder + +WORKDIR /app/ +COPY package.json . +RUN yarn install +COPY . . +RUN yarn build + +FROM nginx:1.24-bullseye +COPY --from=builder /app/build /usr/share/nginx/html + +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf +COPY nginx/gzip.conf /etc/nginx/conf.d/gzip.conf + +WORKDIR /usr/share/nginx/html +RUN chown -R :www-data /usr/share/nginx/html + +COPY ./env.sh . +RUN chmod +x env.sh + +COPY .env.production .env +RUN ./env.sh -e .env -o ./ + +CMD ["nginx", "-g", "daemon off;"] diff --git a/env.sh b/env.sh new file mode 100755 index 0000000..5c94083 --- /dev/null +++ b/env.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +usage() { + BASENAME=$(basename "$0") + echo "Usage: $BASENAME -e <env-file> -o <output-file>" + echo " -e, --env-file Path to .env file" + echo " -o, --output-file Path to output '.env-config.js' file" + exit 1 +} + +while getopts "e:o:" opt; do + case $opt in + e) + ENV_FILE=$OPTARG + ;; + o) + OUT_DIRECTORY=$OPTARG + ;; + \?) + # Invalid option + echo "Invalid option: -$OPTARG" + usage + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." + usage + exit 1 + ;; + esac +done + +# Check if required options are provided +if [ -z "$OUT_DIRECTORY" ]; then + echo "Error: -o (output directory) is required." + echo "" + usage + exit 1 +fi + +if [ -z "$ENV_FILE" ]; then + echo "Error: -e (environment file) is required." + echo "" + usage + exit 1 +fi + +ENV_FILE_PATH=$(realpath $ENV_FILE) + +# Check if the environment file exists +if [[ ! -f $ENV_FILE_PATH ]]; then + echo "Environment file does not exist" + echo "" + usage + exit 1 +fi + +# Check if the environment file is readable +if [[ ! -r $ENV_FILE_PATH ]]; then + echo "Environment file is not readable" + echo "" + usage + exit 1 +fi + +# Check if the environment file is empty +if [[ ! -s $ENV_FILE_PATH ]]; then + echo "Environment file is empty" + echo "" + usage + exit 1 +fi + +# Check if the environment file has the correct format +BAD_LINES=$(grep -v -P '^[^=\s]+=[^=\s]+$' "$ENV_FILE_PATH") +if [ -n "$BAD_LINES" ]; then + echo "Environment file has incorrect format or contains empty values:" + echo "$BAD_LINES" + echo "" + usage + exit 1 +fi + +FULL_PATH_OUTPUT_DIRECTORY=$(realpath $OUT_DIRECTORY) + +# Check if the output directory is a directory, exists, and can be written +if [[ ! -d $FULL_PATH_OUTPUT_DIRECTORY ]]; then + echo "Output directory does not exist or is not a directory" + echo "" + usage + exit 1 +fi + +if [[ ! -w $FULL_PATH_OUTPUT_DIRECTORY ]]; then + echo "Output directory is not writable" + echo "" + usage + exit 1 +fi + +OUTPUT_FILE="env-config.js" +FULL_OUTPUT_PATH="$FULL_PATH_OUTPUT_DIRECTORY/$OUTPUT_FILE" + +# Remove the output file if it exists +if [[ -f $FULL_OUTPUT_PATH ]]; then + rm $FULL_OUTPUT_PATH +fi + +# Add assignment +echo "window._env_ = {" >>$FULL_OUTPUT_PATH + +while read -r line || [[ -n "$line" ]]; do + # Split env variables by '=' + varname=$(echo $line | cut -d'=' -f1) + value=$(echo $line | cut -d'=' -f2) + + # Append configuration property to JS file + echo " $varname: \"$value\"," >>$FULL_OUTPUT_PATH +done <$ENV_FILE_PATH + +echo "}" >>$FULL_OUTPUT_PATH diff --git a/nginx/gzip.conf b/nginx/gzip.conf new file mode 100644 index 0000000..2f54ae1 --- /dev/null +++ b/nginx/gzip.conf @@ -0,0 +1,44 @@ +# Enable Gzip compressed. +# gzip on; + + # Enable compression both for HTTP/1.0 and HTTP/1.1 (required for CloudFront). + gzip_http_version 1.0; + + # Compression level (1-9). + # 5 is a perfect compromise between size and cpu usage, offering about + # 75% reduction for most ascii files (almost identical to level 9). + gzip_comp_level 5; + + # Don't compress anything that's already small and unlikely to shrink much + # if at all (the default is 20 bytes, which is bad as that usually leads to + # larger files after gzipping). + gzip_min_length 256; + + # Compress data even for clients that are connecting to us via proxies, + # identified by the "Via" header (required for CloudFront). + gzip_proxied any; + + # Tell proxies to cache both the gzipped and regular version of a resource + # whenever the client's Accept-Encoding capabilities header varies; + # Avoids the issue where a non-gzip capable client (which is extremely rare + # today) would display gibberish if their proxy gave them the gzipped version. + gzip_vary on; + + # Compress all output labeled with one of the following MIME-types. + gzip_types + application/atom+xml + application/javascript + application/json + application/rss+xml + application/vnd.ms-fontobject + application/x-font-ttf + application/x-web-app-manifest+json + application/xhtml+xml + application/xml + font/opentype + image/svg+xml + image/x-icon + text/css + text/plain + text/x-component; + # text/html is always compressed by HttpGzipModule \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..e3c6319 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,60 @@ +include /etc/nginx/mime.types; + +server { + + listen 80; + server_name -; + + ssl_certificate /etc/ssl/fullchain.pem; + ssl_certificate_key /etc/ssl/fullchain.pem.key; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + + access_log /var/log/nginx/host.access.log; + error_log /var/log/nginx/host.error.log; + + root /usr/share/nginx/html; + index index.html index.htm; + + location /static/media/ { + try_files $uri /usr/share/nginx/html/static/media; + } + + location / { + + root /usr/share/nginx/html; + index index.html; + autoindex on; + set $fallback_file /index.html; + if ($http_accept !~ text/html) { + set $fallback_file /null; + } + if ($uri ~ /$) { + set $fallback_file /null; + } + try_files $uri $fallback_file; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + + add_header 'Access-Control-Allow-Origin' "$http_origin" always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always; + } + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/package.json b/package.json index ee45802..299f394 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "in-sylva.search", - "version": "1.0.0", + "version": "1.0.1", "private": true, "homepage": ".", "dependencies": { @@ -18,6 +18,7 @@ "i18next-http-backend": "^2.5.2", "moment": "^2.27.0", "mui-datatables": "^4.3.0", + "oidc-react": "^3.4.1", "ol": "^9.2.4", "proj4": "^2.11.0", "react": "^18.3.1", @@ -30,8 +31,8 @@ "react-use-storage": "^0.5.1" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", + "start": "./env.sh -e .env.development -o ./public && BROWSER=none NODE_OPTIONS=--openssl-legacy-provider react-scripts start", + "build": "NODE_OPTIONS=--openssl-legacy-provider react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "lint": "eslint ./src", diff --git a/public/env-config.js b/public/env-config.js new file mode 100644 index 0000000..b723f4b --- /dev/null +++ b/public/env-config.js @@ -0,0 +1,8 @@ +window._env_ = { + REACT_APP_BASE_URL: "http://localhost:3000", + REACT_APP_KEYCLOAK_BASE_URL: "https://in-sylva.inrae.fr/keycloak/realms/in-sylva", + REACT_APP_KEYCLOAK_CLIENT_ID: "in-sylva.user.app", + REACT_APP_KEYCLOAK_CLIENT_SECRET: "oONCnrlp0zvFnXphZdbGhRUhADZvtu5D", + REACT_APP_IN_SYLVA_GATEKEEPER_BASE_URL: "https://in-sylva.inrae.fr/gatekeeper", + NODE_TLS_REJECT_UNAUTHORIZED: "0", +} diff --git a/public/index.html b/public/index.html index 5e6c564..d605a92 100644 --- a/public/index.html +++ b/public/index.html @@ -1,12 +1,13 @@ <!DOCTYPE html> <html lang="en"> -<head> - <meta charset="utf-8" /> - <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <meta name="theme-color" content="#000000" /> - <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> - <!-- + <head> + <meta charset="utf-8" /> + <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.png" /> + <meta name="viewport" + content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <meta name="theme-color" content="#000000" /> + <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> + <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build. Only files inside the `public` folder can be referenced from the HTML. @@ -15,11 +16,11 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - <title>IN-SYLVA Search</title> - <script src="%PUBLIC_URL%/env-config.js"></script> -</head> -<body> - <noscript>You need to enable JavaScript to run this app.</noscript> - <div id="root"></div> -</body> + <title>IN-SYLVA Search</title> + <script src="%PUBLIC_URL%/env-config.js"></script> + </head> + <body style="font-family: 'Roboto', sans-serif;"> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div id="root"></div> + </body> </html> diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 4daa91e..d0dc82c 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -46,7 +46,8 @@ "groups": { "groupsList": "Existing groups", "groupName": "Name", - "groupDescription": "Description" + "groupDescription": "Description", + "groupMember": "Member ?" }, "requestsList": { "requestsList": "Requests list", diff --git a/public/locales/fr/maps.json b/public/locales/fr/maps.json index 51f7f8d..072712e 100644 --- a/public/locales/fr/maps.json +++ b/public/locales/fr/maps.json @@ -25,8 +25,8 @@ "title": "Ressources sélectionnées", "empty": "Sélectionnez des ressources pour les afficher ici", "actions": { - "openResourceFlyout": "Ouvrir la fiche de la resource", - "unselectResource": "Désélectionner la resource" + "openResourceFlyout": "Ouvrir la fiche de la ressource", + "unselectResource": "Déselectionner la resource" } } } diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json index 22d784c..60412a9 100644 --- a/public/locales/fr/profile.json +++ b/public/locales/fr/profile.json @@ -46,7 +46,8 @@ "groups": { "groupsList": "Groupes existants", "groupName": "Nom", - "groupDescription": "Description" + "groupDescription": "Description", + "groupMember": "Member ?" }, "requestsList": { "requestsList": "Liste des requêtes", diff --git a/src/App.js b/src/App.js index 181dc86..80aa31b 100644 --- a/src/App.js +++ b/src/App.js @@ -4,7 +4,6 @@ import { createHashRouter, Route, createRoutesFromElements, - Navigate, } from 'react-router-dom'; import Home from './pages/home'; import Search from './pages/search'; @@ -16,9 +15,8 @@ const App = () => { const router = createHashRouter( createRoutesFromElements( <> - <Route path="/" element={<Navigate to="/home" />} /> <Route errorElement={<ErrorBoundary />} element={<Layout />}> - <Route index path="/home" element={<Home />} /> + <Route index path="/" element={<Home />} /> <Route path="/search" element={<Search />} /> <Route path="/profile" element={<Profile />} /> </Route> diff --git a/src/Utils.js b/src/Utils.js index 7042c6f..65fc6c4 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -8,12 +8,6 @@ export class SearchField { } } -export const getLoginUrl = () => { - return process.env.REACT_APP_IN_SYLVA_LOGIN_PORT - ? `${process.env.REACT_APP_IN_SYLVA_LOGIN_HOST}:${process.env.REACT_APP_IN_SYLVA_LOGIN_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_LOGIN_HOST}`; -}; - export const removeNullFields = (standardFields) => { let standardFieldsNoNull = standardFields; let nullIndex; @@ -47,28 +41,32 @@ export const getSections = (fields) => { }; export const getFieldsBySection = (fields, fieldSection) => { + if (!fieldSection) { + return []; + } + let filteredFields = []; fields.forEach((field) => { if ( field.field_name && field.field_name.split('.', 1)[0].replace(/_|\./g, ' ') === fieldSection.label ) { - if (field.sources.length) { - filteredFields.push({ - label: field.field_name - .substring(field.field_name.indexOf('.') + 1) - .replace(/_|\./g, ' '), - color: 'danger', - }); - } else { - filteredFields.push({ - label: field.field_name - .substring(field.field_name.indexOf('.') + 1) - .replace(/_|\./g, ' '), - color: 'primary', - }); - } + // if (field.sources.length) { + // filteredFields.push({ + // label: field.field_name + // .substring(field.field_name.indexOf('.') + 1) + // .replace(/_|\./g, ' '), + // color: 'danger', + // }); + // } else { + filteredFields.push({ + label: field.field_name + .substring(field.field_name.indexOf('.') + 1) + .replace(/_|\./g, ' '), + color: 'primary', + }); } + //} }); return filteredFields; }; @@ -268,7 +266,7 @@ const buildDslClause = (clause, fields) => { // const buildDslQuery = (request, query, fields) => { // let tmpQuery = query -const buildDslQuery = (request, fields) => { +export const buildDslQuery = (request, fields) => { let requestStr = request.replace(/ OR /g, ' | ').replace(/ AND /g, ' & ').trim(); let splitRequest = splitString(requestStr, /\(/, /\)/, /&|\|/); let tmpQuery = ''; @@ -361,70 +359,6 @@ export const createAdvancedQueriesBySource = ( const indicesLists = []; const publicFieldnames = []; const privateFields = []; - /* fields.forEach(field => { - if (field.ispublic) { - publicFieldnames.push(field.field_name) - } else { - privateFields.push(field) - } - }) - - if (selectedSources.length) { - selectedSources.forEach(source => { - queriedSourcesId.push(source.id) - }) - } else { - sources.forEach(source => { - queriedSourcesId.push(source.id) - }) - } - - noPolicySourcesId = queriedSourcesId - - //browse fields to create a map containing the fields available by source - //ie : [field1, field2] - [source1], [field1, field3, field4] - [source2], [field1] - [] - //empty brackets means "the sources not affected by policies" at that point - this list is being compiled at the same time - if (privateFields.length) { - privateFields.forEach(field => { - field.sources.forEach(sourceId => { - if (queriedSourcesId.includes(sourceId)) { - const index = findArray(sourcesLists, [sourceId]) - if (index >= 0) { - fieldsLists[index].push(field.field_name) - } else { - fieldsLists.push([field.field_name]) - sourcesLists.push([sourceId]) - } - } - //filter no policy sources - if (noPolicySourcesId.includes(sourceId)) - noPolicySourcesId = removeArrayElement(noPolicySourcesId, noPolicySourcesId.indexOf(sourceId)) - }) - }) - //add the public fields for every source - fieldsLists.forEach(fieldList => { - fieldList.push(...publicFieldnames) - }) - } - - const indexOfAllSources = findArray(sourcesLists, []) - //if there isn't any source with no policy, remove corresponding items in sourcesLists and fieldsLists - if (!noPolicySourcesId.length) { - sourcesLists = removeArrayElement(sourcesLists, indexOfAllSources) - fieldsLists = removeArrayElement(fieldsLists, indexOfAllSources) - } else { - sourcesLists = updateArrayElement(sourcesLists, indexOfAllSources, noPolicySourcesId) - } - - //get elastic indices from sources id - sourcesLists.forEach(sourceList => { - const indicesList = [] - sourceList.forEach(sourceId => { - const source = sources.find(src => src.id === sourceId) - indicesList.push(source.index_id) - }) - indicesLists.push(indicesList) - }) */ fields.forEach((field) => { if (field.ispublic) { @@ -499,6 +433,7 @@ export const createAdvancedQueriesBySource = ( }); const queryContent = buildDslQuery(searchRequest, fields); + console.log('dslQuery', queryContent); sourcesLists.forEach((sourcesArray, index) => { let sourceParam = `"_source": [`; @@ -517,104 +452,3 @@ export const createAdvancedQueriesBySource = ( }); return queries; }; - -export const createBasicQueriesBySource = ( - fields, - searchRequest, - selectedSources, - sources -) => { - const queries = []; - let fieldsLists = [[]]; - let sourcesLists = [[]]; - let queriedSourcesId = []; - let noPolicySourcesId = []; - const indicesLists = []; - const publicFieldnames = []; - const privateFields = []; - - fields.forEach((field) => { - if (field.ispublic) { - publicFieldnames.push(field.field_name); - } else { - privateFields.push(field); - } - }); - - if (selectedSources.length) { - selectedSources.forEach((source) => { - queriedSourcesId.push(source.id); - }); - } else { - if (sources.length) { - sources.forEach((source) => { - queriedSourcesId.push(source.id); - }); - } - } - - noPolicySourcesId = queriedSourcesId; - - //browse fields to create a map containing the fields available by source - //ie : [field1, field2] - [source1], [field1, field3, field4] - [source2], [field1] - [] - //empty brackets means "the sources not affected by policies" at that point - if (privateFields.length) { - privateFields.forEach((field) => { - field.sources.forEach((sourceId) => { - if (queriedSourcesId.includes(sourceId)) { - const index = findArray(sourcesLists, [sourceId]); - if (index >= 0) { - fieldsLists[index].push(field.field_name); - } else { - fieldsLists.push([field.field_name]); - sourcesLists.push([sourceId]); - } - } - //filter no policy sources - if (noPolicySourcesId.includes(sourceId)) - noPolicySourcesId = removeArrayElement( - noPolicySourcesId, - noPolicySourcesId.indexOf(sourceId) - ); - }); - }); - //add the public fields for every source - fieldsLists.forEach((fieldList) => { - fieldList.push(...publicFieldnames); - }); - } - - const indexOfAllSources = findArray(sourcesLists, []); - //if there is no source with no policy, remove corresponding items in sourcesLists and fieldsLists - if (!noPolicySourcesId.length) { - sourcesLists = removeArrayElement(sourcesLists, indexOfAllSources); - fieldsLists = removeArrayElement(fieldsLists, indexOfAllSources); - } else { - sourcesLists = updateArrayElement(sourcesLists, indexOfAllSources, noPolicySourcesId); - fieldsLists = updateArrayElement(fieldsLists, indexOfAllSources, publicFieldnames); - } - - //get elastic indices from sources id - sourcesLists.forEach((sourceList) => { - const indicesList = []; - sourceList.forEach((sourceId) => { - const source = sources.find((src) => src.id === sourceId); - indicesList.push(source.index_id); - }); - indicesLists.push(indicesList); - }); - - sourcesLists.forEach((sourcesArray, index) => { - let sourceParam = `"_source": [`; - fieldsLists[index].forEach((fieldName) => { - sourceParam = `${sourceParam} "${fieldName}", `; - }); - if (sourceParam.endsWith(', ')) { - sourceParam = sourceParam.substring(0, sourceParam.length - 2); - } - sourceParam = `${sourceParam}],`; - let query = `{ ${sourceParam} "query": { "multi_match": { "query": "${searchRequest}", "operator": "AND", "type": "cross_fields" } } }`; - queries.push({ indicesId: indicesLists[index], query: JSON.parse(query) }); - }); - return queries; -}; diff --git a/src/actions/index.js b/src/actions/index.js deleted file mode 100644 index 245f568..0000000 --- a/src/actions/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import * as user from './user'; -export { user }; diff --git a/src/actions/source.js b/src/actions/source.js deleted file mode 100644 index cd208f5..0000000 --- a/src/actions/source.js +++ /dev/null @@ -1,61 +0,0 @@ -import { InSylvaSourceManagerClient } from '../context/InSylvaSourceManagerClient'; -import { InSylvaSearchClient } from '../context/InSylvaSearchClient'; -import { refreshToken } from '../context/UserContext'; -import { tokenTimedOut } from '../Utils'; - -const ismClient = new InSylvaSourceManagerClient(); -ismClient.baseUrl = process.env.REACT_APP_IN_SYLVA_SOURCE_MANAGER_PORT - ? `${process.env.REACT_APP_IN_SYLVA_SOURCE_MANAGER_HOST}:${process.env.REACT_APP_IN_SYLVA_SOURCE_MANAGER_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_SOURCE_MANAGER_HOST}`; -const isClient = new InSylvaSearchClient(); -isClient.baseUrl = process.env.REACT_APP_IN_SYLVA_SEARCH_PORT - ? `${process.env.REACT_APP_IN_SYLVA_SEARCH_HOST}:${process.env.REACT_APP_IN_SYLVA_SEARCH_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_SEARCH_HOST}`; - -export { - fetchPublicFields, - fetchUserPolicyFields, - fetchSources, - searchQuery, - getQueryCount, -}; - -async function fetchPublicFields() { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - refreshToken(); - } - ismClient.token = sessionStorage.getItem('access_token'); - return await ismClient.publicFields(); -} - -async function fetchUserPolicyFields(kcId) { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - refreshToken(); - } - ismClient.token = sessionStorage.getItem('access_token'); - return await ismClient.userPolicyFields(kcId); -} - -async function fetchSources(kcId) { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - refreshToken(); - } - ismClient.token = sessionStorage.getItem('access_token'); - return await ismClient.sourcesWithIndexes(kcId); -} - -async function searchQuery(query) { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - refreshToken(); - } - isClient.token = sessionStorage.getItem('access_token'); - return await isClient.search(query); -} - -async function getQueryCount(queries) { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - refreshToken(); - } - isClient.token = sessionStorage.getItem('access_token'); - return await isClient.count(queries); -} diff --git a/src/actions/user.js b/src/actions/user.js deleted file mode 100644 index d5cc589..0000000 --- a/src/actions/user.js +++ /dev/null @@ -1,216 +0,0 @@ -import { InSylvaGatekeeperClient } from '../context/InSylvaGatekeeperClient'; -import { refreshToken } from '../context/UserContext'; -import { tokenTimedOut } from '../Utils'; - -const igClient = new InSylvaGatekeeperClient(); -igClient.baseUrl = process.env.REACT_APP_IN_SYLVA_GATEKEEPER_PORT - ? `${process.env.REACT_APP_IN_SYLVA_GATEKEEPER_HOST}:${process.env.REACT_APP_IN_SYLVA_GATEKEEPER_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_GATEKEEPER_HOST}`; - -export const findOneUser = async (id, request = igClient) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - const user = await request.findOneUser(id); - if (user) { - return user; - } - } catch (error) { - console.error(error); - } -}; - -export const findOneUserWithGroupAndRole = async (kcId) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - const user = await igClient.findOneUserWithGroupAndRole(kcId); - if (user) { - return user; - } - } catch (error) { - console.error(error); - } -}; - -export const getGroups = async () => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - const groups = await igClient.getGroups(); - if (groups) { - return groups; - } - } catch (error) { - console.error(error); - } -}; - -export const getRoles = async () => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - const roles = await igClient.findRole(); - if (roles) { - return roles; - } - } catch (error) { - console.error(error); - } -}; - -export const sendMail = async (subject, message, request = igClient) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - await request.sendMail(subject, message); - } catch (error) { - console.error(error); - } -}; - -export const fetchUserDetails = async (kcId) => { - try { - return await igClient.getUserDetails(kcId); - } catch (error) { - console.error(error); - } -}; - -export const fetchUserRequests = async (kcId) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - const requests = await igClient.getUserRequests(kcId); - if (requests) { - return requests; - } - } catch (error) { - console.error(error); - } -}; - -export const createUserRequest = async (kcId, message) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - return await igClient.createUserRequest(kcId, message); - } catch (error) { - console.error(error); - } -}; - -export const deleteUserRequest = async (requestId) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - return await igClient.deleteUserRequest(requestId); - } catch (error) { - console.error(error); - } -}; - -export const addUserHistory = async (kcId, query, name, uiStructure, description) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - const jsonUIStructure = JSON.stringify(uiStructure); - return await igClient.addUserHistory(kcId, query, name, jsonUIStructure, description); - } catch (error) { - console.error(error); - } -}; - -export const fetchUserHistory = async (kcId) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - const history = await igClient.userHistory(kcId); - if (history) { - return history; - } - } catch (error) { - console.error(error); - } -}; - -export const deleteUserHistory = async (id) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - await igClient.deleteUserHistory(id); - } catch (error) { - console.error(error); - } -}; - -export const fetchUserFieldsDisplaySettings = async (userId) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - const row = await igClient.fetchUserFieldsDisplaySettings(userId); - return row.std_fields_ids; - } catch (error) { - console.error(error); - } -}; - -export const createUserFieldsDisplaySettings = async (userId, stdFieldsIds) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - return await igClient.createUserFieldsDisplaySettings(userId, stdFieldsIds); - } catch (error) { - console.error(error); - } -}; - -export const updateUserFieldsDisplaySettings = async (userId, stdFieldsIds) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - return await igClient.updateUserFieldsDisplaySettings(userId, stdFieldsIds); - } catch (error) { - console.error(error); - } -}; - -export const deleteUserFieldsDisplaySettings = async (userId) => { - if (tokenTimedOut(process.env.REACT_KEYCLOAK_TOKEN_VALIDITY)) { - await refreshToken(); - } - igClient.token = sessionStorage.getItem('access_token'); - try { - return await igClient.deleteUserFieldsDisplaySettings(userId); - } catch (error) { - console.error(error); - } -}; diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 2e653c1..dddf2ba 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -17,7 +17,7 @@ const routes = [ { id: 0, label: 'home', - href: '/home', + href: '/', }, { id: 1, diff --git a/src/components/Header/HeaderUserMenu.js b/src/components/Header/HeaderUserMenu.js index 0b4a4a9..cbf3f37 100644 --- a/src/components/Header/HeaderUserMenu.js +++ b/src/components/Header/HeaderUserMenu.js @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useAuth } from 'oidc-react'; import { EuiAvatar, EuiFlexGroup, @@ -8,11 +9,11 @@ import { EuiPopover, EuiButtonIcon, } from '@elastic/eui'; -import { signOut } from '../../context/UserContext'; import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; const HeaderUserMenu = () => { + const auth = useAuth(); const { t } = useTranslation('header'); const [isOpen, setIsOpen] = useState(false); const [username, setUsername] = useState(''); @@ -26,7 +27,7 @@ const HeaderUserMenu = () => { }; useEffect(() => { - setUsername(sessionStorage.getItem('username')); + setUsername(auth.userData?.profile?.preferred_username || ''); }, []); const HeaderUserButton = ( @@ -63,7 +64,7 @@ const HeaderUserMenu = () => { </NavLink> </EuiFlexItem> <EuiFlexItem grow={false}> - <NavLink to={'/'} onClick={() => signOut()}> + <NavLink to={'/'} onClick={() => auth.signOutRedirect()}> {t('header:userMenu.logOutButton')} </NavLink> </EuiFlexItem> diff --git a/src/context/InSylvaGatekeeperClient.js b/src/context/InSylvaGatekeeperClient.js deleted file mode 100644 index 26f1078..0000000 --- a/src/context/InSylvaGatekeeperClient.js +++ /dev/null @@ -1,138 +0,0 @@ -class InSylvaGatekeeperClient { - async post(method, path, requestContent) { - const headers = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.token}`, - }; - const response = await fetch(`${this.baseUrl}${path}`, { - method, - headers, - body: JSON.stringify(requestContent), - mode: 'cors', - }); - return await response.json(); - } - - async getGroups() { - const path = `/user/groups`; - return await this.post('POST', `${path}`, {}); - } - - async getUserRequests(kcId) { - const path = `/user/list-requests-by-user`; - return await this.post('POST', `${path}`, { - kcId, - }); - } - - async createUserRequest(kcId, message) { - const path = `/user/create-request`; - return await this.post('POST', `${path}`, { - kcId, - message, - }); - } - - async deleteUserRequest(id) { - const path = `/user/delete-request`; - return await this.post('DELETE', `${path}`, { id }); - } - - async findRole() { - const path = `/role/find`; - return await this.post('GET', `${path}`); - } - - async kcId({ email }) { - const path = `/user/kcid`; - return await this.post('POST', `${path}`, { - email, - }); - } - - async sendMail(subject, message) { - const path = `/user/send-mail`; - - await this.post('POST', `${path}`, { - subject, - message, - }); - } - - async findOneUser(id) { - const path = `/user/findOne`; - return await this.post('POST', `${path}`, { - id, - }); - } - - // Returns an array containing objects for each user group. - async findOneUserWithGroupAndRole(kcId) { - const path = `/user/one-with-groups-and-roles`; - return await this.post('POST', `${path}`, { - id: kcId, - }); - } - - async getUserDetails(kcId) { - const path = `/user/detail`; - return await this.post('POST', `${path}`, { - kcId, - }); - } - - async addUserHistory(kcId, query, name, uiStructure, description) { - const path = `/user/add-history`; - return await this.post('POST', `${path}`, { - kcId, - query, - name, - uiStructure, - description, - }); - } - - async userHistory(kcId) { - const path = `/user/fetch-history`; - return await this.post('POST', `${path}`, { - kcId, - }); - } - - async deleteUserHistory(id) { - const path = `/user/delete-history`; - await this.post('POST', `${path}`, { - id, - }); - } - - // GET Fetches user fields display settings - async fetchUserFieldsDisplaySettings(userId) { - return await this.post('GET', `/user/fields-display-settings/${userId}`); - } - - // POST Creates user fields display settings - async createUserFieldsDisplaySettings(userId, stdFieldsIds) { - return await this.post('POST', `/user/fields-display-settings/`, { - userId, - stdFieldsIds, - }); - } - - // PUT Updates user fields display settings - async updateUserFieldsDisplaySettings(userId, stdFieldsIds) { - return await this.post('PUT', `/user/fields-display-settings/`, { - userId, - stdFieldsIds, - }); - } - - // DELETE Remove user fields display settings - async deleteUserFieldsDisplaySettings(userId) { - return await this.post('DELETE', `/user/fields-display-settings/${userId}`); - } -} - -InSylvaGatekeeperClient.prototype.baseUrl = null; -InSylvaGatekeeperClient.prototype.token = null; -export { InSylvaGatekeeperClient }; diff --git a/src/context/InSylvaKeycloakClient.js b/src/context/InSylvaKeycloakClient.js deleted file mode 100644 index 092f27c..0000000 --- a/src/context/InSylvaKeycloakClient.js +++ /dev/null @@ -1,66 +0,0 @@ -import { getLoginUrl } from '../Utils'; - -class InSylvaKeycloakClient { - async post(path, requestContent) { - const headers = { - 'Content-Type': 'application/x-www-form-urlencoded', - // "Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE", - // "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization" - }; - let formBody = []; - for (const property in requestContent) { - const encodedKey = encodeURIComponent(property); - const encodedValue = encodeURIComponent(requestContent[property]); - formBody.push(encodedKey + '=' + encodedValue); - } - formBody = formBody.join('&'); - const response = await fetch(`${this.baseUrl}${path}`, { - method: 'POST', - headers, - body: formBody, - mode: 'cors', - }); - if (!response.ok) { - await this.logout(); - sessionStorage.removeItem('kcId'); - sessionStorage.removeItem('access_token'); - sessionStorage.removeItem('refresh_token'); - window.location.replace(getLoginUrl() + '?requestType=search'); - } - if (response.statusText !== 'No Content') { - return await response.json(); - } - } - - async refreshToken({ - realm = this.realm, - client_id = this.client_id, - grant_type = 'refresh_token', - refresh_token, - }) { - const path = `/auth/realms/${realm}/protocol/openid-connect/token`; - const token = await this.post(`${path}`, { - client_id, - grant_type, - refresh_token, - }); - return { token }; - } - - async logout() { - const refresh_token = sessionStorage.getItem('refresh_token'); - if (refresh_token) { - const client_id = this.client_id; - await this.post(`/auth/realms/${this.realm}/protocol/openid-connect/logout`, { - client_id, - refresh_token, - }); - } - } -} - -InSylvaKeycloakClient.prototype.baseUrl = null; -InSylvaKeycloakClient.prototype.client_id = null; -InSylvaKeycloakClient.prototype.grant_type = null; -InSylvaKeycloakClient.prototype.realm = null; -export { InSylvaKeycloakClient }; diff --git a/src/context/InSylvaSearchClient.js b/src/context/InSylvaSearchClient.js deleted file mode 100644 index ffdc831..0000000 --- a/src/context/InSylvaSearchClient.js +++ /dev/null @@ -1,67 +0,0 @@ -class InSylvaSearchClient { - async post(method, path, requestContent) { - const headers = { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - Authorization: `Bearer ${this.token}`, - 'Access-Control-Max-Age': 86400, - // 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH', - }; - const response = await fetch(`${this.baseUrl}${path}`, { - method, - headers, - body: JSON.stringify(requestContent), - mode: 'cors', - }); - return await response.json(); - } - - /* async search(query, indices) { - const indicesId = [] - indices.forEach(index => { - indicesId.push(index.index_id) - }) - const path = `/search`; - const result = await this.post('POST', `${path}`, { - indicesId, - query - }); - return result; - } */ - - async search(queries) { - let finalResult = []; - - for (let i = 0; i < queries.length; i++) { - const indicesId = queries[i].indicesId; - const query = queries[i].query; - const result = await this.post('POST', '/scroll-search', { - indicesId, - query, - }); - if (!result.statusCode) { - finalResult.push(...result); - } - } - return finalResult; - } - - async count(queries) { - let finalResult = 0; - for (let i = 0; i < queries.length; i++) { - const indicesId = queries[i].indicesId; - const query = queries[i].query; - const path = `/count`; - const result = await this.post('POST', `${path}`, { - indicesId, - query, - }); - finalResult = finalResult + result.count; - } - return finalResult; - } -} - -InSylvaSearchClient.prototype.baseUrl = null; -InSylvaSearchClient.prototype.token = null; -export { InSylvaSearchClient }; diff --git a/src/context/InSylvaSourceManagerClient.js b/src/context/InSylvaSourceManagerClient.js deleted file mode 100644 index ceffbd6..0000000 --- a/src/context/InSylvaSourceManagerClient.js +++ /dev/null @@ -1,34 +0,0 @@ -class InSylvaSourceManagerClient { - async post(method, path, requestContent) { - const headers = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.token}`, - }; - const response = await fetch(`${this.baseUrl}${path}`, { - method, - headers, - body: JSON.stringify(requestContent), - mode: 'cors', - }); - return await response.json(); - } - - async publicFields() { - const path = `/publicFieldList`; - return this.post('GET', `${path}`); - } - - async userPolicyFields(userId) { - const path = `/policy-stdfields`; - return this.post('POST', `${path}`, { userId }); - } - - async sourcesWithIndexes(kc_id) { - const path = `/source_indexes`; - return this.post('POST', `${path}`, { kc_id }); - } -} - -InSylvaSourceManagerClient.prototype.baseUrl = null; -InSylvaSourceManagerClient.prototype.token = null; -export { InSylvaSourceManagerClient }; diff --git a/src/context/UserContext.js b/src/context/UserContext.js deleted file mode 100644 index 1b6d8b7..0000000 --- a/src/context/UserContext.js +++ /dev/null @@ -1,141 +0,0 @@ -import React, { createContext, useContext, useReducer } from 'react'; -import { InSylvaGatekeeperClient } from './InSylvaGatekeeperClient'; -import { InSylvaKeycloakClient } from './InSylvaKeycloakClient'; -import { getLoginUrl } from '../Utils'; -import { fetchUserDetails, findOneUser } from '../actions/user'; - -const UserStateContext = createContext(null); -const UserDispatchContext = createContext(null); - -const igClient = new InSylvaGatekeeperClient(); -igClient.baseUrl = process.env.REACT_APP_IN_SYLVA_GATEKEEPER_PORT - ? `${process.env.REACT_APP_IN_SYLVA_GATEKEEPER_HOST}:${process.env.REACT_APP_IN_SYLVA_GATEKEEPER_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_GATEKEEPER_HOST}`; - -const ikcClient = new InSylvaKeycloakClient(); -ikcClient.baseUrl = process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT - ? `${process.env.REACT_APP_IN_SYLVA_KEYCLOAK_HOST}:${process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT}` - : `${window._env_.REACT_APP_IN_SYLVA_KEYCLOAK_HOST}`; -ikcClient.realm = process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT - ? `${process.env.REACT_APP_IN_SYLVA_REALM}` - : `${window._env_.REACT_APP_IN_SYLVA_REALM}`; -ikcClient.client_id = process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT - ? `${process.env.REACT_APP_IN_SYLVA_CLIENT_ID}` - : `${window._env_.REACT_APP_IN_SYLVA_CLIENT_ID}`; -ikcClient.grant_type = process.env.REACT_APP_IN_SYLVA_KEYCLOAK_PORT - ? `${process.env.REACT_APP_IN_SYLVA_GRANT_TYPE}` - : `${window._env_.REACT_APP_IN_SYLVA_GRANT_TYPE}`; - -function userReducer(state, action) { - switch (action.type) { - case 'USER_LOGGED_IN': - return { ...state, isAuthenticated: true }; - case 'SIGN_OUT_SUCCESS': - return { ...state, isAuthenticated: false }; - case 'EXPIRED_SESSION': - return { ...state, isAuthenticated: false }; - case 'USER_NOT_LOGGED_IN': - return { ...state, isAuthenticated: false }; - case 'STD_FIELDS_SUCCESS': - return { ...state, isAuthenticated: true }; - case 'STD_FIELDS_FAILURE': - return { ...state, isAuthenticated: true }; - default: { - throw new Error(`Unhandled action type: ${action.type}`); - } - } -} - -function UserProvider({ children }) { - const [state, dispatch] = useReducer(userReducer, { - isAuthenticated: !!sessionStorage.getItem('access_token'), - }); - - return ( - <UserStateContext.Provider value={state}> - <UserDispatchContext.Provider value={dispatch}> - {children} - </UserDispatchContext.Provider> - </UserStateContext.Provider> - ); -} - -function useUserState() { - const context = useContext(UserStateContext); - if (context === undefined) { - throw new Error('useUserState must be used within a UserProvider'); - } - return context; -} - -function useUserDispatch() { - const context = useContext(UserDispatchContext); - if (context === undefined) { - throw new Error('useUserDispatch must be used within a UserProvider'); - } - return context; -} - -const getUserIdFromKcId = async (kcId) => { - if (!kcId) { - return; - } - const userDetails = await fetchUserDetails(kcId); - if (!userDetails) { - return; - } - return userDetails.id; -}; - -const checkUserLogin = async (kcId, accessToken, refreshToken) => { - if (!kcId || !accessToken || !refreshToken) { - return; - } - const userId = await getUserIdFromKcId(kcId); - if (userId) { - sessionStorage.setItem('userId', userId.toString()); - } - const user = await findOneUser(kcId); - if (user) { - sessionStorage.setItem('username', user.username); - sessionStorage.setItem('email', user.email); - } - sessionStorage.setItem('kcId', kcId); - sessionStorage.setItem('access_token', accessToken); - sessionStorage.setItem('refresh_token', refreshToken); - if (!sessionStorage.getItem('token_refresh_time')) { - sessionStorage.setItem('token_refresh_time', Date.now().toString()); - } -}; - -async function refreshToken() { - if (!sessionStorage.getItem('kcId')) { - return; - } - setTimeout(async () => { - const result = await ikcClient.refreshToken({ - refresh_token: sessionStorage.getItem('refresh_token'), - }); - if (result) { - sessionStorage.setItem('access_token', result.token.access_token); - sessionStorage.setItem('token_refresh_time', Date.now().toString()); - } - }, 3000); -} - -async function signOut() { - await ikcClient.logout(); - sessionStorage.removeItem('kcId'); - sessionStorage.removeItem('access_token'); - sessionStorage.removeItem('refresh_token'); - window.location.replace(getLoginUrl() + '?requestType=search'); -} - -export { - UserProvider, - useUserState, - useUserDispatch, - checkUserLogin, - refreshToken, - signOut, -}; diff --git a/src/contexts/GatekeeperContext.js b/src/contexts/GatekeeperContext.js new file mode 100644 index 0000000..9604361 --- /dev/null +++ b/src/contexts/GatekeeperContext.js @@ -0,0 +1,19 @@ +import React, { createContext, useContext } from 'react'; +import { InSylvaGatekeeperClient } from '../services/GatekeeperService'; +import { useUserInfo } from './TokenContext'; + +const GatekeeperContext = createContext(null); + +export const useGatekeeper = () => { + const context = useContext(GatekeeperContext); + return context; +}; + +export const GatekeeperProvider = ({ children }) => { + const getUserInfo = useUserInfo(); + const client = new InSylvaGatekeeperClient(getUserInfo); + + return ( + <GatekeeperContext.Provider value={client}>{children}</GatekeeperContext.Provider> + ); +}; diff --git a/src/contexts/TokenContext.js b/src/contexts/TokenContext.js new file mode 100644 index 0000000..dfbd78f --- /dev/null +++ b/src/contexts/TokenContext.js @@ -0,0 +1,52 @@ +import React, { createContext, useContext } from 'react'; +import { useAuth } from 'oidc-react'; +import TokenService from '../services/TokenService'; + +const TokenContext = createContext(null); + +export const useUserInfo = () => { + const context = useContext(TokenContext); + return context; +}; + +export const TokenProvider = ({ children }) => { + const auth = useAuth(); + + const getUserInfo = async () => { + //console.log('Getting access token'); + if (!auth) { + //console.log('Auth not found'); + return null; + } + + if (auth.isLoading) { + //console.log('Auth is loading'); + return null; + } + + if (!auth.userData || !auth.userData.access_token) { + //console.log('User data not found'); + await auth.signOutRedirect(); + } + + const access_token = auth.userData.access_token; + //console.log(access_token); + + let userInfo = await new TokenService().getUserInfo(access_token); + if (!userInfo) { + //console.log('User info not found'); + const refreshed = await new TokenService().refreshToken( + auth.userData.refresh_token + ); + if (!refreshed) { + //console.log('Error refreshing token'); + await auth.signOutRedirect(); + } + userInfo = await new TokenService().getUserInfo(refreshed.access_token); + } + //console.log('User info:', userInfo); + return { ...userInfo, ...auth.userData }; + }; + + return <TokenContext.Provider value={getUserInfo}>{children}</TokenContext.Provider>; +}; diff --git a/src/i18n.js b/src/i18n.js index d422da2..5f49f6e 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -10,7 +10,7 @@ i18n fallbackLng: 'fr', ns: 'common', defaultNS: 'common', - debug: true, + debug: false, load: 'languageOnly', interpolation: { // not needed for react as it escapes by default diff --git a/src/index.js b/src/index.js index 3944da2..1e27e35 100644 --- a/src/index.js +++ b/src/index.js @@ -1,32 +1,38 @@ import React, { Suspense } from 'react'; -import '@elastic/eui/dist/eui_theme_light.css'; -import { UserProvider, checkUserLogin } from './context/UserContext'; +import { createRoot } from 'react-dom/client'; +import { AuthProvider } from 'oidc-react'; import App from './App'; -import { getLoginUrl, getUrlParam, redirect } from './Utils.js'; import './i18n'; import Loading from './components/Loading'; -import { createRoot } from 'react-dom/client'; import { EuiProvider } from '@elastic/eui'; +import '@elastic/eui/dist/eui_theme_light.css'; +import { GatekeeperProvider } from './contexts/GatekeeperContext'; +import { TokenProvider } from './contexts/TokenContext'; -const userId = getUrlParam('kcId', ''); -const accessToken = getUrlParam('accessToken', ''); -let refreshToken = getUrlParam('refreshToken', ''); -if (refreshToken.includes('#/search')) { - refreshToken = refreshToken.substring(0, refreshToken.indexOf('#')); -} -checkUserLogin(userId, accessToken, refreshToken); - -if (sessionStorage.getItem('access_token')) { - const root = createRoot(document.getElementById('root')); - root.render( - <UserProvider> - <Suspense fallback={<Loading />}> - <EuiProvider colorMode={'light'}> - <App /> - </EuiProvider> - </Suspense> - </UserProvider> - ); -} else { - redirect(getLoginUrl() + '?requestType=search'); -} +const root = createRoot(document.getElementById('root')); +root.render( + <AuthProvider + authority={`${process.env.REACT_APP_KEYCLOAK_BASE_URL}`} + clientId={`${process.env.REACT_APP_KEYCLOAK_CLIENT_ID}`} + clientSecret={`${process.env.REACT_APP_KEYCLOAK_CLIENT_SECRET}`} + redirectUri={`${process.env.REACT_APP_BASE_URL}`} + onBeforeSignIn={() => { + const redirectUri = window.location.hash; + return { postLoginRedirect: redirectUri }; + }} + onSignIn={async (user) => { + const postLoginRedirect = user.state?.postLoginRedirect; + window.location.href = process.env.REACT_APP_BASE_URL + '/' + postLoginRedirect; + }} + > + <TokenProvider> + <GatekeeperProvider> + <Suspense fallback={<Loading />}> + <EuiProvider colorMode={'light'}> + <App /> + </EuiProvider> + </Suspense> + </GatekeeperProvider> + </TokenProvider> + </AuthProvider> +); diff --git a/src/pages/profile/GroupSettings.js b/src/pages/profile/GroupSettings.js index 1eeb85b..90e2fe9 100644 --- a/src/pages/profile/GroupSettings.js +++ b/src/pages/profile/GroupSettings.js @@ -10,56 +10,50 @@ import { EuiFlexGroup, EuiSpacer, } from '@elastic/eui'; -import { getGroups, sendMail, createUserRequest } from '../../actions/user'; import { useTranslation } from 'react-i18next'; -import styles from './styles'; import { toast } from 'react-toastify'; import ToastMessage from '../../components/ToastMessage/ToastMessage'; +import { useGatekeeper } from '../../contexts/GatekeeperContext'; +import { useUserInfo } from '../../contexts/TokenContext'; const GroupSettings = ({ userGroups }) => { + const getUserInfo = useUserInfo(); + const client = useGatekeeper(); const { t } = useTranslation(['profile', 'common', 'validation']); const [groups, setGroups] = useState([]); const [selectedUserGroups, setSelectedUserGroups] = useState([]); const [valueError, setValueError] = useState(undefined); useEffect(() => { - setSelectedUserGroups(userGroups); + const getUserGroups = async () => { + if (!client) { + return; + } + const groupResults = await client.getGroups(); + const mappedGroups = groupResults.map((group) => { + return { + id: group.id, + label: group.name, + description: group.description, + member: userGroups.some((userGroup) => userGroup.id === group.id) ? 'X' : '', + }; + }); + setGroups(mappedGroups); + }; getUserGroups(); }, []); const groupColumns = [ { field: 'label', name: t('profile:groups.groupName'), width: '30%' }, { field: 'description', name: t('profile:groups.groupDescription') }, + { + field: 'member', + name: t('profile:groups.groupMember'), + width: '10%', + align: 'center', + }, ]; - const getUserGroups = () => { - getGroups().then((groupsResult) => { - const groupsArray = []; - groupsResult.forEach((group) => { - groupsArray.push({ - id: group.id, - label: group.name, - description: group.description, - }); - }); - setGroups(groupsArray); - }); - }; - - const getUserGroupLabels = (groups) => { - let labelList = ''; - if (!groups || groups.length === 0) { - return labelList; - } - groups.forEach((group) => { - labelList = `${labelList} ${group.label},`; - }); - if (labelList.endsWith(',')) { - labelList = labelList.substring(0, labelList.length - 1); - } - return labelList; - }; - const onValueSearchChange = (value, hasMatchingOptions) => { if (value.length !== 0 && !hasMatchingOptions) { setValueError(t('profile:groupRequests.invalidOption')); @@ -67,13 +61,14 @@ const GroupSettings = ({ userGroups }) => { }; const onSendGroupRequest = async () => { + const userInfo = await getUserInfo(); if (!selectedUserGroups || selectedUserGroups.length === 0) { return; } - const groupList = getUserGroupLabels(selectedUserGroups); - const message = `The user ${sessionStorage.getItem('username')} (${sessionStorage.getItem('email')}) has made a request to be part of these groups : ${groupList}.`; - const result = await createUserRequest(sessionStorage.getItem('kcId'), message); - await sendMail('User group request', message); + const message = `The user ${userInfo.email} has made a request to be part of these groups : ${selectedUserGroups + .map((group) => group.label) + .join(', ')}.`; + const result = await client.createUserRequest(message, userInfo.sub); if (result.error) { toast.error(<ToastMessage title={t('validation:error')} message={result.error} />); } else { @@ -82,61 +77,56 @@ const GroupSettings = ({ userGroups }) => { }; return ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}> - <EuiTitle size="s"> - <h3>{t('profile:groups.groupsList')}</h3> - </EuiTitle> - <EuiSpacer size={'l'} /> - <EuiBasicTable items={groups} columns={groupColumns} /> - </EuiPanel> - </EuiFlexItem> + userGroups && + userGroups.length > 0 && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}> + <EuiTitle size="s"> + <h3>{t('profile:groups.groupsList')}</h3> + </EuiTitle> + <EuiSpacer size={'l'} /> + <EuiBasicTable items={groups} columns={groupColumns} /> + </EuiPanel> + </EuiFlexItem> - <EuiFlexItem> - <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}> - <EuiTitle size="s"> - <h3>{t('profile:groupRequests.requestGroupAssignment')}</h3> - </EuiTitle> - <EuiSpacer size={'l'} /> - {userGroups ? ( - <p - style={styles.currentRoleOrGroupText} - >{`${t('profile:groupRequests.currentGroups')} ${getUserGroupLabels(userGroups)}`}</p> - ) : ( - <p>{t('profile:groupRequests.noGroup')}</p> - )} - <EuiSpacer size={'l'} /> - <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> - <EuiComboBox - placeholder={t('profile:groupRequests.selectGroup')} - options={groups} - selectedOptions={selectedUserGroups} - onChange={(selectedOptions) => { - setValueError(undefined); - setSelectedUserGroups(selectedOptions); - }} - onSearchChange={onValueSearchChange} - /> - </EuiFormRow> - <EuiSpacer size={'l'} /> - - <EuiFlexItem> - <div> - <EuiButton - disabled={selectedUserGroups.length === 0} - onClick={() => { - onSendGroupRequest(); + <EuiFlexItem> + <EuiPanel paddingSize="l" hasShadow={false} hasBorder={true}> + <EuiTitle size="s"> + <h3>{t('profile:groupRequests.requestGroupAssignment')}</h3> + </EuiTitle> + <EuiSpacer size={'l'} /> + <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> + <EuiComboBox + placeholder={t('profile:groupRequests.selectGroup')} + options={groups.filter((group) => group.member != 'X')} + selectedOptions={selectedUserGroups} + onChange={(selectedOptions) => { + setValueError(undefined); + setSelectedUserGroups(selectedOptions); }} - fill - > - {t('common:validationActions.send')} - </EuiButton> - </div> - </EuiFlexItem> - </EuiPanel> - </EuiFlexItem> - </EuiFlexGroup> + onSearchChange={onValueSearchChange} + /> + </EuiFormRow> + <EuiSpacer size={'l'} /> + + <EuiFlexItem> + <div> + <EuiButton + disabled={selectedUserGroups.length === 0} + onClick={() => { + onSendGroupRequest(); + }} + fill + > + {t('common:validationActions.send')} + </EuiButton> + </div> + </EuiFlexItem> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + ) ); }; diff --git a/src/pages/profile/MyProfile.js b/src/pages/profile/MyProfile.js index 57bf89a..a4d5ac5 100644 --- a/src/pages/profile/MyProfile.js +++ b/src/pages/profile/MyProfile.js @@ -10,7 +10,7 @@ import { EuiTitle, EuiIconTip, } from '@elastic/eui'; -import { deleteUserRequest } from '../../actions/user'; +import { useGatekeeper } from '../../contexts/GatekeeperContext'; import { buildFieldName } from '../../Utils'; import BulletPointList from '../../components/BulletPointList/BulletPointList'; import { toast } from 'react-toastify'; @@ -25,6 +25,7 @@ const MyProfile = ({ publicFields, getUserRequests, }) => { + const client = useGatekeeper(); const { t } = useTranslation(['profile']); const MyProfileCustomPanel = ({ @@ -70,9 +71,9 @@ const MyProfile = ({ }; const onDeleteRequest = async (request) => { - const result = await deleteUserRequest(request.id); - if (result.error) { - toast.error(<ToastMessage title={t('validation:error')} message={result.error} />); + const { success, message } = await client.deleteUserRequest(request.id); + if (!success) { + toast.error(<ToastMessage title={t('validation:error')} message={message} />); } else { toast.success(t('profile:requestsList.requestCanceled')); // Refresh requests table @@ -116,9 +117,7 @@ const MyProfile = ({ if (!userGroups || userGroups.length === 0) { return <p>{t('profile:groupRequests.noGroup')}</p>; } - const listItems = userGroups.map((group, index) => ( - <li key={index}>{group.label}</li> - )); + const listItems = userGroups.map((group, index) => <li key={index}>{group.name}</li>); return <BulletPointList>{listItems}</BulletPointList>; }; diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js index 81c0d26..111b6c3 100644 --- a/src/pages/profile/Profile.js +++ b/src/pages/profile/Profile.js @@ -5,14 +5,12 @@ import UserFieldsDisplaySettings from './UserFieldsDisplaySettings'; import GroupSettings from './GroupSettings'; import RoleSettings from './RoleSettings'; import MyProfile from './MyProfile'; -import { - fetchUserFieldsDisplaySettings, - fetchUserRequests, - findOneUserWithGroupAndRole, -} from '../../actions/user'; -import { fetchPublicFields } from '../../actions/source'; +import { useGatekeeper } from '../../contexts/GatekeeperContext'; +import { useUserInfo } from '../../contexts/TokenContext'; const Profile = () => { + const getUserInfo = useUserInfo(); + const client = useGatekeeper(); const { t } = useTranslation(['profile', 'common', 'validation']); const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [userGroups, setUserGroups] = useState([]); @@ -21,56 +19,38 @@ const Profile = () => { const [fieldsDisplaySettings, setFieldsDisplaySettings] = useState(null); const [publicFields, setPublicFields] = useState([]); - useEffect(() => { - findOneUserWithGroupAndRole(sessionStorage.getItem('kcId')).then((result) => { - if (result) { - if (result[0]) { - setUserRole(result[0].rolename); - } - const userGroupList = []; - result.forEach((userGroup) => { - if (userGroup.groupname) { - userGroupList.push({ - id: userGroup.groupid, - label: userGroup.groupname, - description: userGroup.groupdescription, - }); - } - }); - setUserGroups(userGroupList); - } - }); - getUserRequests(); - getUserFieldsDisplaySettings(); - getPublicFields(); - }, []); - - const getUserRequests = () => { - fetchUserRequests(sessionStorage.getItem('kcId')).then((userRequests) => { - if (userRequests) { - setUserRequests([...userRequests]); - } - }); - }; - - const getUserFieldsDisplaySettings = () => { - fetchUserFieldsDisplaySettings(sessionStorage.getItem('userId')).then( - (userSettings) => { - if (userSettings) { - setFieldsDisplaySettings(userSettings); - } - } + const fetchUser = async () => { + const { sub } = await getUserInfo(); + const user = await client.findUserBySub(sub); + if (!user.id) { + return; + } + const { groups, roles, requests, display_fields } = user; + setUserGroups( + groups.map((group) => { + return { + id: group.group.id, + name: group.group.name, + }; + }) ); + setUserRole(roles[0]?.role?.name); + setUserRequests(requests); + setFieldsDisplaySettings(display_fields.map((field) => field.std_field_id)); }; - const getPublicFields = () => { - fetchPublicFields().then((publicFieldsResults) => { - if (publicFieldsResults) { - setPublicFields(publicFieldsResults); - } - }); + const fetchFields = async () => { + const fields = await client.getPublicFields(); + setPublicFields(fields); }; + useEffect(() => { + if (client && selectedTabNumber === 0) { + fetchUser(); + fetchFields(); + } + }, [selectedTabNumber]); + const Tab = ({ children, description }) => { const [showCallOut, setShowCallOut] = useState(true); @@ -113,7 +93,7 @@ const Profile = () => { userRequests={userRequests} fieldsDisplaySettingsIds={fieldsDisplaySettings} publicFields={publicFields} - getUserRequests={() => getUserRequests()} + getUserRequests={() => fetchUser()} /> </Tab> ), diff --git a/src/pages/profile/RoleSettings.js b/src/pages/profile/RoleSettings.js index dc91ad2..df01652 100644 --- a/src/pages/profile/RoleSettings.js +++ b/src/pages/profile/RoleSettings.js @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useAuth } from 'oidc-react'; import { EuiTitle, EuiSelect, @@ -9,36 +10,36 @@ import { EuiSpacer, EuiFlexGroup, } from '@elastic/eui'; -import { getRoles, sendMail, createUserRequest } from '../../actions/user'; import { useTranslation } from 'react-i18next'; import styles from './styles'; import { toast } from 'react-toastify'; import ToastMessage from '../../components/ToastMessage/ToastMessage'; +import { useGatekeeper } from '../../contexts/GatekeeperContext'; +import { useUserInfo } from '../../contexts/TokenContext'; const RoleSettings = ({ userRole }) => { + const client = useGatekeeper(); + const getUserInfo = useUserInfo(); const { t } = useTranslation(['profile', 'common', 'validation']); const [roles, setRoles] = useState([]); const [selectedRole, setSelectedRole] = useState(undefined); useEffect(() => { - getUserRoles(); - }, []); - - const getUserRoles = () => { - getRoles().then((rolesResult) => { - const rolesArray = []; - rolesResult.forEach((role) => { - rolesArray.push({ id: role.id, text: role.name }); - }); - setRoles(rolesArray); - }); - }; + const fetchRoles = async () => { + const roles = await client.getRoles(); + const filteredRoles = roles + .filter((role) => role.name !== userRole) + .map((role) => ({ value: role.name, text: role.name })); + setRoles(filteredRoles); + }; + fetchRoles(); + }, [client]); const onSendRoleRequest = async () => { + const { email, sub } = await getUserInfo(); if (selectedRole) { - const message = `The user ${sessionStorage.getItem('username')} (${sessionStorage.getItem('email')}) has made a request to get the role : ${selectedRole}.`; - const result = await createUserRequest(sessionStorage.getItem('kcId'), message); - await sendMail('User role request', message); + const message = `The user ${email} has made a request to get the role : ${selectedRole}.`; + const result = await client.createUserRequest(message, sub); if (result.error) { toast.error( <ToastMessage title={t('validation:error')} message={result.error} /> @@ -74,7 +75,7 @@ const RoleSettings = ({ userRole }) => { /> </EuiFormRow> <EuiSpacer size={'l'} /> - <EuiButton onClick={() => onSendRoleRequest()} fill> + <EuiButton onClick={() => onSendRoleRequest()} fill disabled={!selectedRole}> {t('common:validationActions.send')} </EuiButton> </EuiPanel> diff --git a/src/pages/profile/UserFieldsDisplaySettings.js b/src/pages/profile/UserFieldsDisplaySettings.js index 2984427..e30e423 100644 --- a/src/pages/profile/UserFieldsDisplaySettings.js +++ b/src/pages/profile/UserFieldsDisplaySettings.js @@ -8,22 +8,21 @@ import { EuiPanel, EuiTitle, } from '@elastic/eui'; -import { - createUserFieldsDisplaySettings, - deleteUserFieldsDisplaySettings, - updateUserFieldsDisplaySettings, -} from '../../actions/user'; import { Trans, useTranslation } from 'react-i18next'; import { buildFieldName } from '../../Utils'; import BulletPointList from '../../components/BulletPointList/BulletPointList'; import { toast } from 'react-toastify'; import ToastMessage from '../../components/ToastMessage/ToastMessage'; +import { useGatekeeper } from '../../contexts/GatekeeperContext'; +import { useUserInfo } from '../../contexts/TokenContext'; /* User fields display settings are used to choose which fields are displayed in results table after a search. If the user has no settings, the default are used. Default settings are the same for all users, chosen by admin at standard setup. */ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields }) => { + const client = useGatekeeper(); + const getUserInfo = useUserInfo(); const { t } = useTranslation(['profile', 'common', 'validation']); const [settingsOptions, setSettingsOptions] = useState([]); const [selectedOptionsIds, setSelectedOptionsIds] = useState([]); @@ -88,21 +87,12 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields return; } let result; - if (userSettings) { - result = await updateUserFieldsDisplaySettings( - sessionStorage.getItem('userId'), - selectedOptionsIds - ); - } else { - result = await createUserFieldsDisplaySettings( - sessionStorage.getItem('userId'), - selectedOptionsIds - ); - } - setUserSettings(selectedOptionsIds); - if (result.error) { + const { sub } = await getUserInfo(); + const response = await client.setUserFieldsDisplaySettings(sub, selectedOptionsIds); + if (response && response.length == 0) { toast.error(<ToastMessage title={t('validation:error')} message={result.error} />); } else { + setUserSettings(selectedOptionsIds); toast.success(t('profile:fieldsDisplaySettings.updatedSettingsSuccess')); } }; @@ -126,21 +116,16 @@ const UserFieldsDisplaySettings = ({ userSettings, setUserSettings, publicFields // With current logic, no user settings means it should use default. // So in this case 'reset' means delete current user settings. const onSettingsReset = async () => { - // TODO add a confirmation modal ? - const result = await deleteUserFieldsDisplaySettings( - sessionStorage.getItem('userId') - ); - if (result.error) { - toast.error(<ToastMessage title={t('validation:error')} message={result.error} />); - } else { - setUserSettings(null); - toast.success( - <ToastMessage - title={t('profile:fieldsDisplaySettings.deleteSettingsSuccess')} - message={t('profile:fieldsDisplaySettings.deleteSettingsSuccessDefault')} - /> - ); + if (!settingsOptions) { + toast.error(t('profile:fieldsDisplaySettings.selectionResetFailure')); + return; } + const newSettingsOptions = []; + settingsOptions.forEach((option) => { + option.checked = false; + newSettingsOptions.push(option); + }); + setSettingsOptions(newSettingsOptions); }; const SelectableSettingsPanel = () => { diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js index 0fd0c86..7586c59 100644 --- a/src/pages/results/ResultsTableMUI.js +++ b/src/pages/results/ResultsTableMUI.js @@ -1,11 +1,11 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import MUIDataTable from 'mui-datatables'; import { createTheme, ThemeProvider } from '@mui/material'; -import { makeStyles } from '@mui/styles'; -import { fetchPublicFields } from '../../actions/source'; -import { fetchUserFieldsDisplaySettings } from '../../actions/user'; import { buildFieldName } from '../../Utils'; +import { useGatekeeper } from '../../contexts/GatekeeperContext'; +import { useUserInfo } from '../../contexts/TokenContext'; +import { makeStyles } from '@mui/styles'; import { useEuiTheme, transparentize, EuiTitle } from '@elastic/eui'; const useStyles = makeStyles(() => ({ @@ -27,52 +27,55 @@ const ResultsTableMUI = ({ setResourceFlyoutDataFromId, }) => { const { t } = useTranslation('results'); + const client = useGatekeeper(); + const getUserInfo = useUserInfo(); const { euiTheme } = useEuiTheme(); const classes = useStyles(); const [publicFields, setPublicFields] = useState([]); - const [userFieldsIds, setUserFieldsIds] = useState([]); const [isLoading, setIsLoading] = useState(true); const [rowsPerPage, setRowsPerPage] = useState(15); + const [rows, setRows] = useState([]); + const [columns, setColumns] = useState([]); - // Fetch public fields and user display settings useEffect(() => { - const fetchData = async () => { - setIsLoading(true); - const publicFieldsResults = await fetchPublicFields(); - setPublicFields(publicFieldsResults); - const userStdFieldsIds = await fetchUserFieldsDisplaySettings( - sessionStorage.getItem('userId') - ); - const defaultStdFieldsIds = [1, 20, 36, 9, 31, 13]; - // TODO replace hard-coded array by gatekeeper fetch on default settings - // If no userStdFields, use system default ones. - setUserFieldsIds(userStdFieldsIds || defaultStdFieldsIds); + const fetchFieldsForUser = async () => { + const publicFields = await client.getPublicFields(); // TODO: get also private fields that user has access to + setPublicFields(publicFields); + }; + const getUserFieldsDisplaySettings = async () => { + const userInfo = await getUserInfo(); + const userFields = await client.getUserFieldsDisplaySettings(userInfo.sub); + if (userFields && userFields.length > 0) { + const columns = buildColumns(userFields); + setColumns(columns); + const rows = buildRows(searchResults, columns); + setRows(rows); + setIsLoading(false); + } }; - fetchData(); + if (!searchResults || searchResults.length === 0 || searchResults.error) { + return; + } + fetchFieldsForUser(); + getUserFieldsDisplaySettings(); }, [searchResults]); - // Memoize columns - const columns = useMemo(() => { - if (publicFields.length === 0) { - return []; - } - let dataColumns = [ + const buildColumns = (userFields) => { + let columns = [ { name: 'id', label: 'ID', options: { display: 'excluded' }, }, ]; - publicFields.forEach((publicField) => { - dataColumns.push({ - name: publicField.field_name, - label: buildFieldName(publicField.field_name), + // Add to columns the standard fields given as parameter + // Retrieve + userFields.forEach((field) => { + columns.push({ + name: field.field_name, + label: buildFieldName(field.field_name), options: { - display: userFieldsIds.includes(publicField.id), - // Apply styling on columns headers - setCellHeaderProps: () => ({ - className: classes.centeredHeader, - }), + display: true, // Apply styling on columns headers text customHeadLabelRender: (columnMeta) => { return ( @@ -83,7 +86,7 @@ const ResultsTableMUI = ({ }, // Truncate text to avoid oversize rows customBodyRenderLite: (dataIndex, rowIndex) => { - let value = rows[rowIndex][publicField.field_name]; + let value = rows[rowIndex][field.field_name]; if (value && value.length >= 150) { value = value.substring(0, 150) + ' ...'; } @@ -93,15 +96,26 @@ const ResultsTableMUI = ({ setCellProps: () => ({ style: { maxWidth: '350px', + textOverflow: 'ellipsis', }, }), }, }); }); - // sort columns alphabetically for clarity - dataColumns.sort((a, b) => (a.name > b.name ? 1 : -1)); - return dataColumns; - }, [publicFields, userFieldsIds]); + columns.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); + return columns; + }; + + const buildRows = (results, columns) => { + return results.map((result) => { + let row = { id: result.id }; + columns.forEach((column) => { + const value = getValueByPath(result, column.name); + row[column.name] = typeof value === 'string' ? value : value?.toString(); + }); + return row; + }); + }; // Returns value from JSON obj associated to key string. const getValueByPath = (obj, path) => { @@ -115,49 +129,9 @@ const ResultsTableMUI = ({ }, obj); }; - const rows = useMemo(() => { - const buildRows = (results, columns) => { - if (results.length === 0) { - return []; - } - return results.map((result) => { - let row = { id: result.id }; - columns.forEach((column) => { - const value = getValueByPath(result, column.name); - row[column.name] = typeof value === 'string' ? value : value?.toString(); - }); - return row; - }); - }; - const rows = buildRows(searchResults, columns); - setIsLoading(false); - return searchResults && columns.length > 0 ? rows : []; - }, [searchResults, columns]); - - const getRowIdFromResourceData = (id) => { - if (!rows || rows.length === 0) { - return -1; - } - for (let index = 0; index < rows.length; index++) { - if (rows[index].id === id) { - return index; - } - } - return -1; - }; - - // On page load, check table rows from selected resources from map - const selectedRows = useMemo(() => { - return selectedRowsIds.map((id) => getRowIdFromResourceData(id)); - }, [rows, selectedRowsIds]); - // Add row to list of selected on checkbox click - const onRowSelectionCallback = (selectedRow, allSelectedRows) => { - setSelectedRowsIds( - allSelectedRows.map((row) => { - return rows[row.dataIndex].id; - }) - ); + const onRowSelectionCallback = (currentRowsSelected, allRowsSelected, rowsSelected) => { + setSelectedRowsIds(rowsSelected.map((index) => rows[index].id)); }; // Open resource flyout on row click (any cell) @@ -242,7 +216,9 @@ const ResultsTableMUI = ({ }, selectableRows: 'multiple', selectableRowsOnClick: false, - rowsSelected: selectedRows, + rowsSelected: rows + .filter((row) => selectedRowsIds.includes(row.id)) + .map((row) => rows.indexOf(row)), rowsPerPage: rowsPerPage, onChangeRowsPerPage: (newRowsPerPage) => { setRowsPerPage(newRowsPerPage); @@ -250,7 +226,7 @@ const ResultsTableMUI = ({ rowsPerPageOptions: [15, 30, 50, 100, 250], jumpToPage: true, searchPlaceholder: t('results:table.search'), - elevation: 0, // remove the boxShadow style + elevation: 0, customToolbarSelect: () => <CustomSelectToolbar />, selectToolbarPlacement: 'above', onRowSelectionChange: onRowSelectionCallback, diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js index 53f5ad4..29625ef 100644 --- a/src/pages/search/AdvancedSearch/AdvancedSearch.js +++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js @@ -32,7 +32,6 @@ import { import React, { Fragment, useEffect, useState } from 'react'; import { changeNameToLabel, - createAdvancedQueriesBySource, getFieldsBySection, getSections, removeArrayElement, @@ -40,15 +39,16 @@ import { updateArrayElement, updateSearchFieldValues, } from '../../../Utils'; -import { getQueryCount, searchQuery } from '../../../actions/source'; import { DateOptions, NumericOptions, Operators } from '../Data'; -import { addUserHistory, fetchUserHistory } from '../../../actions/user'; import { useTranslation } from 'react-i18next'; import styles from './styles.js'; import moment from 'moment'; import SearchModeSwitcher from '../SearchModeSwitcher'; import { toast } from 'react-toastify'; import ToastMessage from '../../../components/ToastMessage/ToastMessage'; +import { useGatekeeper } from '../../../contexts/GatekeeperContext'; +import { useAuth } from 'oidc-react'; +import { buildDslQuery } from '../../../Utils'; const updateSources = ( searchFields, @@ -69,7 +69,7 @@ const updateSources = ( availableSources = updatedSources; } updatedSources = []; - field.sources.forEach((sourceId) => { + field.sources?.forEach((sourceId) => { noPrivateField = false; const source = availableSources.find((src) => src.id === sourceId); if (source && !updatedSources.includes(source)) { @@ -130,18 +130,6 @@ const fieldValuesToString = (field) => { return strValues; }; -const fetchHistory = (setUserHistory) => { - fetchUserHistory(sessionStorage.getItem('kcId')).then((result) => { - if (result[0] && result[0].ui_structure) { - result.forEach((item) => { - item.ui_structure = JSON.parse(item.ui_structure); - item.label = `${item.name} - ${new Date(item.createdat).toLocaleString()}`; - }); - } - setUserHistory(result); - }); -}; - const updateSearch = (setSearch, searchFields, selectedOperatorId, setSearchCount) => { let searchText = ''; searchFields.forEach((field) => { @@ -172,12 +160,27 @@ const HistorySelect = ({ setUserHistory, }) => { const { t } = useTranslation('search'); + const auth = useAuth(); + const client = useGatekeeper(); const [historySelectError, setHistorySelectError] = useState(undefined); const [selectedSavedSearch, setSelectedSavedSearch] = useState(undefined); useEffect(() => { - fetchHistory(setUserHistory); - }, [setUserHistory]); + const fetchHistory = async (sub) => { + const userHistory = await client.getUserHistory(sub); + if (userHistory[0] && userHistory[0].ui_structure) { + userHistory.forEach((item) => { + item.ui_structure = JSON.parse(item.ui_structure); + item.label = `${item.name} - ${new Date(item.createdat).toLocaleString()}`; + }); + setUserHistory(userHistory); + } + }; + const sub = auth.userData?.profile?.sub; + if (sub) { + fetchHistory(sub); + } + }, [auth.userData]); const onHistoryChange = (selectedSavedSearch) => { setHistorySelectError(undefined); @@ -262,69 +265,82 @@ const SearchBar = ({ const [userHistory, setUserHistory] = useState({}); const [isSaveSearchModalOpen, setIsSaveSearchModalOpen] = useState(false); const [readOnlyQuery, setReadOnlyQuery] = useState(true); + const auth = useAuth(); + const client = useGatekeeper(); const closeSaveSearchModal = () => { setIsSaveSearchModalOpen(false); }; - const onClickAdvancedSearch = () => { + const onClickAdvancedSearch = async () => { if (search.trim()) { setIsLoading(true); - const queriesWithIndices = createAdvancedQueriesBySource( - standardFields, - search, - selectedSources, - availableSources - ); - searchQuery(queriesWithIndices).then((result) => { - setSearchResults(result); - setSelectedTabNumber(1); - if (isLoading) { - setIsLoading(false); - } - }); + const advancedQuery = buildDslQuery(search, standardFields); + const params = { + query: advancedQuery, + sourcesId: availableSources.map((source) => source.id), + fieldsId: standardFields, + advancedQuery: true, + }; + const result = await client.searchQuery(params); + setSearchResults(result); + setSelectedTabNumber(1); + if (isLoading) { + setIsLoading(false); + } } }; - const onClickCountResults = () => { + const onClickCountResults = async () => { if (!!search) { - const queriesWithIndices = createAdvancedQueriesBySource( - standardFields, - search, - selectedSources, - availableSources - ); - getQueryCount(queriesWithIndices).then((result) => { - if (result || result === 0) { - setSearchCount(result); - } - }); + const query = buildDslQuery(search, standardFields); + const params = { + query, + sourcesId: availableSources.map((source) => source.id), + fieldsId: standardFields, + advancedQuery: true, + }; + const result = await client.searchQuery(params); + if (!result || result.error) { + toast.error( + <ToastMessage title={t('validation:error')} message={result.error} /> + ); + setSearchCount(0); + } + setSearchCount(result.length); } }; - const addHistory = ( + const addHistory = async ( search, searchName, searchFields, searchDescription, setUserHistory ) => { - addUserHistory( - sessionStorage.getItem('kcId'), - search, - searchName, - searchFields, - searchDescription - ).then((result) => { - if (result.error) { - toast.error( - <ToastMessage title={t('validation:error')} message={result.error} /> - ); - } else { - toast.success(t('search:advancedSearch.searchHistory.searchSaved')); + const sub = auth.userData?.profile?.sub; + if (!sub) { + return; + } + const params = { + name: searchName, + query: search, + ui_structure: JSON.stringify(searchFields), + description: searchDescription, + }; + const result = await client.addHistory(sub, params); + if (result.error) { + toast.error(<ToastMessage title={t('validation:error')} message={result.error} />); + } else { + const userHistory = await client.getUserHistory(sub); + if (userHistory[0] && userHistory[0].ui_structure) { + userHistory.forEach((item) => { + item.ui_structure = JSON.parse(item.ui_structure); + item.label = `${item.name} - ${new Date(item.createdat).toLocaleString()}`; + }); + setUserHistory(userHistory); } - fetchHistory(setUserHistory); - }); + } }; const SaveSearchModal = () => { @@ -544,26 +560,9 @@ const PopoverSelect = ({ standardFields, searchFields, setSearchFields }) => { } }; - const SelectField = () => { - const renderOption = (option, searchValue, contentClassName) => { - const { label, color } = option; - return <EuiHealth color={color}>{label}</EuiHealth>; - }; - if (selectedSection.length) { - return ( - <> - <EuiComboBox - placeholder={t('search:advancedSearch.fields.addFieldPopover.selectSection')} - singleSelection={{ asPlainText: true }} - options={getFieldsBySection(standardFields, selectedSection[0])} - selectedOptions={selectedField} - onChange={(selected) => setSelectedField(selected)} - isClearable={true} - renderOption={renderOption} - /> - </> - ); - } + const renderOption = (option, searchValue, contentClassName) => { + const { label, color } = option; + return <EuiHealth color={color}>{label}</EuiHealth>; }; return ( @@ -596,7 +595,18 @@ const PopoverSelect = ({ standardFields, searchFields, setSearchFields }) => { }} isClearable={false} /> - <SelectField /> + <EuiComboBox + placeholder={t('search:advancedSearch.fields.addFieldPopover.selectSection')} + singleSelection={{ asPlainText: true }} + options={getFieldsBySection(standardFields, selectedSection[0])} + selectedOptions={selectedField} + onChange={(selected) => { + setSelectedField(selected); + }} + isClearable={true} + renderOption={renderOption} + isDisabled={selectedSection.length === 0} + /> </EuiFlexGroup> <EuiPopoverFooter style={styles.noBorder} paddingSize={'m'}> <EuiButton @@ -683,7 +693,7 @@ const PopoverValueContent = ({ setSearchFields(updatedSearchFields); updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); setFieldCount(updateArrayElement(fieldCount, index)); - if (searchFields[index].sources.length) { + if (searchFields[index].sources?.length) { const filteredSources = []; searchFields[index].sources.forEach((sourceId) => { let source; @@ -1098,13 +1108,19 @@ const PopoverValueButton = ({ const { t } = useTranslation('search'); const [isPopoverValueOpen, setIsPopoverValueOpen] = useState(false); + const handleButtonClick = (e) => { + e.preventDefault(); + e.stopPropagation(); + setIsPopoverValueOpen((prevState) => !prevState); + }; + return ( <EuiPopover panelPaddingSize="s" button={ <EuiButtonIcon size="s" - onClick={() => setIsPopoverValueOpen(!isPopoverValueOpen)} + onClick={handleButtonClick} iconType="documentEdit" title={t('search:advancedSearch.fields.fieldContentPopover.addFieldValues')} aria-label={t( @@ -1154,19 +1170,20 @@ const FieldsPanel = ({ sources, }) => { const { t } = useTranslation('search'); + const client = useGatekeeper(); - const countFieldValues = (field, index) => { + const countFieldValues = async (field, index) => { const fieldStr = `{${fieldValuesToString(field)}}`; - const queriesWithIndices = createAdvancedQueriesBySource( - standardFields, - fieldStr, - selectedSources, - availableSources - ); - getQueryCount(queriesWithIndices).then((result) => { - if (result || result === 0) - setFieldCount(updateArrayElement(fieldCount, index, result)); - }); + const advancedQuery = buildDslQuery(fieldStr, standardFields); + const params = { + query: advancedQuery, + sourcesId: availableSources.map((source) => source.id), + fieldsId: standardFields, + advancedQuery: true, + }; + const result = await client.searchQuery(params); + const fieldCountUpdated = updateArrayElement(fieldCount, index, result.length); + setFieldCount(fieldCountUpdated); }; const handleRemoveField = (index) => { @@ -1224,7 +1241,7 @@ const FieldsPanel = ({ updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); }; - if (standardFields === []) { + if (standardFields == []) { return <h2>{t('search:advancedSearch.fields.loadingFields')}</h2>; } @@ -1256,7 +1273,7 @@ const FieldsPanel = ({ <EuiFlexItem> {field.isValidated ? ( <> - {field.sources.length ? ( + {field.sources?.length ? ( <EuiHealth color="danger"> {fieldValuesToString(field).replace(/_|\./g, ' ')} </EuiHealth> @@ -1268,7 +1285,7 @@ const FieldsPanel = ({ </> ) : ( <> - {field.sources.length ? ( + {field.sources?.length ? ( <EuiHealth color="danger"> {field.name.replace(/_|\./g, ' ')} </EuiHealth> diff --git a/src/pages/search/BasicSearch/BasicSearch.js b/src/pages/search/BasicSearch/BasicSearch.js index dfdd1f1..77f208a 100644 --- a/src/pages/search/BasicSearch/BasicSearch.js +++ b/src/pages/search/BasicSearch/BasicSearch.js @@ -6,15 +6,13 @@ import { EuiFlexItem, EuiSpacer, } from '@elastic/eui'; -import { createBasicQueriesBySource } from '../../../Utils'; -import { searchQuery } from '../../../actions/source'; import { useTranslation } from 'react-i18next'; import SearchModeSwitcher from '../SearchModeSwitcher'; +import { useGatekeeper } from '../../../contexts/GatekeeperContext'; const BasicSearch = ({ standardFields, availableSources, - selectedSources, basicSearch, setBasicSearch, setIsAdvancedSearch, @@ -23,24 +21,20 @@ const BasicSearch = ({ setSelectedTabNumber, }) => { const { t } = useTranslation('search'); + const client = useGatekeeper(); const [isLoading, setIsLoading] = useState(false); - const onFormSubmit = (e) => { + const onFormSubmit = async (e) => { e.preventDefault(); setIsLoading(true); - const queriesWithIndices = createBasicQueriesBySource( - standardFields, - basicSearch, - selectedSources, - availableSources - ); - searchQuery(queriesWithIndices).then((result) => { - setSearchResults(result); - if (isLoading) { - setIsLoading(false); - } - setSelectedTabNumber(1); + const result = await client.searchQuery({ + query: basicSearch, + sourcesId: availableSources.map((source) => source.id), + fieldsId: standardFields, }); + setIsLoading(false); + setSearchResults(result); + setSelectedTabNumber(1); }; return ( diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index 6f31278..2afd2c3 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -2,18 +2,14 @@ import React, { useState, useEffect } from 'react'; import { EuiTabbedContent, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import Results from '../results/Results'; import SearchMap from '../maps/SearchMap'; -import { removeNullFields } from '../../Utils.js'; -import { - fetchPublicFields, - fetchUserPolicyFields, - fetchSources, -} from '../../actions/source'; import { useTranslation } from 'react-i18next'; import AdvancedSearch from './AdvancedSearch/AdvancedSearch'; import BasicSearch from './BasicSearch/BasicSearch'; +import { useGatekeeper } from '../../contexts/GatekeeperContext'; import ResourceFlyout from '../results/ResourceFlyout/ResourceFlyout'; const Search = () => { + const client = useGatekeeper(); const { t } = useTranslation('search'); const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [isAdvancedSearch, setIsAdvancedSearch] = useState(false); @@ -29,37 +25,59 @@ const Search = () => { const [resourceFlyoutData, setResourceFlyoutData] = useState({}); useEffect(() => { - fetchPublicFields().then((resultStdFields) => { - resultStdFields.forEach((field) => { - field.sources = []; - }); - setStandardFields(resultStdFields); - fetchUserPolicyFields(sessionStorage.getItem('kcId')).then((resultPolicyFields) => { - const userFields = resultStdFields; - resultPolicyFields.forEach((polField) => { - const stdFieldIndex = userFields.findIndex( - (stdField) => stdField.id === polField.std_id - ); - if (stdFieldIndex >= 0) { - if (!userFields[stdFieldIndex].sources.includes(polField.source_id)) - userFields[stdFieldIndex].sources.push(polField.source_id); - } else { - const newField = { - id: polField.std_id, - sources: [polField.source_id], - ...polField, - }; - userFields.push(newField); - } - }); - userFields.sort((a, b) => (a.id > b.id ? 1 : b.id > a.id ? -1 : 0)); - setStandardFields(removeNullFields(userFields)); - }); - }); - fetchSources(sessionStorage.getItem('kcId')).then((result) => { - setSources(result); - setAvailableSources(result); - }); + if (!client) { + return; + } + const fetchElements = async () => { + const publicFields = await client.getPublicFields(); + setStandardFields(publicFields); + const sources = await client.getSources(); + setSources(sources); + setAvailableSources(sources); + }; + fetchElements(); + // client + // .getPublicFields() + // .then((resultStdFields) => { + // resultStdFields.forEach((field) => { + // field.sources = []; + // }); + // setStandardFields(resultStdFields); + // fetchUserPolicyFields(sessionStorage.getItem('kcId')).then( + // (resultPolicyFields) => { + // const userFields = resultStdFields; + // resultPolicyFields.forEach((polField) => { + // const stdFieldIndex = userFields.findIndex( + // (stdField) => stdField.id === polField.std_id + // ); + // if (stdFieldIndex >= 0) { + // if (!userFields[stdFieldIndex].sources.includes(polField.source_id)) + // userFields[stdFieldIndex].sources.push(polField.source_id); + // } else { + // const newField = { + // id: polField.std_id, + // sources: [polField.source_id], + // ...polField, + // }; + // userFields.push(newField); + // } + // }); + // userFields.sort((a, b) => (a.id > b.id ? 1 : b.id > a.id ? -1 : 0)); + // setStandardFields(removeNullFields(userFields)); + // } + // ); + // }) + // .catch((error) => { + // console.error(error); + // }); + // fetchSources(sessionStorage.getItem('kcId')) + // .then((result) => { + // setSources(result); + // setAvailableSources(result); + // }) + // .catch((error) => { + // console.error(error); + // }); }, []); // On new search, reset selected rows @@ -109,7 +127,6 @@ const Search = () => { setIsAdvancedSearch={setIsAdvancedSearch} standardFields={standardFields} availableSources={availableSources} - selectedSources={selectedSources} basicSearch={basicSearch} setBasicSearch={setBasicSearch} setSearchResults={setSearchResults} diff --git a/src/services/GatekeeperService.js b/src/services/GatekeeperService.js new file mode 100644 index 0000000..498864a --- /dev/null +++ b/src/services/GatekeeperService.js @@ -0,0 +1,148 @@ +export class InSylvaGatekeeperClient { + constructor(getUserInfo) { + this.getUserInfo = getUserInfo; + } + async get(path, payload) { + const userInfo = await this.getUserInfo(); + const { access_token } = userInfo; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${access_token}`, + }; + const response = await fetch( + `${process.env.REACT_APP_IN_SYLVA_GATEKEEPER_BASE_URL}${path}`, + { + method: 'GET', + headers, + mode: 'cors', + body: JSON.stringify(payload), + } + ); + return await response.json(); + } + + async post(path, requestContent) { + const userInfo = await this.getUserInfo(); + const { access_token } = userInfo; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${access_token}`, + }; + const response = await fetch( + `${process.env.REACT_APP_IN_SYLVA_GATEKEEPER_BASE_URL}${path}`, + { + method: 'POST', + headers, + body: JSON.stringify(requestContent), + mode: 'cors', + } + ); + return await response.json(); + } + + async delete(path, requestContent) { + const userInfo = await this.getUserInfo(); + const { access_token } = userInfo; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${access_token}`, + }; + const response = await fetch( + `${process.env.REACT_APP_IN_SYLVA_GATEKEEPER_BASE_URL}${path}`, + { + method: 'DELETE', + headers, + body: JSON.stringify(requestContent), + mode: 'cors', + } + ); + if (response.status === 204) { + return { + success: true, + message: 'Request deleted successfully', + }; + } else { + return { + success: false, + message: 'Request not deleted', + }; + } + } + + async createUser(sub, email) { + const path = `/users`; + return await this.post(path, { + kc_id: sub, + email, + }); + } + + async findUserBySub(sub) { + const path = `/users/${sub}`; + return await this.get(path); + } + + async getRoles() { + const path = `/roles`; + return await this.get(path); + } + + async createUserRequest(message, sub) { + const path = `/users/${sub}/requests`; + return await this.post(path, { + message: message, + }); + } + + async deleteUserRequest(id) { + const path = `/user-requests/${id}`; + return await this.delete(path, {}); + } + + async getGroups() { + const path = `/groups`; + return await this.get(path); + } + + async getPublicFields() { + const path = `/public_std_fields`; + return await this.get(path); + } + + async getUserFieldsDisplaySettings(sub) { + const path = `/users/${sub}/fields`; + return await this.get(path); + } + + async setUserFieldsDisplaySettings(sub, fields) { + const path = `/users/${sub}/fields`; + return await this.post(path, { + fields_id: fields, + }); + } + + async getSources() { + const path = `/sources`; + return await this.get(path); + } + + async getUserSources(sub) { + const path = `/users/${sub}/sources`; + return await this.get(path); + } + + async searchQuery(payload) { + const path = `/search`; + return await this.post(path, payload); + } + + async getUserHistory(sub) { + const path = `/users/${sub}/history`; + return await this.get(path); + } + + async addHistory(sub, payload) { + const path = `/users/${sub}/history`; + return await this.post(path, payload); + } +} diff --git a/src/services/TokenService.js b/src/services/TokenService.js new file mode 100644 index 0000000..4ead043 --- /dev/null +++ b/src/services/TokenService.js @@ -0,0 +1,38 @@ +export default class TokenService { + async getUserInfo(access_token) { + const issuerUrl = process.env.REACT_APP_KEYCLOAK_BASE_URL; + const userInfoEndpoint = issuerUrl + '/protocol/openid-connect/userinfo'; + + const response = await fetch(userInfoEndpoint, { + headers: { + Authorization: `Bearer ${access_token}`, + }, + }); + if (response.status !== 200) { + return null; + } else { + const userInfo = await response.json(); + return userInfo; + } + } + + async refreshToken(refresh_token) { + const issuerUrl = process.env.REACT_APP_KEYCLOAK_BASE_URL; + const tokenEndpoint = issuerUrl + '/protocol/openid-connect/token'; + + const response = await fetch(tokenEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `grant_type=refresh_token&refresh_token=${refresh_token}&client_id=${process.env.REACT_APP_KEYCLOAK_CLIENT_ID}&client_secret=${process.env.REACT_APP_KEYCLOAK_CLIENT_SECRET}`, + }); + + if (response.status !== 200) { + return null; + } else { + const response = await response.json(); + return response; + } + } +} -- GitLab