diff --git a/package.json b/package.json index c706491433ac7e31c08ae506d715918096870261..4faa70aceb1fb67cc7f91d86150fb56cde7f90c2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@material-ui/lab": "^4.0.0-alpha.48", "@material-ui/styles": "^4.10.0", "downloadjs": "^1.4.7", + "i18next": "^23.11.2", + "i18next-http-backend": "^2.5.1", "moment": "^2.27.0", "mui-datatables": "^3.4.0", "ol": "^6.3.2-dev.1594217558556", @@ -20,6 +22,7 @@ "react-dom": "^16.13.1", "react-hanger": "^2.2.1", "react-html-parser": "^2.0.2", + "react-i18next": "^14.1.1", "react-router-dom": "^5.2.0", "react-scripts": "^3.3.0" }, @@ -45,12 +48,12 @@ ] }, "devDependencies": { - "jscs": "^3.0.7", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", "husky": "8.0.3", + "jscs": "^3.0.7", "lint-staged": "14.0.1", - "prettier": "^3.2.5", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.1.3" + "prettier": "^3.2.5" }, "husky": { "hooks": { diff --git a/public/index.html b/public/index.html index 38223fe591121af96fe75b88c0b43242e7ad2dd6..5e6c56481c2b2e5db90e8168d6f8261aab9e9f72 100644 --- a/public/index.html +++ b/public/index.html @@ -1,17 +1,11 @@ <!DOCTYPE html> <html lang="en"> - <head> <meta charset="utf-8" /> - <!-- <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> --> + <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" /> - <!-- - manifest.json provides metadata used when your web app is installed on a - user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ - --> <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. @@ -21,23 +15,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> + <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> - <!-- - This HTML file is a template. - If you open it directly in the browser, you will see an empty page. - - You can add webfonts, meta tags, or analytics to this file. - The build step will place the bundled scripts into the <body> tag. - - To begin the development, run `npm start` or `yarn start`. - To create a production bundle, use `npm run build` or `yarn build`. - --> </body> - </html> diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000000000000000000000000000000000000..506293c381c1ba92b8c840aaea2d59b85ba3c993 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,13 @@ +{ + "languages": { + "en": "English", + "fr": "French" + }, + "inSylvaLogoAlt": "In-Sylva logo", + "validationActions": { + "cancel": "Cancel", + "send": "Send", + "save": "Save", + "validate": "Validate" + } +} diff --git a/public/locales/en/header.json b/public/locales/en/header.json new file mode 100644 index 0000000000000000000000000000000000000000..889a13d8020dc576da3ac10b65ed7e7c394204f6 --- /dev/null +++ b/public/locales/en/header.json @@ -0,0 +1,11 @@ +{ + "tabs": { + "home": "Home", + "search": "Search" + }, + "userMenu": { + "title": "User profile", + "editProfileButton": "Edit profile", + "logOutButton": "Log out" + } +} diff --git a/public/locales/en/home.json b/public/locales/en/home.json new file mode 100644 index 0000000000000000000000000000000000000000..d1038e005f2484a8a1e436071e369bfadee70574 --- /dev/null +++ b/public/locales/en/home.json @@ -0,0 +1,11 @@ +{ + "pageTitle": "Welcome on In-Sylva search module's homepage.", + "searchToolDescription": { + "part1": "As a reminder, it should be remembered that the metadata stored in IN-SYLVA IS are structured around the IN-SYLVA standard.", + "part2": "This standard is composed of metadata fields. A metadata record is therefore made up of a series of fields accompanied by their value.", + "part3": "With this part of the interface you will be able to search for metadata records (previously loaded via the portal), by defining a certain number of criteria.", + "part4": "By default the \"search\" interface opens to a \"plain text\" search, ie the records returned in the result are those which, in one of the field values, contains the supplied character string.", + "part5": "A click on the Advanced search button gives access to a more complete form via which you can do more precise searches on one or more targeted fields.", + "part6": "Click on the \"Search\" tab to access the search interface." + } +} diff --git a/public/locales/en/maps.json b/public/locales/en/maps.json new file mode 100644 index 0000000000000000000000000000000000000000..ca5269d597d32187d9878e055f1cdae7fe0e3002 --- /dev/null +++ b/public/locales/en/maps.json @@ -0,0 +1,23 @@ +{ + "layersChoiceTitle": "Click on layers to toggle display.", + "layersTableHeaders": { + "cartography": "Cartography", + "filters": "Filters", + "tools": "Tools" + }, + "layersTable": { + "openStreetMap": "Open Street Map", + "bingAerial": "Bing Aerial", + "IGN": "IGN map", + "SylvoEcoRegions": "SylvoEcoRegions", + "queryResults": "Query results", + "regions": "Regions", + "departments": "Departments", + "selectFilterOption": "Select a single option", + "selectedPointsList": "Selected resources list", + "pointSelectionMode": { + "select": "Select", + "unselect": "Unselect" + } + } +} diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json new file mode 100644 index 0000000000000000000000000000000000000000..76ab2db9f4c52d0a9f25098cf4c7cc302f038eac --- /dev/null +++ b/public/locales/en/profile.json @@ -0,0 +1,23 @@ +{ + "pageTitle": "Profile management", + "groups": { + "groupsList": "Group list", + "groupName": "Name", + "groupDescription": "Description" + }, + "requestsList": { + "requestsList": "Requests list", + "requestsMessage": "Message", + "processed": "Processed", + "cancelRequest": "Cancel this request" + }, + "groupRequests": { + "requestGroupAssignment": "Request a group assignment", + "currentGroups": "You currently belong to (or have a pending request for) these groups:", + "noGroup": "You currently don't belong to any group." + }, + "roleRequests": { + "requestRoleAssignment": "Request an application role", + "currentRole": "You currently have (or have a pending request for) this role:" + } +} diff --git a/public/locales/en/results.json b/public/locales/en/results.json new file mode 100644 index 0000000000000000000000000000000000000000..61cf048cef51645529f6353f5e08fde90b74541a --- /dev/null +++ b/public/locales/en/results.json @@ -0,0 +1,10 @@ +{ + "yourQuery": "Your query: {{query}}", + "clickOnRowTip": "Click on a resource row to display full metadata.", + "downloadResultsButton": { + "JSON": "Download as JSON" + }, + "table": { + "title": "Search results" + } +} diff --git a/public/locales/en/search.json b/public/locales/en/search.json new file mode 100644 index 0000000000000000000000000000000000000000..cebc9ebd923a6e84a544c21650da1a833a2c1bea --- /dev/null +++ b/public/locales/en/search.json @@ -0,0 +1,78 @@ +{ + "pageTitle": "In-Sylva Metadata Search Platform", + "tabs": { + "composeSearch": "Compose search", + "results": "Results", + "map": "Map" + }, + "sendSearchButton": "Search", + "basicSearch": { + "switchSearchMode": "Switch to advanced search", + "searchInputPlaceholder": "Search..." + }, + "advancedSearch": { + "switchSearchMode": "Switch to basic search", + "textQueryPlaceholder": "Add fields...", + "countResultsButton": "Count results", + "resultsCount_one": "{{count}} result", + "resultsCount_other": "{{count}} results", + "editableSearchButton": "Editable", + "errorInvalidOption": "\"{{value}}\" is not a valid option.", + "fields": { + "title": "Field search", + "loadingFields": "Loading fields...", + "removeFieldButton": "Remove field", + "clearValues": "Clear values", + "addFieldPopover": { + "openPopoverButton": "Add field", + "title": "Select a field", + "button": "Add this field", + "selectSection": "Select a section" + }, + "fieldContentPopover": { + "addFieldValues": "Add field values", + "addValue": "Add value", + "firstValue": "1st value", + "secondValue": "2nd value", + "inputTextValue": "Type value", + "betweenDate": "between", + "andDate": "and", + "selectValues": "Select values" + } + }, + "searchHistory": { + "placeholder": "Load a previous request", + "saveSearch": "Save search", + "addSavedSearchName": "Search name", + "addSavedSearchDescription": "Description (optional)", + "addSavedSearchDescriptionPlaceholder": "Search description..." + }, + "searchOptions": { + "title": "Search option", + "matchAll": "Match all criterias", + "matchAtLeastOne": "Match at least one criteria" + }, + "partnerSources": { + "title": "Partner sources", + "allSourcesSelected": "By default, all sources are selected", + "noSourceAvailable": "No source available." + }, + "policyToast": { + "title": "Private field selected", + "content": [ + "You selected a private field.", + "Access to this field was granted for specific sources, which means that your search will be restricted to those.", + "Please check the sources list before searching." + ] + }, + "editableQueryToast": { + "title": "Proceed with caution", + "content": { + "part1": "Manually editing can spoil query results. Syntax must be respected:", + "part2": "Fields and their values should be put between brackets: { } - Make sure every opened bracket is closed", + "part3": "\"AND\" and \"OR\" should be capitalized between fields and lowercase within a field expression", + "part4": "Make sure to check for typing errors" + } + } + } +} diff --git a/public/locales/en/validation.json b/public/locales/en/validation.json new file mode 100644 index 0000000000000000000000000000000000000000..b12bd6a67aead265e9d0262c00b61d58d51137f7 --- /dev/null +++ b/public/locales/en/validation.json @@ -0,0 +1,3 @@ +{ + "requestSent": "Your request has been sent to the administrators." +} diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json new file mode 100644 index 0000000000000000000000000000000000000000..8e9085fbe434c4ceab454e939e90ab0139dbef0e --- /dev/null +++ b/public/locales/fr/common.json @@ -0,0 +1,13 @@ +{ + "languages": { + "en": "Anglais", + "fr": "Français" + }, + "inSylvaLogoAlt": "Logo In-Sylva", + "validationActions": { + "cancel": "Annuler", + "send": "Envoyer", + "save": "Sauvegarder", + "validate": "Valider" + } +} diff --git a/public/locales/fr/header.json b/public/locales/fr/header.json new file mode 100644 index 0000000000000000000000000000000000000000..a5e28a5e7bc627ee31d4145c909be2da802a29d4 --- /dev/null +++ b/public/locales/fr/header.json @@ -0,0 +1,11 @@ +{ + "tabs": { + "home": "Page d'accueil", + "search": "Recherche" + }, + "userMenu": { + "title": "Profil utilisateur", + "editProfileButton": "Modifier mon profil", + "logOutButton": "Déconnexion" + } +} diff --git a/public/locales/fr/home.json b/public/locales/fr/home.json new file mode 100644 index 0000000000000000000000000000000000000000..f9bdcd684df9cd4074239e84b369a5a97893ffcc --- /dev/null +++ b/public/locales/fr/home.json @@ -0,0 +1,11 @@ +{ + "pageTitle": "Bienvenue sur la page d'accueil du module de recherche du Système d'Information In-Sylva", + "searchToolDescription": { + "part1": "Il est important de rappeler que les métadonnées stockées dans le SI In-Sylva sont structurées autour du standard établi par In-Sylva.", + "part2": "Il est composé de champs de métadonnées. Une fiche de métadonnées est donc constituée d'une série de champs accompagnés de leur valeur.", + "part3": "Cette interface vous permettra de rechercher des fiches de métadonnées (chargées au préalable par le Portal), en définissant un certain nombre de critères.", + "part4": "L'interface \"Recherche\" ouvre une zone de texte de \"Recherche basique\". Les résultats correspondent aux fiches de métadonnées contenant, dans un de leurs champs, la chaîne de caractère renseignée.", + "part5": "Un click sur le bouton \"Recherche avancée\" vous permets d'accéder à un formulaire plus complet qui vous permettra des recherches plus précises sur un ou plusieurs champs donnés.", + "part6": "Clickez sur l'onglet \"Recherche\" pour accéder à l'interface de recherche." + } +} diff --git a/public/locales/fr/maps.json b/public/locales/fr/maps.json new file mode 100644 index 0000000000000000000000000000000000000000..c113605f1b4a4203a817549c7c0c6e5d1c65c33d --- /dev/null +++ b/public/locales/fr/maps.json @@ -0,0 +1,23 @@ +{ + "layersChoiceTitle": "Cliquez sur les couches pour modifier l'affichage.", + "layersTableHeaders": { + "cartography": "Cartographie", + "filters": "Filtres", + "tools": "Outils" + }, + "layersTable": { + "openStreetMap": "Open Street Map", + "bingAerial": "Bing vue aérienne", + "IGN": "Plan IGN", + "SylvoEcoRegions": "SylvoEcoRégions", + "queryResults": "Résultats de la requête", + "regions": "Régions", + "departments": "Départements", + "selectFilterOption": "Sélectionnez une option", + "selectedPointsList": "Liste des resources sélectionnées", + "pointSelectionMode": { + "select": "Sélection", + "unselect": "Désélection" + } + } +} diff --git a/public/locales/fr/profile.json b/public/locales/fr/profile.json new file mode 100644 index 0000000000000000000000000000000000000000..9f30ddb283a8147bd3dff379de9a808674709736 --- /dev/null +++ b/public/locales/fr/profile.json @@ -0,0 +1,23 @@ +{ + "pageTitle": "Gestion du profil", + "groups": { + "groupsList": "Liste des groupes", + "groupName": "Nom", + "groupDescription": "Description" + }, + "requestsList": { + "requestsList": "Liste des requêtes", + "requestsMessage": "Message", + "processed": "Traitée", + "cancelRequest": "Annuler cette requête" + }, + "groupRequests": { + "requestGroupAssignment": "Demander à faire parti d'un groupe", + "currentGroups": "Vous faites actuellement parti (ou avez une demande pour) de ces groupes :", + "noGroup": "Vous ne faites actuellement parti d'aucun groupe." + }, + "roleRequests": { + "requestRoleAssignment": "Demander un rôle", + "currentRole": "Votre rôle actuel (ou demande en cours):" + } +} diff --git a/public/locales/fr/results.json b/public/locales/fr/results.json new file mode 100644 index 0000000000000000000000000000000000000000..e611299744c96fb46abb224d811621c4df0bd5e8 --- /dev/null +++ b/public/locales/fr/results.json @@ -0,0 +1,10 @@ +{ + "yourQuery": "Votre requête : {{query}}", + "clickOnRowTip": "Clickez sur une ressource (ligne du tableau) pour afficher ses métadonnées.", + "downloadResultsButton": { + "JSON": "Télécharger en JSON" + }, + "table": { + "title": "Résultats de la recherche" + } +} diff --git a/public/locales/fr/search.json b/public/locales/fr/search.json new file mode 100644 index 0000000000000000000000000000000000000000..5f6ab3907ccd0c6176b0d8b81d03c3f1d808f28b --- /dev/null +++ b/public/locales/fr/search.json @@ -0,0 +1,78 @@ +{ + "pageTitle": "Plateforme de recherche de métadonnées In-Sylva", + "tabs": { + "composeSearch": "Composer une recherche", + "results": "Résultats", + "map": "Carte" + }, + "sendSearchButton": "Lancer la recherche", + "basicSearch": { + "switchSearchMode": "Passer en recherche avancée", + "searchInputPlaceholder": "Chercher..." + }, + "advancedSearch": { + "switchSearchMode": "Passer en recherche basique", + "textQueryPlaceholder": "Ajoutez des champs...", + "countResultsButton": "Compter les résultats", + "resultsCount_one": "{{count}} résultat", + "resultsCount_other": "{{count}} résultats", + "editableSearchButton": "Modifiable", + "errorInvalidOption": "\"{{value}}\" n'est pas une option valide.", + "fields": { + "title": "Recherche de champ", + "loadingFields": "Chargement des champs...", + "removeFieldButton": "Supprimer le champ", + "clearValues": "Vider les valeurs", + "addFieldPopover": { + "openPopoverButton": "Selectionnez un champ", + "title": "Ajouter ce champ", + "button": "Selectionnez une section", + "selectSection": "Ajouter un champ" + }, + "fieldContentPopover": { + "addValue": "Ajouter une valeur", + "addFieldValues": "Ajouter des valeurs de champ", + "firstValue": "1ère valeur", + "secondValue": "2ème valeur", + "inputTextValue": "Entrez une valeur", + "betweenDate": "entre", + "andDate": "et", + "selectValues": "Sélectionnez au moins une valeur" + } + }, + "searchHistory": { + "placeholder": "Charger une recherche précédente", + "saveSearch": "Sauvegarder ma recherche", + "addSavedSearchName": "Nom de la recherche", + "addSavedSearchDescription": "Description (optionel)", + "addSavedSearchDescriptionPlaceholder": "Description de la recherche..." + }, + "searchOptions": { + "title": "Option de recherche", + "matchAll": "Répondre à tous les critères", + "matchAtLeastOne": "Répondre à au moins un critère" + }, + "partnerSources": { + "title": "Liste des sources de partenaires", + "allSourcesSelected": "Toutes les sources sont sélectionnées par défaut", + "noSourceAvailable": "Pas de source disponible." + }, + "policyToast": { + "title": "Champ privé sélectionné", + "content": [ + "Vous avez sélectionné un champ privé.", + "L'accès à ce champ à été donné par certaines sources, ce qui veut dire que votre recherche va être limitée à celles-ci.", + "Veuillez prếter attention à la liste des sources avant de continuer." + ] + }, + "editableQueryToast": { + "title": "Procéder avec prudence", + "content": { + "part1": "En éditant manuellement la recherche, vous pouvez facilement gâcher les résultats. Veuillez respecter la syntaxe :", + "part2": "Les champs et leurs valeurs doivent être comprises entre accolade: { } - Bien fermer toute accolade ouverte", + "part3": "\"AND\" et \"OR\" en majuscule entre les champs et en minuscule à l'intérieur d'une valeur de champ", + "part4": "Attention à corriger vos fautes de frappe" + } + } + } +} diff --git a/public/locales/fr/validation.json b/public/locales/fr/validation.json new file mode 100644 index 0000000000000000000000000000000000000000..0f29592894aacffcd6d069a539ed50c510cc7661 --- /dev/null +++ b/public/locales/fr/validation.json @@ -0,0 +1,3 @@ +{ + "requestSent": "Votre requête à bien été envoyée." +} diff --git a/src/Map.svg b/src/assets/Map.svg similarity index 100% rename from src/Map.svg rename to src/assets/Map.svg diff --git a/src/favicon.svg b/src/assets/favicon.svg similarity index 100% rename from src/favicon.svg rename to src/assets/favicon.svg diff --git a/src/logo.svg b/src/assets/logo.svg similarity index 100% rename from src/logo.svg rename to src/assets/logo.svg diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index f1e74d049f358e01ef1b3056e39b97508da1a37c..d44347a8ca84f3c432f0bff0277ad98eaf33f542 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -7,49 +7,58 @@ import { EuiHeaderLinks, EuiHeaderLink, } from '@elastic/eui'; -import HeaderUserMenu from './header_user_menu'; +import HeaderUserMenu from './HeaderUserMenu'; import style from './styles'; -import logoInSylva from '../../favicon.svg'; +import logoInSylva from '../../assets/favicon.svg'; +import { useTranslation } from 'react-i18next'; +import LanguageSwitcher from '../LanguageSwitcher/LanguageSwitcher'; const structure = [ { id: 0, - label: 'Home', + label: 'home', href: '/app/home', icon: '', }, { id: 1, - label: 'Search', + label: 'search', href: '/app/search', icon: '', }, ]; const Header = () => { + const { t } = useTranslation(['header', 'common']); + return ( <> <EuiHeader> <EuiHeaderSection grow={true}> - <EuiHeaderSectionItem border="right"> + <EuiHeaderSectionItem> <img - style={style} + style={style.logo} src={logoInSylva} width="75" height="45" - alt="Logo INRAE" + alt={t('common:inSylvaLogoAlt')} /> </EuiHeaderSectionItem> - <EuiHeaderLinks border="right"> + <EuiHeaderLinks> {structure.map((link) => ( <EuiHeaderLink iconType="empty" key={link.id}> - <Link to={link.href}>{link.label}</Link> + <Link to={link.href}>{t(`tabs.${link.label}`)}</Link> </EuiHeaderLink> ))} </EuiHeaderLinks> </EuiHeaderSection> <EuiHeaderSection side="right"> - <EuiHeaderSectionItem>{HeaderUserMenu()}</EuiHeaderSectionItem> + <EuiHeaderSectionItem style={style.languageSwitcherItem} border={'none'}> + <LanguageSwitcher /> + </EuiHeaderSectionItem> + <EuiHeaderSectionItem style={style.userMenuItem} border={'none'}> + <HeaderUserMenu /> + </EuiHeaderSectionItem> </EuiHeaderSection> </EuiHeader> </> diff --git a/src/components/Header/HeaderUserMenu.js b/src/components/Header/HeaderUserMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..2235573629ac2c2fa85fe3614f5e5b5dca6a43df --- /dev/null +++ b/src/components/Header/HeaderUserMenu.js @@ -0,0 +1,94 @@ +import React, { useEffect, useState } from 'react'; +import { + EuiAvatar, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiText, + EuiSpacer, + EuiPopover, + EuiButtonIcon, +} from '@elastic/eui'; +import { signOut } from '../../context/UserContext'; +import { findOneUser } from '../../actions/user'; +import { useTranslation } from 'react-i18next'; + +const HeaderUserMenu = () => { + const { t } = useTranslation('header'); + const [isOpen, setIsOpen] = useState(false); + const [user, setUser] = useState({}); + + const onMenuButtonClick = () => { + setIsOpen(!isOpen); + }; + + const closeMenu = () => { + setIsOpen(false); + }; + + useEffect(() => { + const loadUser = () => { + if (sessionStorage.getItem('user_id')) { + findOneUser(sessionStorage.getItem('user_id')).then((user) => { + setUser(user); + }); + } + }; + + loadUser(); + }, []); + + const HeaderUserButton = ( + <EuiButtonIcon + size="s" + onClick={onMenuButtonClick} + iconType="user" + title={t('userMenu.title')} + aria-label={t('userMenu.title')} + /> + ); + + return user.username ? ( + <EuiPopover + id="headerUserMenu" + ownFocus + button={HeaderUserButton} + isOpen={isOpen} + anchorPosition="downRight" + closePopover={closeMenu} + panelPaddingSize="none" + > + <div> + <EuiFlexGroup gutterSize="m" className="euiHeaderProfile" responsive={false}> + <EuiFlexItem grow={false}> + <EuiAvatar name={user.username} size="xl" /> + </EuiFlexItem> + <EuiFlexItem> + <EuiText>{user.username}</EuiText> + <EuiSpacer size="m" /> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiLink href="#/app/profile"> + {t('userMenu.editProfileButton')} + </EuiLink> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiLink onClick={() => signOut()}> + {t('userMenu.logOutButton')} + </EuiLink> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </div> + </EuiPopover> + ) : ( + <></> + ); +}; + +export default HeaderUserMenu; diff --git a/src/components/Header/header_user_menu.js b/src/components/Header/header_user_menu.js deleted file mode 100644 index feb51bb45544c887a679289dab19aa1fa8401652..0000000000000000000000000000000000000000 --- a/src/components/Header/header_user_menu.js +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - EuiAvatar, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiText, - EuiSpacer, - EuiPopover, - EuiButtonIcon, -} from '@elastic/eui'; -import { signOut } from '../../context/UserContext'; -import { findOneUser } from '../../actions/user'; - -export default function HeaderUserMenu() { - const [isOpen, setIsOpen] = useState(false); - const [user, setUser] = useState({}); - - const onMenuButtonClick = () => { - setIsOpen(!isOpen); - }; - - const closeMenu = () => { - setIsOpen(false); - }; - - const loadUser = () => { - if (sessionStorage.getItem('user_id')) { - findOneUser(sessionStorage.getItem('user_id')).then((user) => { - setUser(user); - }); - } - }; - - useEffect(() => { - loadUser(); - }, []); - - const HeaderUserButton = ( - <EuiButtonIcon - size="s" - onClick={onMenuButtonClick} - iconType="user" - title="User profile" - aria-label="User profile" - /> - ); - - return ( - user.username && ( - <EuiPopover - id="headerUserMenu" - ownFocus - button={HeaderUserButton} - isOpen={isOpen} - anchorPosition="downRight" - closePopover={closeMenu} - panelPaddingSize="none" - > - <div style={{ width: 320 }}> - <EuiFlexGroup gutterSize="m" className="euiHeaderProfile" responsive={false}> - <EuiFlexItem grow={false}> - <EuiAvatar name={user.username} size="xl" /> - </EuiFlexItem> - <EuiFlexItem> - <EuiText>{user.username}</EuiText> - <EuiSpacer size="m" /> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiLink href="#/app/profile">Edit profile</EuiLink> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiLink onClick={() => signOut()}>Log out</EuiLink> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </div> - </EuiPopover> - ) - ); -} diff --git a/src/components/Header/styles.js b/src/components/Header/styles.js index 2515727ce2391dc110ed0e581dfe850c9968dde9..27b176118e227f03aede6c0cc94d7938e23266af 100644 --- a/src/components/Header/styles.js +++ b/src/components/Header/styles.js @@ -1,8 +1,16 @@ const headerStyle = { - paddingTop: '10px', - paddingBottom: '-6px', - paddingRight: '10px', - paddingLeft: '10px', + logo: { + paddingTop: '10px', + paddingBottom: '-6px', + paddingRight: '10px', + paddingLeft: '10px', + }, + languageSwitcherItem: { + margin: '10px', + }, + userMenuItem: { + marginRight: '10px', + }, }; export default headerStyle; diff --git a/src/components/LanguageSwitcher/LanguageSwitcher.js b/src/components/LanguageSwitcher/LanguageSwitcher.js new file mode 100644 index 0000000000000000000000000000000000000000..7fb068cd82dad00d1b5b180728c1754869c3daf6 --- /dev/null +++ b/src/components/LanguageSwitcher/LanguageSwitcher.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import styles from './styles'; +import { EuiSelect } from '@elastic/eui'; + +const LanguageSwitcher = () => { + const { t, i18n } = useTranslation('common'); + + const options = [ + { text: t('languages.en'), value: 'en' }, + { text: t('languages.fr'), value: 'fr' }, + ]; + + const changeLanguage = (newLng) => { + i18n.changeLanguage(newLng).then(); + }; + + return ( + <EuiSelect + style={styles.select} + options={options} + compressed={true} + value={i18n.resolvedLanguage} + onChange={(e) => changeLanguage(e.target.value)} + /> + ); +}; + +export default LanguageSwitcher; diff --git a/src/components/LanguageSwitcher/styles.js b/src/components/LanguageSwitcher/styles.js new file mode 100644 index 0000000000000000000000000000000000000000..9c4c4bb7ae6b5014574eb8d32698e75136b9bcae --- /dev/null +++ b/src/components/LanguageSwitcher/styles.js @@ -0,0 +1,7 @@ +const styles = { + select: { + borderRadius: '6px', + }, +}; + +export default styles; diff --git a/src/components/Loading/Loading.js b/src/components/Loading/Loading.js new file mode 100644 index 0000000000000000000000000000000000000000..f5213a7dd183cad47b2cd4f933abd3a36fca42df --- /dev/null +++ b/src/components/Loading/Loading.js @@ -0,0 +1,12 @@ +import React from 'react'; +import styles from './styles'; + +const Loading = () => { + return ( + <div style={styles.container}> + <h1>Loading...</h1> + </div> + ); +}; + +export default Loading; diff --git a/src/components/Loading/package.json b/src/components/Loading/package.json new file mode 100644 index 0000000000000000000000000000000000000000..b4c70453df6e4755baccb4ff5e3783d092bca4e8 --- /dev/null +++ b/src/components/Loading/package.json @@ -0,0 +1,6 @@ +{ + "name": "Loading", + "version": "1.0.0", + "private": true, + "main": "Loading.js" +} diff --git a/src/components/Loading/styles.js b/src/components/Loading/styles.js new file mode 100644 index 0000000000000000000000000000000000000000..6c4d2706c3230a1d87dde4f06a41bcc1024c802f --- /dev/null +++ b/src/components/Loading/styles.js @@ -0,0 +1,11 @@ +const styles = { + container: { + width: '100vw', + height: '100vh', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, +}; + +export default styles; diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000000000000000000000000000000000000..236acc5804dc20022e3b4d769204a2cf81f67c06 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,22 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import Backend from 'i18next-http-backend'; + +i18n + .use(Backend) + .use(initReactI18next) + .init({ + lng: 'fr', + fallbackLng: 'fr', + ns: 'common', + defaultNS: 'common', + debug: true, + load: 'languageOnly', + loadPath: 'locales/{{lng}}/{{ns}}.json', + interpolation: { + // not needed for react as it escapes by default + escapeValue: false, + }, + }); + +export default i18n; diff --git a/src/index.js b/src/index.js index ab31dbff2807f9889d87325b74923ae2ce76a431..285bd62b697c75b71cb71332d329aa4a2f62f677 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,11 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import '@elastic/eui/dist/eui_theme_light.css'; import { UserProvider, checkUserLogin } from './context/UserContext'; import App from './App'; import { getLoginUrl, getUrlParam, redirect } from './Utils.js'; +import './i18n'; +import Loading from './components/Loading'; const userId = getUrlParam('kcId', ''); const accessToken = getUrlParam('accessToken', ''); @@ -16,7 +18,9 @@ checkUserLogin(userId, accessToken, refreshToken); if (sessionStorage.getItem('access_token')) { ReactDOM.render( <UserProvider> - <App userId={userId} accessToken={accessToken} refreshToken={refreshToken} /> + <Suspense fallback={<Loading />}> + <App userId={userId} accessToken={accessToken} refreshToken={refreshToken} /> + </Suspense> </UserProvider>, document.getElementById('root') ); diff --git a/src/pages/home/Home.js b/src/pages/home/Home.js index c54f9082634e36a5f12a16691c28c76b8dd5efc5..28040d15303ee4a05db3c9a67c483022dbea0a0f 100644 --- a/src/pages/home/Home.js +++ b/src/pages/home/Home.js @@ -3,54 +3,35 @@ import { EuiPageContent, EuiPageContentHeader, EuiPageContentHeaderSection, - EuiPageContentBody, EuiTitle, } from '@elastic/eui'; +import { useTranslation } from 'react-i18next'; const Home = () => { + const { t } = useTranslation('home'); + return ( <> <EuiPageContent> <EuiPageContentHeader> <EuiPageContentHeaderSection> <EuiTitle> - <h2>Welcome to the IN-SYLVA IS application search module</h2> + <h2>{t('pageTitle')}</h2> </EuiTitle> <br /> + <p>{t('searchToolDescription.part1')}</p> <br /> - <p> - As a reminder, it should be remembered that the metadata stored in IN-SYLVA - IS are structured around the IN-SYLVA standard. - </p> - <br /> - <p> - This standard is composed of metadata fields. A metadata record is therefore - made up of a series of fields accompanied by their value. - </p> - <br /> - <br /> - <p> - With this part of the interface you will be able to search for metadata - records (previously loaded via the portal), by defining a certain number of - criteria. - </p> + <p>{t('searchToolDescription.part2')}</p> <br /> - <p> - By default the "search" interface opens to a "plain text" search, ie the - records returned in the result are those which, in one of the field values, - contains the supplied character string. - </p> + <p>{t('searchToolDescription.part3')}</p> <br /> - <p> - A click on the Advanced search button gives access to a more complete form - via which you can do more precise searches on one or more targeted fields. - </p> + <p>{t('searchToolDescription.part4')}</p> <br /> + <p>{t('searchToolDescription.part5')}</p> <br /> - <p>Click on the "Search" tab to access the search interface.</p> + <p>{t('searchToolDescription.part6')}</p> </EuiPageContentHeaderSection> </EuiPageContentHeader> - <EuiPageContentBody></EuiPageContentBody> </EuiPageContent> </> ); diff --git a/src/pages/maps/SearchMap.js b/src/pages/maps/SearchMap.js index 67ebbb5c4c029217aaef9f0932e316c8531afffa..5ba404cfa9dfbc852696dec82dc32fe393093c90 100644 --- a/src/pages/maps/SearchMap.js +++ b/src/pages/maps/SearchMap.js @@ -1,19 +1,17 @@ import React, { useState, useEffect } from 'react'; import { Map, View } from 'ol'; import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'; -import ImageLayer from 'ol/layer/Image'; +import { Fill, Stroke, Style } from 'ol/style'; import SourceOSM from 'ol/source/OSM'; import BingMaps from 'ol/source/BingMaps'; import { Vector as VectorSource } from 'ol/source'; import WMTS from 'ol/source/WMTS'; import WMTSTileGrid from 'ol/tilegrid/WMTS'; import { getWidth } from 'ol/extent'; -import {platformModifierKeyOnly} from 'ol/events/condition.js'; -import ImageWMS from 'ol/source/ImageWMS'; +import { platformModifierKeyOnly } from 'ol/events/condition'; import GeoJSON from 'ol/format/GeoJSON'; -import {DragBox, Select} from 'ol/interaction.js'; -import { Fill, Stroke, Style, Text, Icon } from 'ol/style'; -import { Circle, Point, Polygon } from 'ol/geom'; +import { DragBox, Select } from 'ol/interaction'; +import { Circle } from 'ol/geom'; import Feature from 'ol/Feature'; import * as proj from 'ol/proj'; import * as olColor from 'ol/color'; @@ -23,463 +21,170 @@ import { OverviewMap, defaults as defaultControls, } from 'ol/control'; -import { toStringXY } from 'ol/coordinate'; import 'ol/ol.css'; -import { EuiCheckbox, EuiComboBox, EuiPopover, EuiButtonEmpty, EuiText } from '@elastic/eui'; +import { + EuiCheckbox, + EuiComboBox, + EuiPopover, + EuiButtonEmpty, + EuiText, +} from '@elastic/eui'; import { htmlIdGenerator } from '@elastic/eui/lib/services'; import { updateArrayElement } from '../../Utils.js'; +import { useTranslation } from 'react-i18next'; +import styles from './styles'; -const SearchMap = (props) => { - - const onChange = (selectedOptions) => { //selected map as filter - const selectedOption = selectedOptions; - setSelected(selectedOptions); - var selectedMap = selectedOption[0].value; - var deselectOption = document.getElementById('deselectCheckboxId'); - - // a DragBox interaction used to select features - const dragBox = new DragBox({ - condition: platformModifierKeyOnly, - }); - - var allMyLayers = map.getLayers(); - //remove previous selected map used as filter - for (let ifilter = 0; ifilter < options.length; ifilter++) { - for (let i = 0; i < allMyLayers.getLength(); i++) { - if ( options[ifilter].value == map.getLayers().item(i).get('name')){ - //window.alert('removed map:' + options[ifilter].value); - map.removeLayer(map.getLayers().item(i)); - } - } - } - - var pointsSource = map.getLayers().item(getLayerIndex('query_results')).getSource(); - - if(selectedMap == 'ResRequete'){ - toggleLayer('query_results'); - var polygonSource = pointsSource; - } - else{ - map.addLayer(maps4Filter[selectedMap]); - var polygonSource = maps4Filter[selectedMap].get('source'); - } - - map.addInteraction(dragBox); - - // clear selection when drawing a new box and when clicking on the map - dragBox.on('boxstart', function () { - selectedFeatures.clear(); - }); - - dragBox.on('boxend', function () { - const boxExtent = dragBox.getGeometry().getExtent(); - - // if the extent crosses the antimeridian process each world separately - const worldExtent = map.getView().getProjection().getExtent(); - const worldWidth = getWidth(worldExtent); - const startWorld = Math.floor((boxExtent[0] - worldExtent[0]) / worldWidth); - const endWorld = Math.floor((boxExtent[2] - worldExtent[0]) / worldWidth); - - for (let world = startWorld; world <= endWorld; ++world) { - const left = Math.max(boxExtent[0] - world * worldWidth, worldExtent[0]); - const right = Math.min(boxExtent[2] - world * worldWidth, worldExtent[2]); - const extent = [left, boxExtent[1], right, boxExtent[3]]; - - //const boxFeatures = maps4Filter[selectedMap].get('source') - const boxFeatures = polygonSource.getFeaturesInExtent(extent) - .filter( - (feature) => - !selectedFeatures.getArray().includes(feature) && - feature.getGeometry().intersectsExtent(extent), - ); - - var pointsFeatures = map.getLayers().item(getLayerIndex('query_results')).getSource().getFeatures(); - var selectedPointsSource = map.getLayers().item(getLayerIndex('selectedPointsSource')).getSource(); - - for (let polygone = 0; polygone < boxFeatures.length; polygone++) { - var polygonGeometry = boxFeatures[polygone].getGeometry(); - if (!deselectOption.checked){ - for (let point = 0; point < pointsFeatures.length; point++) { - var pointGeom = pointsFeatures[point].getGeometry(); - var pointName = pointsFeatures[point].get('nom'); - var coords = pointGeom.getCenter(); - if (polygonGeometry.intersectsCoordinate(coords)){ - if (!getFeatureNames(selectedPointsSource).includes(pointName)){ - var selectedPointFeature = new Feature(new Circle(coords,5000)); - selectedPointFeature.set('nom', pointName); - selectedPointsSource.addFeature(selectedPointFeature); - } - } - } - } - else { //remove selected features - var selectedPointsFeatures = selectedPointsSource.getFeatures(); - if(selectedPointsFeatures.length > 0 ) { - for (let point = 0; point < selectedPointsFeatures.length; point++) { - var pointGeom = selectedPointsFeatures[point].getGeometry(); - var coords = pointGeom.getCenter(); - if (polygonGeometry.intersectsCoordinate(coords)){ - selectedPointsSource.removeFeature(selectedPointsFeatures[point]); - } - } - } - } - } - selectedFeatures.extend(boxFeatures); - } - }); - }; - - const selectedStyle = new Style({ - fill: new Fill({ - color: 'rgba(255, 255, 255, 0.6)', - }), - stroke: new Stroke({ - color: 'rgba(255, 255, 255, 0.7)', - width: 2, - }), - }); - - // a normal select interaction to handle click - const select = new Select({ - style: function (feature) { - //const color = feature.get('COLOR_BIO') || '#eeeeee'; - const color = 'rgba(255, 255, 255, 0.4)'; - selectedStyle.getFill().setColor(color); - return selectedStyle; - }, - }); - - const selectedFeatures = select.getFeatures(); - const selectedPointsFeatures = select.getFeatures(); +const initResolutions = () => { + const resolutions = []; + const proj3857 = proj.get('EPSG:3857'); - selectedFeatures.on(['add', 'remove'], function () { - const names = selectedFeatures.getArray().map((feature) => { - //return feature.get('ECO_NAME'); - return feature.get('nom'); - }); - if (names.length > 0) { - console.log(names.join(', ')); - } - else { - console.log('None'); - } - }); + const maxResolution = getWidth(proj3857.getExtent()) / 256; + for (let i = 0; i < 20; i++) { + resolutions[i] = maxResolution / Math.pow(2, i); + } + return resolutions; +}; - selectedPointsFeatures.on(['add', 'remove'], function () { - const names = getFeatureNames(selectedPointsSource); - if (names.length > 0) { - //infoBoxPoints.innerHTML = names.join(', '); - } else { - //infoBoxPoints.innerHTML = 'None'; - } - }); +const initMatrixIds = () => { + const matrixIds = []; - function getFeatureNames(source) { - var featureNames = []; - source.getFeatures().forEach(function (feature) { - var name = feature.get('nom'); - if (name) { - featureNames.push(name); - } - }); - return featureNames; + for (let i = 0; i < 20; i++) { + matrixIds[i] = i.toString(); } - const styles = { - tableStyle: { - border: '1px solid black', - }, - trStyle: { - verticalAlign: 'top' - }, - 'Point': new Style({ - image: new Icon({ - anchor: [0.5, 46], - anchorXUnits: 'fraction', - anchorYUnits: 'pixels', - src: 'https://openlayers.org/en/v3.20.1/examples/data/icon.png', - }), - }), - 'MultiPoint': new Style({ - image: new Icon({ - anchor: [0.5, 46], - anchorXUnits: 'fraction', - anchorYUnits: 'pixels', - src: 'https://openlayers.org/en/v3.20.1/examples/data/icon.png' - }) - }), - Circle: new Style({ - stroke: new Stroke({ - color: 'black', - width: 2, - }), - //radius: 1000, - fill: new Fill({ - color: 'rgba(0,0,255,0.3)', - }), - }), - /* 'LineString': new Style({ - stroke: new Stroke({ - color: 'green', - width: 1, - }), - }), - 'MultiLineString': new Style({ - stroke: new Stroke({ - color: 'green', - width: 1, - }), - }), - 'MultiPolygon': new Style({ - stroke: new Stroke({ - color: 'yellow', - width: 1, - }), - fill: new Fill({ - color: 'rgba(255, 255, 0, 0.1)', - }), - }), - 'Polygon': new Style({ - stroke: new Stroke({ - color: 'blue', - lineDash: [4], - width: 3, - }), - fill: new Fill({ - color: 'rgba(0, 0, 255, 0.1)', - }), - }), - 'GeometryCollection': new Style({ - stroke: new Stroke({ - color: 'magenta', - width: 2, - }), - fill: new Fill({ - color: 'magenta', - }), - image: new CircleStyle({ - radius: 10, - fill: null, - stroke: new Stroke({ - color: 'magenta', - }), - }), - }),*/ - mapContainer: { - height: '80vh', - width: '60vw', - }, - layerTree: { - cursor: 'pointer', - }, - }; - const source = new SourceOSM(); - const overviewMapControl = new OverviewMap({ - layers: [ - new TileLayer({ - source: source, - }), - ], - }); + return matrixIds; +}; + +const SearchMap = ({ searchResults }) => { + const { t } = useTranslation('maps'); const [center, setCenter] = useState(proj.fromLonLat([2.5, 46.5])); - //const [center, setCenter] = useState(proj.fromLonLat([3.5, 44.5])); const [zoom, setZoom] = useState(6); - const styleFunction = function (feature) { - return styles[feature.getGeometry().getType()]; - }; - - //maps used to filter results - const options = [ - { label: 'Résultats de la requête', value: 'ResRequete' }, - //{ label: 'SylvoEcoRégions', value: 'SylvoEcoRegions' }, - { label: 'Régions', value: 'Regions' }, - { label: 'Départements', value: 'Departements' }, + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [deselectChecked, setDeselectChecked] = useState(false); + const filterOptions = [ + { label: t('maps:layersTable.queryResults'), value: 'ResRequete' }, + { label: t('maps:layersTable.regions'), value: 'regions' }, + { label: t('maps:layersTable.departments'), value: 'departements' }, ]; - const [selectedOptions, setSelected] = useState([options[0]]); - - const regions_IGN = new VectorSource({ - url: 'https://data.geopf.fr/wfs/ows?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&typeName=ADMINEXPRESS-COG-CARTO.LATEST:region&outputFormat=application/json', - format: new GeoJSON(), - }); - - const departements_IGN = new VectorSource({ - url: 'https://data.geopf.fr/wfs/ows?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&typeName=ADMINEXPRESS-COG-CARTO.LATEST:departement&outputFormat=application/json', - format: new GeoJSON(), + const [selectedOptions, setSelected] = useState([filterOptions[0]]); + const resultPointsSource4BG = new VectorSource({}); + const resultPointsSource = new VectorSource({}); + let selectedPointsSource = new VectorSource({}); + const vectorLayersStyle = () => { + const style = new Style({ + fill: new Fill({ + color: '#eeeeee', + }), }); - - const sylvoecoregions_IGN = new VectorSource({ - url: 'https://wxs.ign.fr/environnement/geoportail/wmts?VERSION=2.0.0&request=GetFeature&typeName=LANDCOVER.SYLVOECOREGIONS&outputFormat=application/json', - format: new GeoJSON(), + const stroke = new Stroke({ + width: 2, + lineDash: [4], + color: 'black', }); - - const resultPointsSource = new VectorSource({}); - const resultPointsSource4BG = new VectorSource({}); - var selectedPointsSource = new VectorSource({}); - - var maps4Filter = { - 'ResRequete': new VectorLayer({ - name: 'ResRequete', - source: resultPointsSource, - style: styleFunction, + let colorArray = olColor.asArray('grey').slice(); + colorArray[3] = 0.5; + style.getFill().setColor(colorArray); + style.setStroke(stroke); + return style; + }; + const mapFilters = { + ResRequete: new VectorLayer({ + name: 'ResRequete', + source: resultPointsSource, + style: (feature) => { + return styles[feature.getGeometry().getType()]; + }, }), - 'selectedPointsLayer': new VectorLayer({ + selectedPointsLayer: new VectorLayer({ name: 'selectedPointsSource', source: selectedPointsSource, - style: styleFunction, - }), - 'Regions': new VectorLayer({ - name: 'Regions', - source: regions_IGN, - //background: '#1a2b39', - opacity: 0.5, - style: function (feature) { - ////const color = feature.get('COLOR_BIO') || '#eeeeee'; - ////const color = '#eeeeee'; - var stroke = new Stroke({ - width : 2, - lineDash: [4], - color : 'black' - }); - var colorArray = olColor.asArray('grey').slice(); - colorArray[3] = 0.5; - style.getFill().setColor(colorArray); - style.setStroke(stroke); - return style; + style: (feature) => { + return styles[feature.getGeometry().getType()]; }, }), - 'Departements': new VectorLayer({ - name: 'Departements', - source: departements_IGN, - //background: '#1a2b39', + regions: new VectorLayer({ + name: 'regions', + source: new VectorSource({ + url: 'https://data.geopf.fr/wfs/ows?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&typeName=ADMINEXPRESS-COG-CARTO.LATEST:region&outputFormat=application/json', + format: new GeoJSON(), + }), opacity: 0.5, - style: function (feature) { - ////const color = feature.get('COLOR_BIO') || '#eeeeee'; - ////const color = '#eeeeee'; - var stroke = new Stroke({ - width : 2, - lineDash: [4], - color : 'black' - }); - var colorArray = olColor.asArray('grey').slice(); - colorArray[3] = 0.5; - style.getFill().setColor(colorArray); - style.setStroke(stroke); - return style; - }, + style: vectorLayersStyle(), }), - } - - const style = new Style({ - fill: new Fill({ - color: '#eeeeee', + departements: new VectorLayer({ + name: 'departements', + source: new VectorSource({ + url: 'https://data.geopf.fr/wfs/ows?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&typeName=ADMINEXPRESS-COG-CARTO.LATEST:departement&outputFormat=application/json', + format: new GeoJSON(), + }), + opacity: 0.5, + style: vectorLayersStyle(), }), - }); - - const resolutions = []; - const matrixIds = []; - const proj3857 = proj.get('EPSG:3857'); - const maxResolution = getWidth(proj3857.getExtent()) / 256; - - for (let i = 0; i < 20; i++) { - matrixIds[i] = i.toString(); - resolutions[i] = maxResolution / Math.pow(2, i); - } - - const tileGrid = new WMTSTileGrid({ - origin: [-20037508, 20037508], - resolutions: resolutions, - matrixIds: matrixIds, - }); - + }; const [mapLayers, setMapLayers] = useState([ new TileLayer({ name: 'osm-layer', - source: source, + source: new SourceOSM(), }), - /* Bing Aerial */ new TileLayer({ name: 'Bing Aerial', + visible: false, preload: Infinity, source: new BingMaps({ key: 'AtdZQap9X-lowJjvdPhTgr1BctJuGGm-ZoVw9wO6dHt1VDURjRKEkssetwOe31Xt', imagerySet: 'Aerial', }), - //visible: false }), new TileLayer({ name: 'IGN', + visible: false, source: new WMTS({ url: 'https://wxs.ign.fr/choisirgeoportail/geoportail/wmts', layer: 'GEOGRAPHICALGRIDSYSTEMS.PLANIGNV2', matrixSet: 'PM', format: 'image/png', projection: 'EPSG:3857', - tileGrid: tileGrid, + tileGrid: new WMTSTileGrid({ + origin: [-20037508, 20037508], + resolutions: initResolutions(), + matrixIds: initMatrixIds(), + }), style: 'normal', attributions: - '<a href="https://www.ign.fr/" target="_blank">' + - '<img src="https://www.ign.fr/files/default/styles/thumbnail/public/2020-06/logoIGN_300x200.png?itok=V80_0fm-" title="Institut national de l\'' + - //'<img src="https://wxs.ign.fr/static/logos/IGN/IGN.gif" title="Institut national de l\'' + - 'information géographique et forestière" alt="IGN"></a>', + '<a href="https://www.ign.fr/" target="_blank">' + + '<img src="https://www.ign.fr/files/default/styles/thumbnail/public/2020-06/logoIGN_300x200.png?itok=V80_0fm-" title="Institut national de l\'information géographique et forestière" alt="IGN"></a>', }), }), new TileLayer({ name: 'SylvoEcoregions', + visible: false, source: new WMTS({ url: 'https://data.geopf.fr/wmts?', + crossOrigin: 'anonymous', layer: 'LANDCOVER.SYLVOECOREGIONS', matrixSet: 'PM', format: 'image/png', projection: 'EPSG:3857', - tileGrid: tileGrid, + tileGrid: new WMTSTileGrid({ + origin: [-20037508, 20037508], + resolutions: initResolutions(), + matrixIds: initMatrixIds(), + }), style: 'normal', attributions: - '<a href="https://www.ign.fr/" target="_blank">' + - '<img src="https://www.ign.fr/files/default/styles/thumbnail/public/2020-06/logoIGN_300x200.png?itok=V80_0fm-" title="Institut national de l\'' + - 'information géographique et forestière" alt="IGN"></a>', + '<a href="https://www.ign.fr/" target="_blank">' + + '<img src="https://www.ign.fr/files/default/styles/thumbnail/public/2020-06/logoIGN_300x200.png?itok=V80_0fm-" title="Institut national de l\'' + + 'information géographique et forestière" alt="IGN"></a>', }), }), new VectorLayer({ - name: 'query_results', + name: 'queryResults', source: resultPointsSource4BG, }), - maps4Filter['selectedPointsLayer'], + mapFilters['selectedPointsLayer'], ]); - const [mapLayersVisibility, setMapLayersVisibility] = useState( - new Array(mapLayers.length).fill(true) - ); - - const [deselectChecked, setDeselectChecked] = useState(); - const handleDeselectCheckboxChange = (event) => { - setDeselectChecked(!deselectChecked); - }; - - - // const posGreenwich = proj.fromLonLat([0, 51.47]); - // set initial map objects - const view = new View({ - center: center, - zoom: zoom, - }); - - //list of selected results - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const onButtonClick = () =>{ - setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); - } - const closePopover = () => setIsPopoverOpen(false); - - const button = ( - <EuiButtonEmpty - iconType="documentation" - iconSide="right" - onClick={onButtonClick} - > - Liste des résultats sélectionnés - </EuiButtonEmpty> + new Array(mapLayers.length).fill(false) ); - const [map] = useState( new Map({ target: null, @@ -489,26 +194,216 @@ const SearchMap = (props) => { projection: 'EPSG:4326', }), new ScaleLine(), - overviewMapControl, + new OverviewMap({ + layers: [ + new TileLayer({ + source: new SourceOSM(), + }), + ], + }), ]), - view: view, + view: new View({ + center: center, + zoom: zoom, + }), }) ); - const processData = (props) => { - if (props.searchResults) { - props.searchResults.forEach((result) => { + // a DragBox interaction used to select features + const dragBox = new DragBox({ + condition: platformModifierKeyOnly, + }); + + // set the initial map targets + useEffect(() => { + setMapLayersVisibility((prev) => + updateArrayElement(prev, getLayerIndex('osm-layer'), true) + ); + setMapLayersVisibility((prev) => + updateArrayElement(prev, getLayerIndex('queryResults'), true) + ); + map.setTarget('map'); + map.on('moveend', () => { + setCenter(map.getView().getCenter()); + setZoom(map.getView().getZoom()); + }); + map.addInteraction(dragBox); + map.addInteraction(select); + // clear selection when drawing a new box and when clicking on the map + dragBox.on('boxstart', function () { + selectedFeatures.clear(); + }); + dragBox.on('boxend', () => { + const boxExtent = dragBox.getGeometry().getExtent(); + // if the extent crosses the antimeridian process each world separately + const worldExtent = map.getView().getProjection().getExtent(); + const worldWidth = getWidth(worldExtent); + const startWorld = Math.floor((boxExtent[0] - worldExtent[0]) / worldWidth); + const endWorld = Math.floor((boxExtent[2] - worldExtent[0]) / worldWidth); + for (let world = startWorld; world <= endWorld; ++world) { + const left = Math.max(boxExtent[0] - world * worldWidth, worldExtent[0]); + const right = Math.min(boxExtent[2] - world * worldWidth, worldExtent[2]); + const extent = [left, boxExtent[1], right, boxExtent[3]]; + const pointsSource = map + .getLayers() + .item(getLayerIndex('queryResults')) + .getSource(); + let polygonSource; + if (selectedOptions[0].value === 'queryResults') { + polygonSource = pointsSource; + } else { + polygonSource = mapFilters[selectedOptions[0].value].get('source'); + } + const boxFeatures = polygonSource + .getFeaturesInExtent(extent) + .filter( + (feature) => + !selectedFeatures.getArray().includes(feature) && + feature.getGeometry().intersectsExtent(extent) + ); + + const pointsFeatures = map + .getLayers() + .item(getLayerIndex('queryResults')) + .getSource() + .getFeatures(); + const selectedPointsSource = map + .getLayers() + .item(getLayerIndex('selectedPointsSource')) + .getSource(); + + for (let polygon = 0; polygon < boxFeatures.length; polygon++) { + const polygonGeometry = boxFeatures[polygon].getGeometry(); + if (!deselectChecked) { + for (let point = 0; point < pointsFeatures.length; point++) { + const pointGeom = pointsFeatures[point].getGeometry(); + const pointName = pointsFeatures[point].get('nom'); + const coords = pointGeom.getCenter(); + if (polygonGeometry.intersectsCoordinate(coords)) { + if (!getSelectedPointsNames(selectedPointsSource).includes(pointName)) { + let selectedPointFeature = new Feature(new Circle(coords, 5000)); + selectedPointFeature.set('nom', pointName); + selectedPointsSource.addFeature(selectedPointFeature); + } + } + } + } else { + // remove selected features + const selectedPointsFeatures = selectedPointsSource.getFeatures(); + if (selectedPointsFeatures.length > 0) { + for (let point = 0; point < selectedPointsFeatures.length; point++) { + const pointGeom = selectedPointsFeatures[point].getGeometry(); + const coords = pointGeom.getCenter(); + if (polygonGeometry.intersectsCoordinate(coords)) { + selectedPointsSource.removeFeature(selectedPointsFeatures[point]); + } + } + } + } + } + selectedFeatures.extend(boxFeatures); + } + }); + map.getView().animate({ zoom: zoom }, { center: center }, { duration: 2000 }); + processData(); + }, [searchResults, map, deselectChecked]); + + const handleDeselectCheckboxChange = () => { + setDeselectChecked((prev) => !prev); + }; + + const onButtonClick = () => { + setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const onFilterSelectChange = (newSelectedOptions) => { + setSelected(newSelectedOptions); + const allMyLayers = map.getLayers(); + // remove previously selected filter + for (let i = 0; i < filterOptions.length; i++) { + for (let j = 0; j < allMyLayers.getLength(); j++) { + if (filterOptions[i].value === map.getLayers().item(j).get('name')) { + map.removeLayer(map.getLayers().item(j)); + break; + } + } + } + if (newSelectedOptions.length === 0) { + return; + } + const selectedMap = newSelectedOptions[0].value; + if (selectedMap === 'ResRequete') { + toggleLayer('queryResults'); + } else { + map.addLayer(mapFilters[selectedMap]); + } + }; + + // a normal select interaction to handle click + const select = new Select({ + style: () => { + const selectedStyle = new Style({ + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.6)', + }), + stroke: new Stroke({ + color: 'rgba(255, 255, 255, 0.7)', + width: 2, + }), + }); + const color = 'rgba(255, 255, 255, 0.4)'; + selectedStyle.getFill().setColor(color); + return selectedStyle; + }, + }); + + const selectedFeatures = select.getFeatures(); + + selectedFeatures.on(['add', 'remove'], () => { + const names = selectedFeatures.getArray().map((feature) => { + return feature.get('nom'); + }); + if (names.length > 0) { + console.log(names.join(', ')); + } else { + console.log('None'); + } + }); + + const getSelectedPointsNames = (source) => { + let featureNames = []; + source.getFeatures().forEach(function (feature) { + const name = feature.get('nom'); + if (name) { + featureNames.push(name); + } + }); + return featureNames; + }; + + const processData = () => { + if (searchResults) { + searchResults.forEach((result) => { if ( result.experimental_site.geo_point && result.experimental_site.geo_point.longitude && result.experimental_site.geo_point.latitude ) { - const coord = [ + const resourcePointCoordinates = [ result.experimental_site.geo_point.longitude, result.experimental_site.geo_point.latitude, ]; - const pointIdentifier = result.resource.identifier + "--" + result.context.related_experimental_network_title - var pointFeature = new Feature(new Circle(proj.fromLonLat(coord),2000)); + const pointIdentifier = + result.resource.identifier + + '--' + + result.context.related_experimental_network_title; + const pointFeature = new Feature( + new Circle(proj.fromLonLat(resourcePointCoordinates), 2000) + ); pointFeature.set('nom', pointIdentifier); resultPointsSource4BG.addFeature(pointFeature); } @@ -516,24 +411,6 @@ const SearchMap = (props) => { } }; - // useEffect Hooks - // [] = component did mount - // set the initial map targets - useEffect(() => { - map.setTarget('map'); - map.on('moveend', () => { - setCenter(map.getView().getCenter()); - setZoom(map.getView().getZoom()); - }); - map.addInteraction(select); - map.getView().animate({ zoom: zoom }, { center: center }, { duration: 2000 }); - processData(props); - // clean up upon component unmount - /* return () => { - map.setTarget(null); - }; */ - }, [props]); - const getLayerIndex = (name) => { let index = 0; mapLayers.forEach((layer) => { @@ -547,7 +424,6 @@ const SearchMap = (props) => { const toggleLayer = (name) => { let updatedLayers = mapLayers; const layerIndex = getLayerIndex(name); - // let updatedLayer = updatedLayers[getLayerIndex(name)] setMapLayersVisibility( updateArrayElement( mapLayersVisibility, @@ -559,43 +435,26 @@ const SearchMap = (props) => { setMapLayers(updatedLayers); }; - // helpers - /* const btnAction = () => { - // when button is clicked, recentre map - // this does not work :( - setCenter(posGreenwich); - setZoom(6); - }; */ - // render return ( - <div> + <> <div id="map" style={styles.mapContainer}></div> <div id="layertree"> - <br /> - <h5>Couches géographiques</h5> - <table style={styles.tableStyle}> + <table style={styles.layersTable}> <thead> - <tr style={styles.trStyle}> - <th>Fonds de carte</th> - <th>Filtres géo-thématiques</th> + <tr> + <th>{t('layersTableHeaders.cartography')}</th> + <th>{t('layersTableHeaders.filters')}</th> + <th>{t('layersTableHeaders.tools')}</th> </tr> </thead> <tbody> - <tr style={styles.trStyle}> - <td> + <tr> + <td style={styles.layersTableCells}> <ul> <li> <EuiCheckbox id={htmlIdGenerator()()} - label="Query result" - checked={mapLayersVisibility[getLayerIndex('query_results')]} - onChange={(e) => toggleLayer('query_results')} - /> - </li> - <li> - <EuiCheckbox - id={htmlIdGenerator()()} - label="Open Street Map" + label={t('layersTable.openStreetMap')} checked={mapLayersVisibility[getLayerIndex('osm-layer')]} onChange={(e) => toggleLayer('osm-layer')} /> @@ -603,7 +462,7 @@ const SearchMap = (props) => { <li> <EuiCheckbox id={htmlIdGenerator()()} - label="Bing Aerial" + label={t('layersTable.bingAerial')} checked={mapLayersVisibility[getLayerIndex('Bing Aerial')]} onChange={(e) => toggleLayer('Bing Aerial')} /> @@ -611,7 +470,7 @@ const SearchMap = (props) => { <li> <EuiCheckbox id={htmlIdGenerator()()} - label="PLAN IGN" + label={t('maps:layersTable.IGN')} checked={mapLayersVisibility[getLayerIndex('IGN')]} onChange={(e) => toggleLayer('IGN')} /> @@ -626,62 +485,67 @@ const SearchMap = (props) => { </li> </ul> </td> - <td> + <td style={styles.layersTableCells}> + <ul> + <EuiCheckbox + id={htmlIdGenerator()()} + label={t('layersTable.queryResults')} + checked={mapLayersVisibility[getLayerIndex('queryResults')]} + onChange={() => toggleLayer('queryResults')} + /> + <br /> + <EuiComboBox + aria-label={t('maps:layersTable.selectFilterOption')} + placeholder={t('maps:layersTable.selectFilterOption')} + singleSelection={{ asPlainText: true }} + options={filterOptions} + selectedOptions={selectedOptions} + onChange={onFilterSelectChange} + /> + </ul> + </td> + <td style={styles.layersTableCells}> <ul> <input - id="deselectCheckboxId" type="checkbox" checked={deselectChecked} onChange={handleDeselectCheckboxChange} - /><i>Délectionner</i> - <EuiComboBox - aria-label="Accessible screen reader label" - placeholder="Select a single option" - singleSelection={{ asPlainText: true }} - options={options} - selectedOptions={selectedOptions} - onChange={onChange} /> - <br/> + <i> + {deselectChecked + ? t('maps:layersTable.pointSelectionMode.select') + : t('maps:layersTable.pointSelectionMode.unselect')} + </i> + <br /> <EuiPopover - button={button} + button={ + <EuiButtonEmpty onClick={onButtonClick}> + {t('maps:layersTable.selectedPointsList')} + </EuiButtonEmpty> + } isOpen={isPopoverOpen} closePopover={closePopover} > <EuiText style={{ width: 300 }}> - <p>{getFeatureNames(map.getLayers().item(getLayerIndex('selectedPointsSource')).getSource()).toString()}</p> + <p> + {map.getLayers().item(getLayerIndex('selectedPointsSource')) && + getSelectedPointsNames( + map + .getLayers() + .item(getLayerIndex('selectedPointsSource')) + .getSource() + ).toString()} + </p> </EuiText> </EuiPopover> </ul> </td> - <td> - <div id="info"> </div> - </td> </tr> </tbody> </table> </div> - {/*<div - style={styles.bluecircle} - ref={overlayRef} - id="overlay" - title="overlay" - />*/} - {/*<button - style={{ - position: "absolute", - right: 10, - top: 10, - backgroundColor: "white" - }} - onClick={() => { - btnAction(); - }} - > - CLICK - </button>*/} - </div> + </> ); }; -export default SearchMap; \ No newline at end of file +export default SearchMap; diff --git a/src/pages/maps/styles.js b/src/pages/maps/styles.js new file mode 100644 index 0000000000000000000000000000000000000000..72257f8df8e8a461b206e9bd55aa3dfce453c1e9 --- /dev/null +++ b/src/pages/maps/styles.js @@ -0,0 +1,44 @@ +import { Fill, Icon, Stroke, Style } from 'ol/style'; + +const styles = { + Point: new Style({ + image: new Icon({ + anchor: [0.5, 46], + anchorXUnits: 'fraction', + anchorYUnits: 'pixels', + src: 'https://openlayers.org/en/v3.20.1/examples/data/icon.png', + }), + }), + MultiPoint: new Style({ + image: new Icon({ + anchor: [0.5, 46], + anchorXUnits: 'fraction', + anchorYUnits: 'pixels', + src: 'https://openlayers.org/en/v3.20.1/examples/data/icon.png', + }), + }), + Circle: new Style({ + stroke: new Stroke({ + color: 'black', + width: 2, + }), + fill: new Fill({ + color: 'rgba(0,0,255,0.3)', + }), + }), + mapContainer: { + height: '80vh', + width: '60vw', + }, + layerTree: { + cursor: 'pointer', + }, + layersTable: { + margin: '20px', + }, + layersTableCells: { + padding: '10px', + }, +}; + +export default styles; diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js index d6fecd338609b352e8e558aff61e4d54d576bb79..89bd3a7edde33dcf0540186c86bba5aedbae42ff 100644 --- a/src/pages/profile/Profile.js +++ b/src/pages/profile/Profile.js @@ -23,20 +23,11 @@ import { createUserRequest, deleteUserRequest, } from '../../actions/user'; - -/* const fieldsGridOptions = { - filter: true, - filterType: "dropdown", - responsive: "stacked", - selectableRows: 'multiple', - selectableRowsOnClick: true, - onRowsSelect: (rowsSelected, allRows) => { - }, - onRowClick: (rowData, rowState) => { - }, -}; */ +import { useTranslation } from 'react-i18next'; +import styles from './styles'; const Profile = () => { + const { t } = useTranslation(['profile', 'common', 'validation']); const [user, setUser] = useState({}); const [userRole, setUserRole] = useState(''); const [groups, setGroups] = useState([]); @@ -47,15 +38,36 @@ const Profile = () => { const [valueError, setValueError] = useState(undefined); useEffect(() => { + const loadUser = () => { + if (sessionStorage.getItem('user_id')) { + findOneUser(sessionStorage.getItem('user_id')).then((user) => { + setUser(user); + }); + findOneUserWithGroupAndRole(sessionStorage.getItem('user_id')).then((result) => { + const userGroupList = userGroups; + result.forEach((user) => { + if (user.groupname) { + userGroupList.push({ + id: user.groupid, + label: user.groupname, + description: user.groupdescription, + }); + } + setUserRole(user.rolename); + }); + setUserGroups(userGroupList); + }); + } + }; loadUser(); getUserRequests(); getUserGroups(); getUserRoles(); - }, []); + }, [userGroups]); const groupColumns = [ - { field: 'label', name: 'Group Name', width: '30%' }, - { field: 'description', name: 'Group description' }, + { field: 'label', name: t('groups.groupName'), width: '30%' }, + { field: 'description', name: t('groups.groupDescription') }, ]; const getUserRoles = () => { @@ -98,8 +110,8 @@ const Profile = () => { const requestActions = [ { - name: 'Cancel', - description: 'Cancel this request', + name: t('common:validationActions.cancel'), + description: t('requestsList.cancelRequest'), icon: 'trash', type: 'icon', onClick: onDeleteRequest, @@ -107,33 +119,15 @@ const Profile = () => { ]; const requestsColumns = [ - { field: 'request_message', name: 'Message', width: '90%' }, - { field: 'is_processed', name: 'Processed' }, - { name: 'Delete', actions: requestActions }, + { + field: 'request_message', + name: t('requestsList.requestsMessage'), + width: '85%', + }, + { field: 'is_processed', name: t('requestsList.processed') }, + { name: t('common:validationActions.cancel'), actions: requestActions }, ]; - const loadUser = () => { - if (sessionStorage.getItem('user_id')) { - findOneUser(sessionStorage.getItem('user_id')).then((user) => { - setUser(user); - }); - findOneUserWithGroupAndRole(sessionStorage.getItem('user_id')).then((result) => { - const userGroupList = userGroups; - result.forEach((user) => { - if (user.groupname) { - userGroupList.push({ - id: user.groupid, - label: user.groupname, - description: user.groupdescription, - }); - } - setUserRole(user.rolename); - }); - setUserGroups(userGroupList); - }); - } - }; - const getUserGroupLabels = () => { let labelList = ''; if (!!userGroups) { @@ -155,42 +149,65 @@ const Profile = () => { ); }; + const onSendRoleRequest = () => { + if (selectedRole) { + const message = `The user ${user.username} (${user.email}) has made a request to get the role : ${selectedRole}.`; + createUserRequest(user.id, message); + sendMail('User role request', message); + alert(t('validation:requestSent')); + } + getUserRequests(); + }; + + const onSendGroupRequest = () => { + const groupList = []; + if (userGroups) { + userGroups.forEach((group) => { + groupList.push(group.label); + }); + const message = `The user ${user.username} (${user.email}) has made a request to be part of these groups : ${groupList}.`; + createUserRequest(user.id, message); + sendMail('User group request', message); + alert(t('validation:requestSent')); + } + getUserRequests(); + }; + return ( <> <EuiPageContent> <EuiPageContentHeader> <EuiPageContentHeaderSection> <EuiTitle> - <h2>Profile management</h2> + <h2>{t('pageTitle')}</h2> </EuiTitle> </EuiPageContentHeaderSection> </EuiPageContentHeader> <EuiPageContentBody> <EuiForm component="form"> <EuiTitle size="s"> - <h3>Group list</h3> + <h3>{t('groups.groupsList')}</h3> </EuiTitle> <EuiFormRow fullWidth label=""> <EuiBasicTable items={groups} columns={groupColumns} /> </EuiFormRow> <EuiSpacer size="l" /> <EuiTitle size="s"> - <h3>Requests list</h3> + <h3>{t('requestsList.requestsList')}</h3> </EuiTitle> <EuiFormRow fullWidth label=""> <EuiBasicTable items={userRequests} columns={requestsColumns} /> </EuiFormRow> <EuiSpacer size="l" /> <EuiTitle size="s"> - <h3>Request group assignment modifications</h3> + <h3>{t('groupRequests.requestGroupAssignment')}</h3> </EuiTitle> {getUserGroupLabels() ? ( - <p> - You currently belong to (or have a pending demand for) these groups :{' '} - {getUserGroupLabels()}{' '} - </p> + <p + style={styles.currentRoleOrGroupText} + >{`${t('groupRequests.currentGroups')} ${getUserGroupLabels()}`}</p> ) : ( - <p>You currently belong to no group</p> + <p>{t('groupRequests.noGroup')}</p> )} <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> <EuiComboBox @@ -207,28 +224,20 @@ const Profile = () => { <EuiSpacer size="m" /> <EuiButton onClick={() => { - if (userGroups) { - const groupList = []; - userGroups.forEach((group) => { - groupList.push(group.label); - }); - const message = `The user ${user.username} (${user.email}) has made a request to be part of these groups : ${groupList}.`; - createUserRequest(user.id, message); - sendMail('User group request', message); - alert('Your group request has been sent to the administrators.'); - } - getUserRequests(); + onSendGroupRequest(); }} fill > - Send request + {t('common:validationActions.send')} </EuiButton> <EuiSpacer size="l" /> <EuiTitle size="s"> - <h3>Request an application role</h3> + <h3>{t('roleRequests.requestRoleAssignment')}</h3> </EuiTitle> {userRole ? ( - <p>Your current role is (or have a pending demand for) {userRole}</p> + <p + style={styles.currentRoleOrGroupText} + >{`${t('roleRequests.currentRole')} ${userRole}`}</p> ) : ( <></> )} @@ -245,17 +254,11 @@ const Profile = () => { <EuiSpacer size="m" /> <EuiButton onClick={() => { - if (selectedRole) { - const message = `The user ${user.username} (${user.email}) has made a request to get the role : ${selectedRole}.`; - createUserRequest(user.id, message); - sendMail('User role request', message); - alert('Your role request has been sent to the administrators.'); - } - getUserRequests(); + onSendRoleRequest(); }} fill > - Send request + {t('common:validationActions.send')} </EuiButton> </EuiForm> </EuiPageContentBody> diff --git a/src/pages/profile/styles.js b/src/pages/profile/styles.js new file mode 100644 index 0000000000000000000000000000000000000000..4c2776e98c63a6bf9e919a8f152504dccba201f4 --- /dev/null +++ b/src/pages/profile/styles.js @@ -0,0 +1,8 @@ +const style = { + currentRoleOrGroupText: { + marginTop: '10px', + marginBottom: '10px', + }, +}; + +export default style; diff --git a/src/pages/results/Results.js b/src/pages/results/Results.js index c54481e0a83b3d9be02a63e6c191c8544493ce56..b647f7738dbe1b6beff02d77d7e6bd933048dc74 100644 --- a/src/pages/results/Results.js +++ b/src/pages/results/Results.js @@ -15,8 +15,8 @@ import { createTheme, MuiThemeProvider } from '@material-ui/core/styles'; import MUIDataTable from 'mui-datatables'; import JsonView from '@in-sylva/json-view'; import { updateArrayElement } from '../../Utils.js'; - -const download = require('downloadjs'); +import download from 'downloadjs'; +import { useTranslation } from 'react-i18next'; const getMuiTheme = () => createTheme({ @@ -38,16 +38,17 @@ const changeFlyoutState = (array, index, value, defaultValue) => { return newArray; }; -const Results = (searchResults, search, basicSearch) => { +const Results = ({ searchResults, searchQuery }) => { + const { t } = useTranslation('results'); const [resultsCol, setResultsCol] = useState([]); const [results, setResults] = useState([]); const [isFlyoutOpen, setIsFlyoutOpen] = useState([false]); - const [searchQuery, setSearchQuery] = useState(''); + const [searchQueryText, setSearchQueryText] = useState(''); useEffect(() => { processData(searchResults); - search.length ? setSearchQuery(search) : setSearchQuery(basicSearch); - }, [searchResults, search, basicSearch]); + setSearchQueryText(searchQuery); + }, [searchResults]); const updateTableCell = (tableContent, value, colIndex, rowIndex) => { const updatedRow = updateArrayElement(tableContent[rowIndex], colIndex, value); @@ -62,6 +63,55 @@ const Results = (searchResults, search, basicSearch) => { return updatedResults; }; + const buildColumnName = (name) => { + // Replace underscore with spaces + name = name.split('_').join(' '); + // Uppercase first character + name = name.charAt(0).toUpperCase() + name.slice(1); + return name; + }; + + const processData = (metadata) => { + if (metadata) { + const columns = []; + const rows = []; + columns.push({ + name: 'Currently open', + options: { + display: true, + viewColumns: true, + filter: true, + }, + }); + for (let recordIndex = 0; recordIndex < metadata.length; recordIndex++) { + const row = []; + const displayedFields = metadata[recordIndex].resource; + const flyoutCell = recordFlyout(metadata[recordIndex], recordIndex); + if (recordIndex >= isFlyoutOpen.length) { + setIsFlyoutOpen([...isFlyoutOpen, false]); + } + row.push(flyoutCell); + for (const fieldName in displayedFields) { + if (typeof displayedFields[fieldName] === 'string') { + if (recordIndex === 0) { + const column = { + name: buildColumnName(fieldName), + options: { + display: true, + }, + }; + columns.push(column); + } + row.push(displayedFields[fieldName]); + } + } + rows.push(row); + } + setResultsCol(columns); + setResults(rows); + } + }; + const recordFlyout = (record, recordIndex, isOpen) => { if (isOpen) { return ( @@ -113,13 +163,11 @@ const Results = (searchResults, search, basicSearch) => { filterType: 'dropdown', responsive: 'standard', selectableRows: 'none', - selectableRowsOnClick: false, + selectableRowsOnClick: true, onRowSelectionChange: (rowsSelected, allRows) => {}, onRowClick: (rowData, rowState) => {}, onCellClick: (val, colMeta) => { - // if (searchResults.hits.hits && colMeta.colIndex !== 0) { if (searchResults && colMeta.colIndex !== 0) { - // const updatedTable = updateTableCell(closeAllFlyouts(results), recordFlyout(searchResults.hits.hits[colMeta.rowIndex]._source, colMeta.rowIndex, !isFlyoutOpen[colMeta.rowIndex]), 0, colMeta.rowIndex) const updatedTable = updateTableCell( closeAllFlyouts(results), recordFlyout( @@ -142,178 +190,13 @@ const Results = (searchResults, search, basicSearch) => { }, }; - /* const displayRecord = (record) => { - let recordDisplay = [] - if (!!record) { - const fields = Object.keys(record) - fields.forEach(field => { - if (typeof record[field] != 'string') { - // const rndId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); - if (isNaN(field)) { - // const buttonContent = `"${field}"` - let isStrArray = false - if (Array.isArray(record[field])) { - isStrArray = true - record[field].forEach(item => { - if (typeof item != 'string') - isStrArray = false - }) - } - if (isStrArray) { - recordDisplay.push( - <> - <h3> -  {field} - </h3> - {displayRecord(record[field])} - </> - ) - } else { - recordDisplay.push( - <> - <EuiSpacer size="s" /> - <EuiPanel paddingSize="s"> - <EuiAccordion id={Math.random().toString()} buttonContent={field}> - <EuiText size="s"> - {displayRecord(record[field])} - </EuiText> - </EuiAccordion> - </EuiPanel> - </> - ) - } - } else { - recordDisplay.push( - <> - {displayRecord(record[field])} - </> - ) - if (fields[fields.indexOf(field) + 1]) - recordDisplay.push( - <> - <EuiSpacer size="m" /> - <hr /> - </> - ) - } - } else { - if (isNaN(field)) { - recordDisplay.push( - <> - <h3> -  {field} - </h3> - <EuiTextColor color="secondary">  {record[field]}</EuiTextColor> - </> - ) - } else { - recordDisplay.push( - <> - <EuiSpacer size="s" /> - <EuiTextColor color="secondary">  {record[field]}</EuiTextColor> - </> - ) - } - } - }) - return recordDisplay - } - } - - const recordFlyout = (record, recordIndex, isFlyoutOpen, setIsFlyoutOpen) => { - let flyout - if (isFlyoutOpen[recordIndex]) { - // const flyOutContent = ReactHtmlParser(displayRecord(record, 1)) - const flyOutStr = displayRecord(record) - // const flyOutContent = parse(flyOutStr, { htmlparser2: { lowerCaseTags: false } }) - const flyout = ( - <> - <EuiFlyout - onClose={() => { - // setIsFlyoutOpen(updateArrayElement(isFlyoutOpen, recordIndex, false)) - // updateResultsCell(false, 0, recordIndex) - const updatedArray = changeFlyoutState(isFlyoutOpen, recordIndex, !isFlyoutOpen[recordIndex], false) - setIsFlyoutOpen(updatedArray) - }} - aria-labelledby={recordIndex}> - <EuiFlyoutBody> - <EuiText size="s"> - <Fragment> - {flyOutStr} - </Fragment> - </EuiText> - </EuiFlyoutBody> - </EuiFlyout> - <EuiIcon type='eye' color='danger' /> - </> - ); - return (flyout) - } - } */ - - /* const viewButton = (record, recordIndex, isFlyoutOpenIndex, isFlyoutOpen, setIsFlyoutOpen) => { - return ( - <> - <EuiButtonIcon - size="m" - color="success" - onClick={() => { - const flyOutArray = updateArrayElement(isFlyoutOpen, recordIndex, !isFlyoutOpen[recordIndex]) - setIsFlyoutOpen(flyOutArray) - updateResultsCell(!isFlyoutOpen[recordIndex], isFlyoutOpenIndex, recordIndex) - }} - iconType="eye" - title="View record" - /> - {recordFlyout(record, recordIndex, isFlyoutOpen, setIsFlyoutOpen)} - </> - ) - } */ - - const processData = (metadata) => { - // if (metadata && metadata.hits) { - if (metadata) { - const columns = []; - const rows = []; - // const metadataRecords = metadata.hits.hits - columns.push({ - name: 'currently open', - options: { - display: true, - viewColumns: true, - filter: true, - }, - }); - /* for (let recordIndex = 0; recordIndex < metadataRecords.length; recordIndex++) { - const row = [] - const displayedFields = metadataRecords[recordIndex]._source.resource - const flyoutCell = recordFlyout(metadataRecords[recordIndex]._source, recordIndex) */ - for (let recordIndex = 0; recordIndex < metadata.length; recordIndex++) { - const row = []; - const displayedFields = metadata[recordIndex].resource; - const flyoutCell = recordFlyout(metadata[recordIndex], recordIndex); - if (recordIndex >= isFlyoutOpen.length) { - setIsFlyoutOpen([...isFlyoutOpen, false]); - } - row.push(flyoutCell); - for (const fieldName in displayedFields) { - if (typeof displayedFields[fieldName] === 'string') { - if (recordIndex === 0) { - const column = { - name: fieldName, - options: { - display: true, - }, - }; - columns.push(column); - } - row.push(displayedFields[fieldName]); - } - } - rows.push(row); - } - setResultsCol(columns); - setResults(rows); + const downloadResults = () => { + if (searchResults) { + download( + `{"metadataRecords": ${JSON.stringify(searchResults, null, '\t')}}`, + 'InSylvaSearchResults.json', + 'application/json' + ); } }; @@ -324,39 +207,24 @@ const Results = (searchResults, search, basicSearch) => { <EuiSpacer size="s" /> <EuiFlexItem grow={false}> <EuiTitle size="xs"> - <h2>Your query : {searchQuery}</h2> + <h2>{t('results:yourQuery', { query: searchQueryText })}</h2> </EuiTitle> </EuiFlexItem> <EuiSpacer size="s" /> </EuiFlexGroup> <EuiFlexGroup> <EuiFlexItem> - <EuiCallOut - size="s" - title="Click on a line of the table to inspect resource metadata (except for the first column)." - iconType="search" - /> + <EuiCallOut size="s" title={t('results:clickOnRowTip')} iconType="search" /> </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiButton - fill - onClick={() => { - if (searchResults) { - download( - `{"metadataRecords": ${JSON.stringify(searchResults, null, '\t')}}`, - 'InSylvaSearchResults.json', - 'application/json' - ); - } - }} - > - Download as JSON + <EuiButton fill onClick={() => downloadResults()}> + {t('results:downloadResultsButton.JSON')} </EuiButton> </EuiFlexItem> </EuiFlexGroup> <MuiThemeProvider theme={getMuiTheme()}> <MUIDataTable - title={'Search results'} + title={t('results:table.title')} data={results} columns={resultsCol} options={resultsGridOptions} diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js new file mode 100644 index 0000000000000000000000000000000000000000..cd4b8a08ae977919cf27a639518b757db83b9dc5 --- /dev/null +++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js @@ -0,0 +1,1647 @@ +import { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiComboBox, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiGlobalToastList, + EuiHealth, + EuiIcon, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiPanel, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiProgress, + EuiRadioGroup, + EuiSelect, + EuiSpacer, + EuiSwitch, + EuiTextArea, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { + changeNameToLabel, + createAdvancedQueriesBySource, + getFieldsBySection, + getSections, + removeArrayElement, + SearchField, + updateArrayElement, + updateSearchFieldValues, +} from '../../../Utils'; +import { getQueryCount, searchQuery } from '../../../actions/source'; +import { DateOptions, NumericOptions, Operators } from '../Data'; +import TextField from '@material-ui/core/TextField'; +import { addUserHistory, fetchUserHistory } from '../../../actions/user'; +import { useTranslation } from 'react-i18next'; + +const updateSources = ( + searchFields, + sources, + setSelectedSources, + setAvailableSources +) => { + let updatedSources = []; + let availableSources = []; + let noPrivateField = true; + //search for policy fields to filter sources + searchFields.forEach((field) => { + if (field.isValidated) { + //if sources haven't already been filtered + if (noPrivateField && !updatedSources.length) { + availableSources = sources; + } else { + availableSources = updatedSources; + } + updatedSources = []; + field.sources.forEach((sourceId) => { + noPrivateField = false; + const source = availableSources.find((src) => src.id === sourceId); + if (source && !updatedSources.includes(source)) updatedSources.push(source); + }); + } + }); + setSelectedSources(updatedSources); + if (noPrivateField && !updatedSources.length) { + setAvailableSources(sources); + } else { + setAvailableSources(updatedSources); + } +}; + +const fieldValuesToString = (field) => { + let strValues = ''; + switch (field.type) { + case 'Numeric': + field.values.forEach((element) => { + switch (element.option) { + case 'between': + strValues = `${strValues} ${element.value1} <= ${field.name} <= ${element.value2} or `; + break; + default: + strValues = `${strValues} ${field.name} ${element.option} ${element.value1} or `; + } + }); + if (strValues.endsWith('or ')) + strValues = strValues.substring(0, strValues.length - 4); + break; + case 'Date': + field.values.forEach((element) => { + switch (element.option) { + case 'between': + strValues = `${strValues} ${element.startDate} <= ${field.name} <= ${element.endDate} or `; + break; + default: + strValues = `${strValues} ${field.name} ${element.option} ${element.startDate} or `; + } + }); + if (strValues.endsWith(' or ')) + strValues = strValues.substring(0, strValues.length - 4); + break; + case 'List': + strValues = `${strValues} ${field.name} = `; + field.values.forEach((element) => { + strValues = `${strValues} ${element.label}, `; + }); + if (strValues.endsWith(', ')) + strValues = strValues.substring(0, strValues.length - 2); + break; + //type : text + default: + strValues = `${strValues} ${field.name} = ${field.values}`; + } + return strValues; +}; + +const addHistory = ( + kcID, + search, + searchName, + searchFields, + searchDescription, + setUserHistory +) => { + addUserHistory( + sessionStorage.getItem('user_id'), + search, + searchName, + searchFields, + searchDescription + ).then(() => { + fetchHistory(setUserHistory); + }); +}; + +const fetchHistory = (setUserHistory) => { + fetchUserHistory(sessionStorage.getItem('user_id')).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) => { + if (field.isValidated) { + searchText = + searchText + + `{${fieldValuesToString(field)} } ${Operators[selectedOperatorId].value.toUpperCase()} `; + } + }); + if (searchText.endsWith(' AND ')) { + searchText = searchText.substring(0, searchText.length - 5); + } else if (searchText.endsWith(' OR ')) { + searchText = searchText.substring(0, searchText.length - 4); + } + setSearchCount(); + setSearch(searchText); +}; + +const HistorySelect = ({ + sources, + setAvailableSources, + setSelectedSources, + setSearch, + setSearchFields, + setSearchCount, + setFieldCount, + userHistory, + setUserHistory, + selectedSavedSearch, + setSelectedSavedSearch, +}) => { + const [historySelectError, setHistorySelectError] = useState(undefined); + const { t } = useTranslation('search'); + + useEffect(() => { + fetchHistory(setUserHistory); + }, [setUserHistory]); + + const onHistoryChange = (selectedSavedSearch) => { + setHistorySelectError(undefined); + if (!!selectedSavedSearch[0].query) { + setSelectedSavedSearch(selectedSavedSearch); + setSearch(selectedSavedSearch[0].query); + setSearchCount(); + setFieldCount([]); + } + if (!!selectedSavedSearch[0].ui_structure) { + updateSources( + selectedSavedSearch[0].ui_structure, + sources, + setSelectedSources, + setAvailableSources + ); + setSearchFields(selectedSavedSearch[0].ui_structure); + } + }; + + const onHistorySearchChange = (value, hasMatchingOptions) => { + if (value.length === 0 || hasMatchingOptions) { + setHistorySelectError(undefined); + } else { + setHistorySelectError(t('search:advancedSearch.errorInvalidOption', { value })); + } + }; + + return ( + <> + {userHistory && Object.keys(userHistory).length !== 0 && ( + <EuiFormRow + error={historySelectError} + isInvalid={historySelectError !== undefined} + > + <EuiComboBox + placeholder={t('search:advancedSearch.searchHistory.placeholder')} + singleSelection={{ asPlainText: true }} + options={userHistory} + selectedOptions={selectedSavedSearch} + onChange={onHistoryChange} + onSearchChange={onHistorySearchChange} + /> + </EuiFormRow> + )} + </> + ); +}; + +const SearchBar = ({ + search, + setSearch, + setSearchResults, + searchFields, + setSearchFields, + searchName, + setSearchName, + searchDescription, + setSearchDescription, + readOnlyQuery, + setReadOnlyQuery, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources, + standardFields, + sources, + setSelectedTabNumber, + searchCount, + setSearchCount, + setFieldCount, + isSaveSearchModalOpen, + setIsSaveSearchModalOpen, + selectedSavedSearch, + setSelectedSavedSearch, + selectedOperatorId, + createEditableQueryToast, +}) => { + const { t } = useTranslation(['search', 'common']); + const [userHistory, setUserHistory] = useState({}); + const [isLoading, setIsLoading] = useState(false); + + const closeSaveSearchModal = () => { + setIsSaveSearchModalOpen(false); + }; + + const onClickAdvancedSearch = () => { + 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 onClickCountResults = () => { + if (!!search) { + const queriesWithIndices = createAdvancedQueriesBySource( + standardFields, + search, + selectedSources, + availableSources + ); + getQueryCount(queriesWithIndices).then((result) => { + if (result || result === 0) setSearchCount(result); + }); + } + }; + + const onClickSaveSearch = () => { + if (!!searchName) { + addHistory( + sessionStorage.getItem('user_id'), + search, + searchName, + searchFields, + searchDescription, + setUserHistory + ); + setSearchName(''); + setSearchDescription(''); + closeSaveSearchModal(); + } + }; + + const SaveSearchModal = () => { + return ( + <EuiOverlayMask> + <EuiModal onClose={closeSaveSearchModal} initialFocus="[name=searchName]"> + <EuiModalHeader> + <EuiModalHeaderTitle> + {t('advancedSearch.searchHistory.saveSearch')} + </EuiModalHeaderTitle> + </EuiModalHeader> + <EuiModalBody> + <EuiForm> + <EuiFormRow label={t('advancedSearch.searchHistory.addSavedSearchName')}> + <EuiFieldText + name="searchName" + value={searchName} + onChange={(e) => { + setSearchName(e.target.value); + }} + /> + </EuiFormRow> + <EuiFormRow + label={t('advancedSearch.searchHistory.addSavedSearchDescription')} + > + <EuiTextArea + value={searchDescription} + onChange={(e) => setSearchDescription(e.target.value)} + placeholder={t( + 'advancedSearch.searchHistory.addSavedSearchDescriptionPlaceholder' + )} + fullWidth + compressed + /> + </EuiFormRow> + </EuiForm> + </EuiModalBody> + + <EuiModalFooter> + <EuiButtonEmpty + onClick={() => { + closeSaveSearchModal(); + }} + > + {t('common:validationActions.cancel')} + </EuiButtonEmpty> + <EuiButton + onClick={() => { + onClickSaveSearch(); + }} + fill + > + {t('common:validationActions.save')} + </EuiButton> + </EuiModalFooter> + </EuiModal> + </EuiOverlayMask> + ); + }; + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem> + <EuiTextArea + readOnly={readOnlyQuery} + value={search} + onChange={(e) => setSearch(e.target.value)} + placeholder={t('search:advancedSearch.textQueryPlaceholder')} + fullWidth + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + size="s" + fill + onClick={() => { + onClickAdvancedSearch(); + }} + > + {t('search:sendSearchButton')} + </EuiButton> + <EuiSpacer size="s" /> + {!isNaN(searchCount) && ( + <> + <EuiTextColor + color="secondary" + style={{ display: 'flex', justifyContent: 'center' }} + > + {t('search:advancedSearch.resultsCount', { count: searchCount })} + </EuiTextColor> + <EuiSpacer size="s" /> + </> + )} + <EuiButton + size="s" + onClick={() => { + onClickCountResults(); + }} + > + {t('search:advancedSearch.countResultsButton')} + </EuiButton> + <EuiSpacer size="s" /> + <EuiButton + size="s" + onClick={() => { + setIsSaveSearchModalOpen(true); + }} + > + {t('search:advancedSearch.searchHistory.saveSearch')} + </EuiButton> + {isSaveSearchModalOpen && <SaveSearchModal />} + <EuiSpacer size="s" /> + <EuiSwitch + compressed + label={t('search:advancedSearch.editableSearchButton')} + checked={!readOnlyQuery} + onChange={() => { + setReadOnlyQuery(!readOnlyQuery); + if (readOnlyQuery) { + createEditableQueryToast(); + } + }} + /> + </EuiFlexItem> + </EuiFlexGroup> + {isLoading && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiProgress postion="fixed" size="l" color="accent" /> + </EuiFlexItem> + </EuiFlexGroup> + )} + <EuiSpacer size="s" /> + <EuiFlexGroup> + <EuiFlexItem> + <HistorySelect + sources={sources} + setAvailableSources={setAvailableSources} + setSelectedSources={setSelectedSources} + setSearch={setSearch} + searchFields={searchFields} + selectedOperatorId={selectedOperatorId} + userHistory={userHistory} + setUserHistory={setUserHistory} + setSearchFields={setSearchFields} + setSearchCount={setSearchCount} + setFieldCount={setFieldCount} + selectedSavedSearch={selectedSavedSearch} + setSelectedSavedSearch={setSelectedSavedSearch} + /> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; + +const PopoverSelect = ({ + standardFields, + searchFields, + setSearchFields, + selectedField, + setSelectedField, + selectedSection, + setSelectedSection, + isPopoverSelectOpen, + setIsPopoverSelectOpen, +}) => { + const { t } = useTranslation('search'); + + const handleAddField = () => { + if (!!selectedField[0]) { + const field = standardFields.find( + (item) => + item.field_name.replace(/_|\./g, ' ') === + selectedSection[0].label + ' ' + selectedField[0].label + ); + switch (field.field_type) { + case 'Text': + setSearchFields([ + ...searchFields, + new SearchField(field.field_name, field.field_type, '', false, field.sources), + ]); + break; + case 'List': + setSearchFields([ + ...searchFields, + new SearchField(field.field_name, field.field_type, [], false, field.sources), + ]); + break; + default: + setSearchFields([ + ...searchFields, + new SearchField( + field.field_name, + field.field_type, + [{}], + false, + field.sources + ), + ]); + } + } + }; + + 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.title')} + singleSelection={{ asPlainText: true }} + options={getFieldsBySection(standardFields, selectedSection[0])} + selectedOptions={selectedField} + onChange={(selected) => setSelectedField(selected)} + isClearable={true} + renderOption={renderOption} + /> + <EuiPopoverFooter> + <EuiButton + size="s" + onClick={() => { + handleAddField(); + setIsPopoverSelectOpen(false); + setSelectedSection([]); + setSelectedField([]); + }} + > + {t('search:advancedSearch.fields.addFieldPopover.button')} + </EuiButton> + </EuiPopoverFooter> + </> + ); + } + }; + + return ( + <EuiPopover + panelPaddingSize="s" + button={ + <EuiButton + iconType="listAdd" + iconSide="left" + onClick={() => setIsPopoverSelectOpen(!isPopoverSelectOpen)} + > + {t('search:advancedSearch.fields.addFieldPopover.openPopoverButton')} + </EuiButton> + } + isOpen={isPopoverSelectOpen} + closePopover={() => setIsPopoverSelectOpen(false)} + > + <div style={{ width: 'intrinsic', minWidth: 240 }}> + <EuiPopoverTitle> + {t('search:advancedSearch.fields.addFieldPopover.title')} + </EuiPopoverTitle> + <EuiComboBox + placeholder={t('search:advancedSearch.fields.addFieldPopover.selectSection')} + singleSelection={{ asPlainText: true }} + options={getSections(standardFields)} + selectedOptions={selectedSection} + onChange={(selected) => { + setSelectedSection(selected); + setSelectedField([]); + }} + isClearable={false} + /> + </div> + {selectField()} + </EuiPopover> + ); +}; + +const PopoverValueContent = ({ + index, + standardFields, + searchFields, + setSearchFields, + valueError, + setValueError, + setSearch, + setSearchCount, + fieldCount, + setFieldCount, + isPopoverValueOpen, + setIsPopoverValueOpen, + selectedOperatorId, + datePickerStyles, + createPolicyToast, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources, +}) => { + const { t } = useTranslation(['search', 'common']); + + const onValueSearchChange = (value, hasMatchingOptions) => { + if (value.length === 0 || hasMatchingOptions) { + setValueError(undefined); + } else { + setValueError(t('search:advancedSearch.errorInvalidOption', { value })); + } + }; + + const validateFieldValues = () => { + let fieldValues; + if (Array.isArray(searchFields[index].values)) { + fieldValues = []; + searchFields[index].values.forEach((value) => { + if (!!value) { + fieldValues.push(value); + } + }); + } else { + fieldValues = searchFields[index].values; + } + + const updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + fieldValues, + true, + searchFields[index].sources + ) + ); + setSearchFields(updatedSearchFields); + updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); + setFieldCount(updateArrayElement(fieldCount, index)); + if (searchFields[index].sources.length) { + const filteredSources = []; + searchFields[index].sources.forEach((sourceId) => { + let source; + if (selectedSources.length) { + source = selectedSources.find((src) => src.id === sourceId); + } else { + source = availableSources.find((src) => src.id === sourceId); + } + if (source) { + filteredSources.push(source); + } + }); + setAvailableSources(filteredSources); + setSelectedSources(filteredSources); + createPolicyToast(); + } + }; + + const invalidateFieldValues = () => { + const updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + searchFields[index].values, + false, + searchFields[index].sources + ) + ); + setSearchFields(updatedSearchFields); + updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); + }; + + const ValuePopoverFooter = (i) => { + if (i === searchFields[index].values.length - 1) { + return ( + <EuiPopoverFooter> + <EuiButton + size="s" + onClick={() => { + setSearchFields( + updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + [...searchFields[index].values, {}], + false, + searchFields[index].sources + ) + ) + ); + }} + > + {t('search:advancedSearch.fields.fieldContentPopover.addValue')} + </EuiButton> + <EuiButton + size="s" + style={{ float: 'right' }} + onClick={() => { + validateFieldValues(); + setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)); + }} + > + {t('common:validationActions.validate')} + </EuiButton> + </EuiPopoverFooter> + ); + } + }; + + const addFieldValue = (i, selectedOption) => { + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { option: selectedOption }) + ) + ); + }; + + const getListFieldValues = () => { + const listFieldValues = []; + standardFields + .find((item) => item.field_name === searchFields[index].name) + .values.split(', ') + .sort() + .forEach((element) => { + listFieldValues.push({ label: element }); + }); + return listFieldValues; + }; + + switch (searchFields[index].type) { + case 'Text': + return ( + <> + <EuiFlexItem> + <EuiFieldText + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.inputTextValue' + )} + value={searchFields[index].values} + onChange={(e) => + setSearchFields( + updateSearchFieldValues(searchFields, index, e.target.value) + ) + } + /> + </EuiFlexItem> + <EuiPopoverFooter> + <EuiButton + size="s" + style={{ float: 'right' }} + onClick={() => { + validateFieldValues(); + setIsPopoverValueOpen( + updateArrayElement(isPopoverValueOpen, index, false) + ); + }} + > + {t('common:validationActions.validate')} + </EuiButton> + </EuiPopoverFooter> + </> + ); + case 'List': + return ( + <> + <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> + <EuiComboBox + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.selectValues' + )} + options={getListFieldValues()} + selectedOptions={searchFields[index].values} + onChange={(selectedOptions) => { + setValueError(undefined); + setSearchFields( + updateSearchFieldValues(searchFields, index, selectedOptions) + ); + }} + onSearchChange={onValueSearchChange} + /> + </EuiFormRow> + <EuiPopoverFooter> + <EuiButton + size="s" + style={{ float: 'right' }} + onClick={() => { + validateFieldValues(); + setIsPopoverValueOpen( + updateArrayElement(isPopoverValueOpen, index, false) + ); + }} + > + {t('common:validationActions.validate')} + </EuiButton> + </EuiPopoverFooter> + </> + ); + case 'Numeric': + const NumericValues = (i) => { + if (!!searchFields[index].values[i].option) { + switch (searchFields[index].values[i].option) { + case 'between': + return ( + <> + <EuiFlexItem> + <EuiFieldText + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.firstValue' + )} + value={searchFields[index].values[i].value1} + onChange={(e) => { + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + value1: e.target.value, + value2: searchFields[index].values[i].value2, + }) + ) + ); + }} + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiFieldText + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.secondValue' + )} + value={searchFields[index].values[i].value2} + onChange={(e) => + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + value1: searchFields[index].values[i].value1, + value2: e.target.value, + }) + ) + ) + } + /> + </EuiFlexItem> + {ValuePopoverFooter(i)} + </> + ); + + default: + return ( + <> + <EuiFlexItem> + <EuiFieldText + placeholder={t( + 'search:advancedSearch.fields.fieldContentPopover.inputTextValue' + )} + value={searchFields[index].values[i].value1} + onChange={(e) => { + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + value1: e.target.value, + value2: searchFields[index].values[i].value2, + }) + ) + ); + }} + /> + </EuiFlexItem> + {ValuePopoverFooter(i)} + </> + ); + } + } + }; + + return ( + <> + {searchFields[index].values.map((value, i) => ( + <div key={i}> + <EuiSelect + hasNoInitialSelection + id="Select an option" + options={NumericOptions} + value={searchFields[index].values[i].option} + onChange={(e) => { + addFieldValue(i, e.target.value); + invalidateFieldValues(); + }} + /> + {NumericValues(i)} + </div> + ))} + </> + ); + case 'Date': + const SelectDates = (i) => { + if (!!searchFields[index].values[i].option) { + switch (searchFields[index].values[i].option) { + case 'between': + return ( + <> + <form className={datePickerStyles.container} noValidate> + <TextField + label={t( + 'search:advancedSearch.fields.fieldContentPopover.betweenDate' + )} + type="date" + defaultValue={ + !!searchFields[index].values[i].startDate + ? searchFields[index].values[i].startDate + : Date.now() + } + className={datePickerStyles.textField} + InputLabelProps={{ + shrink: true, + }} + onChange={(e) => + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + startDate: e.target.value, + endDate: searchFields[index].values[i].endDate, + }) + ) + ) + } + /> + </form> + <form className={datePickerStyles.container} noValidate> + <TextField + label={t( + 'search:advancedSearch.fields.fieldContentPopover.andDate' + )} + type="date" + defaultValue={ + !!searchFields[index].values[i].endDate + ? searchFields[index].values[i].endDate + : Date.now() + } + className={datePickerStyles.textField} + InputLabelProps={{ + shrink: true, + }} + onChange={(e) => + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + startDate: searchFields[index].values[i].startDate, + endDate: e.target.value, + }) + ) + ) + } + /> + </form> + {ValuePopoverFooter(i)} + </> + ); + + default: + return ( + <> + <form className={datePickerStyles.container} noValidate> + <TextField + type="date" + defaultValue={ + !!searchFields[index].values[i].startDate + ? searchFields[index].values[i].startDate + : Date.now() + } + className={datePickerStyles.textField} + InputLabelProps={{ + shrink: true, + }} + onChange={(e) => + setSearchFields( + updateSearchFieldValues( + searchFields, + index, + updateArrayElement(searchFields[index].values, i, { + option: searchFields[index].values[i].option, + startDate: e.target.value, + endDate: Date.now(), + }) + ) + ) + } + /> + </form> + {ValuePopoverFooter(i)} + </> + ); + } + } + }; + + return ( + <> + {searchFields[index].values.map((value, i) => ( + <div key={i}> + <EuiSelect + hasNoInitialSelection + id="Select an option" + options={DateOptions} + value={searchFields[index].values[i].option} + onChange={(e) => { + addFieldValue(i, e.target.value); + invalidateFieldValues(); + }} + /> + {SelectDates(i)} + </div> + ))} + </> + ); + default: + } +}; + +const PopoverValueButton = ({ + index, + standardFields, + searchFields, + setSearchFields, + isPopoverValueOpen, + setIsPopoverValueOpen, + valueError, + setValueError, + setSearch, + setSearchCount, + fieldCount, + setFieldCount, + selectedOperatorId, + datePickerStyles, + createPolicyToast, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources, +}) => { + const { t } = useTranslation('search'); + + return ( + <EuiPopover + panelPaddingSize="s" + button={ + <EuiButtonIcon + size="s" + color="primary" + onClick={() => + setIsPopoverValueOpen( + updateArrayElement(isPopoverValueOpen, index, !isPopoverValueOpen[index]) + ) + } + iconType="documentEdit" + title={t('search:advancedSearch.fields.fieldContentPopover.addFieldValues')} + aria-label={t( + 'search:advancedSearch.fields.fieldContentPopover.addFieldValues' + )} + /> + } + isOpen={isPopoverValueOpen[index]} + closePopover={() => + setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)) + } + > + <div style={{ width: 240 }}> + <PopoverValueContent + index={index} + standardFields={standardFields} + searchFields={searchFields} + setSearchFields={setSearchFields} + valueError={valueError} + setValueError={setValueError} + setSearch={setSearch} + setSearchCount={setSearchCount} + fieldCount={fieldCount} + setFieldCount={setFieldCount} + isPopoverValueOpen={isPopoverValueOpen} + setIsPopoverValueOpen={setIsPopoverValueOpen} + selectedOperatorId={selectedOperatorId} + datePickerStyles={datePickerStyles} + createPolicyToast={createPolicyToast} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + /> + </div> + </EuiPopover> + ); +}; + +const FieldsPanel = ({ + standardFields, + setStandardFields, + searchFields, + setSearchFields, + selectedField, + setSelectedField, + selectedSection, + setSelectedSection, + isPopoverSelectOpen, + setIsPopoverSelectOpen, + isPopoverValueOpen, + setIsPopoverValueOpen, + valueError, + setValueError, + search, + setSearch, + setSearchCount, + selectedOperatorId, + setSelectedOperatorId, + fieldCount, + setFieldCount, + availableSources, + setAvailableSources, + selectedSources, + setSelectedSources, + sources, + datePickerStyles, + createPolicyToast, +}) => { + const { t } = useTranslation('search'); + + const countFieldValues = (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 handleRemoveField = (index) => { + const updatedSearchFields = removeArrayElement(searchFields, index); + setSearchFields(updatedSearchFields); + updateSources(updatedSearchFields, sources, setSelectedSources, setAvailableSources); + updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); + }; + + const handleClearValues = (index) => { + let updatedSearchFields; + switch (searchFields[index].type) { + case 'Text': + updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + '', + false, + searchFields[index].sources + ) + ); + break; + case 'List': + updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + [], + false, + searchFields[index].sources + ) + ); + break; + default: + updatedSearchFields = updateArrayElement( + searchFields, + index, + new SearchField( + searchFields[index].name, + searchFields[index].type, + [{}], + false, + searchFields[index].sources + ) + ); + } + setSearchFields(updatedSearchFields); + updateSources(updatedSearchFields, sources, setSelectedSources, setAvailableSources); + setFieldCount(updateArrayElement(fieldCount, index)); + updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); + }; + + if (standardFields === []) { + return <h2>{t('search:advancedSearch.fields.loadingFields')}</h2>; + } + + return ( + <> + <EuiTitle size="xs"> + <h2>{t('search:advancedSearch.fields.title')}</h2> + </EuiTitle> + <EuiPanel paddingSize="m"> + <EuiFlexGroup direction="column"> + {searchFields.map((field, index) => ( + <EuiPanel key={'field' + index} paddingSize="s"> + <EuiFlexItem grow={false}> + <EuiFlexGroup direction="row" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButtonIcon + size="s" + color="danger" + onClick={() => handleRemoveField(index)} + iconType="indexClose" + title={t('search:advancedSearch.fields.removeFieldButton')} + aria-label={t('search:advancedSearch.fields.removeFieldButton')} + /> + </EuiFlexItem> + <EuiFlexItem> + {field.isValidated ? ( + <> + {field.sources.length ? ( + <EuiHealth color="danger"> + {fieldValuesToString(field).replace(/_|\./g, ' ')} + </EuiHealth> + ) : ( + <EuiHealth color="primary"> + {fieldValuesToString(field).replace(/_|\./g, ' ')} + </EuiHealth> + )} + </> + ) : ( + <> + {field.sources.length ? ( + <EuiHealth color="danger"> + {field.name.replace(/_|\./g, ' ')} + </EuiHealth> + ) : ( + <EuiHealth color="primary"> + {field.name.replace(/_|\./g, ' ')} + </EuiHealth> + )} + </> + )} + </EuiFlexItem> + <EuiFlexItem grow={false}> + {!isNaN(fieldCount[index]) && ( + <EuiTextColor color="secondary"> + {t('search:advancedSearch.resultsCount', { + count: fieldCount[index], + })} + </EuiTextColor> + )} + </EuiFlexItem> + <EuiFlexItem grow={false}> + {field.isValidated && ( + <EuiButtonIcon + size="s" + onClick={() => countFieldValues(field, index)} + iconType="number" + title={t('search:advancedSearch.countResultsButton')} + aria-label={t('search:advancedSearch.countResultsButton')} + /> + )} + </EuiFlexItem> + <EuiFlexItem grow={false}> + {field.isValidated && ( + <EuiButtonIcon + size="s" + color="danger" + onClick={() => handleClearValues(index)} + iconType="trash" + title={t('search:advancedSearch.fields.clearValues')} + aria-label={t('search:advancedSearch.fields.clearValues')} + /> + )} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <PopoverValueButton + index={index} + standardFields={standardFields} + setStandardFields={setStandardFields} + searchFields={searchFields} + setSearchFields={setSearchFields} + isPopoverValueOpen={isPopoverValueOpen} + setIsPopoverValueOpen={setIsPopoverValueOpen} + valueError={valueError} + setValueError={setValueError} + search={search} + setSearch={setSearch} + setSearchCount={setSearchCount} + fieldCount={fieldCount} + setFieldCount={setFieldCount} + selectedOperatorId={selectedOperatorId} + datePickerStyles={datePickerStyles} + createPolicyToast={createPolicyToast} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiPanel> + ))} + </EuiFlexGroup> + <EuiSpacer size="l" /> + <PopoverSelect + standardFields={standardFields} + setStandardFields={setStandardFields} + searchFields={searchFields} + setSearchFields={setSearchFields} + selectedField={selectedField} + setSelectedField={setSelectedField} + selectedSection={selectedSection} + setSelectedSection={setSelectedSection} + isPopoverSelectOpen={isPopoverSelectOpen} + setIsPopoverSelectOpen={setIsPopoverSelectOpen} + fieldCount={fieldCount} + setFieldCount={setFieldCount} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + /> + </EuiPanel> + <EuiSpacer size="s" /> + <EuiRadioGroup + options={Operators.map((operator) => { + return { ...operator, label: t(operator.label) }; + })} + idSelected={selectedOperatorId} + onChange={(id) => { + setSelectedOperatorId(id); + updateSearch(setSearch, searchFields, id, setSearchCount); + }} + name="operators group" + legend={{ + children: <span>{t('search:advancedSearch.searchOptions.title')}</span>, + }} + /> + </> + ); +}; + +const SourceSelect = ({ + availableSources, + selectedSources, + setSelectedSources, + sourceSelectError, + setSourceSelectError, +}) => { + const { t } = useTranslation('search'); + + if (Object.keys(availableSources).length === 0) { + return ( + <p> + <EuiIcon type="alert" color="danger" /> + </p> + ); + } + availableSources.forEach((source) => { + if (source.name) { + source = changeNameToLabel(source); + } + }); + + const onSourceChange = (selectedOptions) => { + setSourceSelectError(undefined); + setSelectedSources(selectedOptions); + }; + + const onSourceSearchChange = (value, hasMatchingOptions) => { + if (value.length === 0 || hasMatchingOptions) { + setSourceSelectError(undefined); + } else { + setSourceSelectError( + t('search:advancedSearch.errorInvalidOption', { value: value }) + ); + } + }; + + return ( + <> + <EuiTitle size="xs"> + <h2>{t('search:advancedSearch.partnerSources.title')}</h2> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiFlexItem> + <EuiFormRow error={sourceSelectError} isInvalid={sourceSelectError !== undefined}> + <EuiComboBox + placeholder={t('search:advancedSearch.partnerSources.allSourcesSelected')} + options={availableSources} + selectedOptions={selectedSources} + onChange={onSourceChange} + onSearchChange={onSourceSearchChange} + /> + </EuiFormRow> + </EuiFlexItem> + </> + ); +}; + +const AdvancedSearch = ({ + search, + setSearch, + searchResults, + setSearchResults, + searchFields, + setSearchFields, + searchName, + setSearchName, + searchDescription, + setSearchDescription, + readOnlyQuery, + setReadOnlyQuery, + selectedSources, + setSelectedSources, + availableSources, + setAvailableSources, + standardFields, + setStandardFields, + sources, + setSelectedTabNumber, + searchCount, + setSearchCount, + setFieldCount, + isReadOnlyModalOpen, + setIsReadOnlyModalOpen, + isSaveSearchModalOpen, + setIsSaveSearchModalOpen, + userHistory, + setUserHistory, + selectedSavedSearch, + setSelectedSavedSearch, + selectedOperatorId, + setIsAdvancedSearch, + isAdvancedSearch, + selectedField, + selectedSection, + setSelectedField, + setSelectedSection, + isPopoverSelectOpen, + setIsPopoverSelectOpen, + setIsPopoverValueOpen, + isPopoverValueOpen, + valueError, + setValueError, + setSelectedOperatorId, + fieldCount, + sourceSelectError, + datePickerStyles, + setSourceSelectError, +}) => { + const { t } = useTranslation('search'); + const [notificationToasts, setNotificationToasts] = useState([]); + + const createPolicyToast = () => { + const toast = { + title: t('search:advancedSearch.policyToast.title'), + color: 'warning', + iconType: 'alert', + toastLifeTimeMs: 15000, + text: ( + <> + <p>{t('search:advancedSearch.policyToast.content.0')}</p> + <p>{t('search:advancedSearch.policyToast.content.1')}</p> + <p>{t('search:advancedSearch.policyToast.content.2')}</p> + </> + ), + }; + setNotificationToasts(notificationToasts.concat(toast)); + }; + + const createEditableQueryToast = () => { + const toast = { + title: t('search:advancedSearch.policyToast.title'), + color: 'warning', + iconType: 'alert', + toastLifeTimeMs: 15000, + text: ( + <> + <p>{t('search:advancedSearch.editableQueryToast.content.part1')}</p> + <ul> + <li>{t('search:advancedSearch.editableQueryToast.content.part2')}</li> + <li>{t('search:advancedSearch.editableQueryToast.content.part3')}</li> + <li>{t('search:advancedSearch.editableQueryToast.content.part4')}</li> + </ul> + </> + ), + }; + setNotificationToasts(notificationToasts.concat(toast)); + }; + + const removeToast = (removedToast) => { + setNotificationToasts( + notificationToasts.filter((toast) => toast.id !== removedToast.id) + ); + }; + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiSpacer size="s" /> + <EuiButtonEmpty + onClick={() => { + setIsAdvancedSearch(!isAdvancedSearch); + }} + > + {t('search:advancedSearch.switchSearchMode')} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <EuiSpacer size="s" /> + <SearchBar + search={search} + setSearch={setSearch} + searchResults={searchResults} + setSearchResults={setSearchResults} + searchFields={searchFields} + setSearchFields={setSearchFields} + searchName={searchName} + setSearchName={setSearchName} + searchDescription={searchDescription} + setSearchDescription={setSearchDescription} + readOnlyQuery={readOnlyQuery} + setReadOnlyQuery={setReadOnlyQuery} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + standardFields={standardFields} + sources={sources} + setSelectedTabNumber={setSelectedTabNumber} + searchCount={searchCount} + setSearchCount={setSearchCount} + setFieldCount={setFieldCount} + isReadOnlyModalOpen={isReadOnlyModalOpen} + setIsReadOnlyModalOpen={setIsReadOnlyModalOpen} + isSaveSearchModalOpen={isSaveSearchModalOpen} + setIsSaveSearchModalOpen={setIsSaveSearchModalOpen} + userHistory={userHistory} + setUserHistory={setUserHistory} + selectedSavedSearch={selectedSavedSearch} + setSelectedSavedSearch={setSelectedSavedSearch} + selectedOperatorId={selectedOperatorId} + createEditableQueryToast={createEditableQueryToast} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <EuiSpacer size="s" /> + <FieldsPanel + standardFields={standardFields} + setStandardFields={setStandardFields} + searchFields={searchFields} + setSearchFields={setSearchFields} + selectedField={selectedField} + setSelectedField={setSelectedField} + selectedSection={selectedSection} + setSelectedSection={setSelectedSection} + isPopoverSelectOpen={isPopoverSelectOpen} + setIsPopoverSelectOpen={setIsPopoverSelectOpen} + isPopoverValueOpen={isPopoverValueOpen} + setIsPopoverValueOpen={setIsPopoverValueOpen} + valueError={valueError} + setValueError={setValueError} + search={search} + setSearch={setSearch} + setSearchCount={setSearchCount} + selectedOperatorId={selectedOperatorId} + setSelectedOperatorId={setSelectedOperatorId} + fieldCount={fieldCount} + setFieldCount={setFieldCount} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + sources={sources} + datePickerStyles={datePickerStyles} + createPolicyToast={createPolicyToast} + /> + <EuiSpacer size="s" /> + <SourceSelect + availableSources={availableSources} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + sourceSelectError={sourceSelectError} + setSourceSelectError={setSourceSelectError} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiGlobalToastList + toasts={notificationToasts} + dismissToast={removeToast} + toastLifeTimeMs={2500} + /> + </> + ); +}; + +export default AdvancedSearch; diff --git a/src/pages/search/BasicSearch/BasicSearch.js b/src/pages/search/BasicSearch/BasicSearch.js new file mode 100644 index 0000000000000000000000000000000000000000..6f8892dfadc2b06779d33e3ffccd166cadb54361 --- /dev/null +++ b/src/pages/search/BasicSearch/BasicSearch.js @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, +} from '@elastic/eui'; +import { createBasicQueriesBySource } from '../../../Utils'; +import { searchQuery } from '../../../actions/source'; +import { useTranslation } from 'react-i18next'; + +const BasicSearch = ({ + standardFields, + availableSources, + selectedSources, + basicSearch, + setBasicSearch, + setIsAdvancedSearch, + isAdvancedSearch, + setSearchResults, + setSelectedTabNumber, +}) => { + const { t } = useTranslation('search'); + const [isLoading, setIsLoading] = useState(false); + + const onFormSubmit = () => { + setIsLoading(true); + const queriesWithIndices = createBasicQueriesBySource( + standardFields, + basicSearch, + selectedSources, + availableSources + ); + searchQuery(queriesWithIndices).then((result) => { + setSearchResults(result); + setSelectedTabNumber(1); + if (isLoading) { + setIsLoading(false); + } + }); + }; + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiSpacer size="s" /> + <EuiButtonEmpty + onClick={() => { + setIsAdvancedSearch(!isAdvancedSearch); + }} + > + {t('basicSearch.switchSearchMode')} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <EuiSpacer size="s" /> + <form onSubmit={() => onFormSubmit()}> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFieldSearch + value={basicSearch} + onChange={(e) => setBasicSearch(e.target.value)} + placeholder={t('basicSearch.searchInputPlaceholder')} + fullWidth + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton type="submit" fill isDisabled={isAdvancedSearch}> + {t('sendSearchButton')} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </form> + {isLoading && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiProgress postion="fixed" size="l" color="accent" /> + </EuiFlexItem> + </EuiFlexGroup> + )} + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; + +export default BasicSearch; diff --git a/src/pages/search/Data.js b/src/pages/search/Data.js index c6577648ea4029f5243b834d71fac2563982fbbb..d0a399a60e2e9dbe10a10c075a207501b35c6130 100644 --- a/src/pages/search/Data.js +++ b/src/pages/search/Data.js @@ -2,12 +2,12 @@ export const Operators = [ { id: '0', value: 'And', - label: 'Match all criterias', + label: 'search:advancedSearch.searchOptions.matchAll', }, { id: '1', value: 'Or', - label: 'Match at least one criteria', + label: 'search:advancedSearch.searchOptions.matchAtLeastOne', }, ]; diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index a1c0044118e8373b06e4335e97f36b759329e7ff..61bd93e712548ff2481e0473f84334777f2c7262 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -1,1525 +1,34 @@ import React, { useState, useEffect } from 'react'; import { - EuiProgress, - EuiRadioGroup, - EuiFieldText, - EuiPanel, - EuiPopover, - EuiPopoverTitle, - EuiPopoverFooter, EuiTabbedContent, - EuiFormRow, - EuiComboBox, EuiPageContentBody, EuiForm, - EuiTextArea, EuiFlexGroup, EuiFlexItem, - EuiFieldSearch, - EuiButton, - EuiButtonEmpty, - EuiSwitch, - EuiButtonIcon, - EuiIcon, EuiSpacer, EuiPageContent, EuiPageContentHeader, EuiTitle, EuiPageContentHeaderSection, - EuiTextColor, - EuiOverlayMask, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, - EuiSelect, - EuiGlobalToastList, - EuiHealth, } from '@elastic/eui'; -import { makeStyles } from '@material-ui/core/styles'; -import TextField from '@material-ui/core/TextField'; -import { Operators, NumericOptions, DateOptions } from './Data'; import Results from '../results/Results'; import SearchMap from '../maps/SearchMap'; -import { - createBasicQueriesBySource, - changeNameToLabel, - SearchField, - removeNullFields, - getSections, - getFieldsBySection, - updateArrayElement, - removeArrayElement, - updateSearchFieldValues, - createAdvancedQueriesBySource, -} from '../../Utils.js'; +import { removeNullFields } from '../../Utils.js'; import { fetchPublicFields, fetchUserPolicyFields, fetchSources, - searchQuery, - getQueryCount, } from '../../actions/source'; -import { addUserHistory, fetchUserHistory } from '../../actions/user'; - -const useStyles = makeStyles((theme) => ({ - container: { - display: 'flex', - flexWrap: 'wrap', - }, - textField: { - marginLeft: theme.spacing(1), - marginRight: theme.spacing(1), - width: 240, - }, -})); - -const fieldValuesToString = (field) => { - let strValues = ''; - switch (field.type) { - case 'Numeric': - field.values.forEach((element) => { - switch (element.option) { - case 'between': - strValues = `${strValues} ${element.value1} <= ${field.name} <= ${element.value2} or `; - break; - default: - strValues = `${strValues} ${field.name} ${element.option} ${element.value1} or `; - } - }); - if (strValues.endsWith('or ')) - strValues = strValues.substring(0, strValues.length - 4); - break; - case 'Date': - field.values.forEach((element) => { - switch (element.option) { - case 'between': - strValues = `${strValues} ${element.startDate} <= ${field.name} <= ${element.endDate} or `; - break; - default: - strValues = `${strValues} ${field.name} ${element.option} ${element.startDate} or `; - } - }); - if (strValues.endsWith(' or ')) - strValues = strValues.substring(0, strValues.length - 4); - break; - case 'List': - strValues = `${strValues} ${field.name} = `; - field.values.forEach((element) => { - strValues = `${strValues} ${element.label}, `; - }); - if (strValues.endsWith(', ')) - strValues = strValues.substring(0, strValues.length - 2); - break; - - //type : text - default: - strValues = `${strValues} ${field.name} = ${field.values}`; - } - return strValues; -}; - -const updateSources = ( - searchFields, - sources, - setSelectedSources, - setAvailableSources -) => { - let updatedSources = []; - let availableSources = []; - let noPrivateField = true; - //search for policy fields to filter sources - searchFields.forEach((field) => { - if (field.isValidated) { - //if sources haven't already been filtered - if (noPrivateField && !updatedSources.length) { - availableSources = sources; - } else { - availableSources = updatedSources; - } - updatedSources = []; - field.sources.forEach((sourceId) => { - noPrivateField = false; - const source = availableSources.find((src) => src.id === sourceId); - if (source && !updatedSources.includes(source)) updatedSources.push(source); - }); - } - }); - setSelectedSources(updatedSources); - if (noPrivateField && !updatedSources.length) { - setAvailableSources(sources); - } else { - setAvailableSources(updatedSources); - } -}; - -const fetchHistory = (setUserHistory) => { - fetchUserHistory(sessionStorage.getItem('user_id')).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 addHistory = ( - kcID, - search, - searchName, - searchFields, - searchDescription, - setUserHistory -) => { - addUserHistory( - sessionStorage.getItem('user_id'), - search, - searchName, - searchFields, - searchDescription - ).then(() => { - fetchHistory(setUserHistory); - }); -}; - -const updateSearch = (setSearch, searchFields, selectedOperatorId, setSearchCount) => { - let searchText = ''; - searchFields.forEach((field) => { - if (field.isValidated) { - searchText = - searchText + - `{${fieldValuesToString(field)} } ${Operators[selectedOperatorId].value.toUpperCase()} `; - } - }); - if (searchText.endsWith(' AND ')) { - searchText = searchText.substring(0, searchText.length - 5); - } else if (searchText.endsWith(' OR ')) { - searchText = searchText.substring(0, searchText.length - 4); - } - setSearchCount(); - setSearch(searchText); -}; - -const HistorySelect = ( - sources, - setAvailableSources, - setSelectedSources, - setSearch, - searchFields, - selectedOperatorId, - userHistory, - setUserHistory, - setSearchFields, - setSearchCount, - setFieldCount, - selectedSavedSearch, - setSelectedSavedSearch, - historySelectError, - setHistorySelectError -) => { - if (Object.keys(userHistory).length !== 0) { - const onHistoryChange = (selectedSavedSearch) => { - setHistorySelectError(undefined); - if (!!selectedSavedSearch[0].query) { - setSelectedSavedSearch(selectedSavedSearch); - setSearch(selectedSavedSearch[0].query); - setSearchCount(); - setFieldCount([]); - } - if (!!selectedSavedSearch[0].ui_structure) { - updateSources( - selectedSavedSearch[0].ui_structure, - sources, - setSelectedSources, - setAvailableSources - ); - setSearchFields(selectedSavedSearch[0].ui_structure); - } - }; - - const onHistorySearchChange = (value, hasMatchingOptions) => { - setHistorySelectError( - value.length === 0 || hasMatchingOptions - ? undefined - : `"${value}" is not a valid option` - ); - }; - - return ( - <> - <EuiFormRow - error={historySelectError} - isInvalid={historySelectError !== undefined} - > - <EuiComboBox - placeholder="Load a previous search" - singleSelection={{ asPlainText: true }} - options={userHistory} - selectedOptions={selectedSavedSearch} - onChange={onHistoryChange} - onSearchChange={onHistorySearchChange} - /> - </EuiFormRow> - </> - ); - } -}; - -const SearchBar = ( - isLoading, - setIsLoading, - search, - setSearch, - searchResults, - setSearchResults, - searchFields, - setSearchFields, - searchName, - setSearchName, - searchDescription, - setSearchDescription, - readOnlyQuery, - setReadOnlyQuery, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources, - standardFields, - sources, - setSelectedTabNumber, - searchCount, - setSearchCount, - setFieldCount, - isReadOnlyModalOpen, - setIsReadOnlyModalOpen, - isSaveSearchModalOpen, - setIsSaveSearchModalOpen, - userHistory, - setUserHistory, - selectedSavedSearch, - setSelectedSavedSearch, - historySelectError, - setHistorySelectError, - selectedOperatorId, - createEditableQueryToast -) => { - // const closeReadOnlyModal = () => setIsReadOnlyModalOpen(false) - - /* const switchReadOnly = (readOnlyQuery, isReadOnlyModalOpen) => { - if (readOnlyQuery) { - setIsReadOnlyModalOpen(true) - } else { - setReadOnlyQuery(true) - } */ - /* if (!localStorage.getItem("InSylvaReadOnlySearch") && readOnlyQuery) { - setIsReadOnlyModalOpen(!isReadOnlyModalOpen) - } */ - // } - - /* let readOnlyModal; - - if (isReadOnlyModalOpen) { - readOnlyModal = ( - <EuiOverlayMask> - <EuiConfirmModal - title="Allow query editing" - onCancel={() => closeReadOnlyModal()} - onConfirm={() => { - setReadOnlyQuery(!readOnlyQuery) - closeReadOnlyModal() - }} - cancelButtonText="No" - confirmButtonText="Yes" - buttonColor="danger" - defaultFocusedButton="confirm"> - <p>Be aware that manually editing the query can spoil search results.</p> - <p>The syntax needs to be respected :</p> - <ul>Fields and their values must be given between brackets : { }</ul> - <ul>Check eventual typing mistakes</ul> - <ul>Make sure every opened bracket is properly closed</ul> - <p>Are you sure you want to do this?</p> - </EuiConfirmModal> - </EuiOverlayMask> - ) - }*/ - - const closeSaveSearchModal = () => setIsSaveSearchModalOpen(false); - - let saveSearchModal; - - if (isSaveSearchModalOpen) { - saveSearchModal = ( - <EuiOverlayMask> - <EuiModal onClose={closeSaveSearchModal} initialFocus="[name=searchName]"> - <EuiModalHeader> - <EuiModalHeaderTitle>Save search</EuiModalHeaderTitle> - </EuiModalHeader> - - <EuiModalBody> - <EuiForm> - <EuiFormRow label="Search name"> - <EuiFieldText - name="searchName" - value={searchName} - onChange={(e) => { - setSearchName(e.target.value); - }} - /> - </EuiFormRow> - <EuiFormRow label="Description (optional)"> - <EuiTextArea - value={searchDescription} - onChange={(e) => setSearchDescription(e.target.value)} - placeholder="Search description..." - fullWidth - compressed - /> - </EuiFormRow> - </EuiForm> - </EuiModalBody> - - <EuiModalFooter> - <EuiButtonEmpty - onClick={() => { - closeSaveSearchModal(); - }} - > - Cancel - </EuiButtonEmpty> - <EuiButton - onClick={() => { - if (!!searchName) { - addHistory( - sessionStorage.getItem('user_id'), - search, - searchName, - searchFields, - searchDescription, - setUserHistory - ); - setSearchName(''); - setSearchDescription(''); - closeSaveSearchModal(); - } - }} - fill - > - Save - </EuiButton> - </EuiModalFooter> - </EuiModal> - </EuiOverlayMask> - ); - } - - return ( - <> - {/*!readOnlyQuery ? - <> - <EuiCallOut title="Proceed with caution!" color="warning" iconType="alert"> - <p>Be aware that manually editing the query can spoil search results. The syntax must be respected :</p> - <ul>Fields and their values should be put between brackets : { } - Make sure every opened bracket is properly closed</ul> - <ul>"AND" and "OR" should be capitalized between different fields conditions and lowercased within a field expression</ul> - <ul>Make sure to check eventual typing mistakes</ul> - - </EuiCallOut> - <EuiSpacer size="s" /> - </> - : <></> - */} - <EuiFlexGroup> - <EuiFlexItem> - <EuiTextArea - readOnly={readOnlyQuery} - value={search} - onChange={(e) => setSearch(e.target.value)} - placeholder="Add fields..." - fullWidth - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - size="s" - fill - onClick={() => { - if (search.trim()) { - setIsLoading(true); - const queriesWithIndices = createAdvancedQueriesBySource( - standardFields, - search, - selectedSources, - availableSources - ); - searchQuery(queriesWithIndices).then((result) => { - // sessionStorage.setItem("searchResults", JSON.stringify(result)) - setSearchResults(result); - setSelectedTabNumber(1); - setIsLoading(false); - }); - } - }} - > - Search - </EuiButton> - <EuiSpacer size="s" /> - {isNaN(searchCount) ? ( - <></> - ) : ( - <> - <EuiTextColor - color="secondary" - style={{ display: 'flex', justifyContent: 'center' }} - > - {searchCount} {searchCount === 1 ? 'result' : 'results'} - </EuiTextColor> - <EuiSpacer size="s" /> - </> - )} - <EuiButton - size="s" - onClick={() => { - if (!!search) { - const queriesWithIndices = createAdvancedQueriesBySource( - standardFields, - search, - selectedSources, - availableSources - ); - getQueryCount(queriesWithIndices).then((result) => { - if (result || result === 0) setSearchCount(result); - }); - } - }} - > - Count results - </EuiButton> - <EuiSpacer size="s" /> - <EuiButton - size="s" - onClick={() => { - setIsSaveSearchModalOpen(true); - }} - > - Save search - </EuiButton> - {saveSearchModal} - <EuiSpacer size="s" /> - <EuiSwitch - compressed - label={'Editable'} - checked={!readOnlyQuery} - onChange={() => { - // switchReadOnly(readOnlyQuery, isReadOnlyModalOpen) - setReadOnlyQuery(!readOnlyQuery); - if (readOnlyQuery) { - createEditableQueryToast(); - } - }} - /> - {/* readOnlyModal */} - </EuiFlexItem> - </EuiFlexGroup> - {isLoading && ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiProgress postion="fixed" size="l" color="accent" /> - </EuiFlexItem> - </EuiFlexGroup> - )} - <EuiSpacer size="s" /> - <EuiFlexGroup> - <EuiFlexItem> - {HistorySelect( - sources, - setAvailableSources, - setSelectedSources, - setSearch, - searchFields, - selectedOperatorId, - userHistory, - setUserHistory, - setSearchFields, - setSearchCount, - setFieldCount, - selectedSavedSearch, - setSelectedSavedSearch, - historySelectError, - setHistorySelectError - )} - </EuiFlexItem> - </EuiFlexGroup> - </> - ); -}; - -const PopoverSelect = ( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - fieldCount, - setFieldCount, - selectedSources, - setSelectedSources -) => { - const handleAddfield = () => { - if (!!selectedField[0]) { - const field = standardFields.find( - (item) => - item.field_name.replace(/_|\./g, ' ') === - selectedSection[0].label + ' ' + selectedField[0].label - ); - switch (field.field_type) { - case 'Text': - setSearchFields([ - ...searchFields, - new SearchField(field.field_name, field.field_type, '', false, field.sources), - ]); - break; - case 'List': - setSearchFields([ - ...searchFields, - new SearchField(field.field_name, field.field_type, [], false, field.sources), - ]); - break; - default: - setSearchFields([ - ...searchFields, - new SearchField( - field.field_name, - field.field_type, - [{}], - false, - field.sources - ), - ]); - } - } - }; - - const selectField = () => { - const renderOption = (option, searchValue, contentClassName) => { - const { label, color } = option; - return <EuiHealth color={color}>{label}</EuiHealth>; - }; - if (selectedSection.length) { - return ( - <> - <EuiComboBox - placeholder="Select a field" - singleSelection={{ asPlainText: true }} - options={getFieldsBySection(standardFields, selectedSection[0])} - selectedOptions={selectedField} - onChange={(selected) => setSelectedField(selected)} - isClearable={true} - renderOption={renderOption} - /> - <EuiPopoverFooter> - <EuiButton - size="s" - onClick={() => { - handleAddfield(); - setIsPopoverSelectOpen(false); - setSelectedSection([]); - setSelectedField([]); - }} - > - Add this field - </EuiButton> - </EuiPopoverFooter> - </> - ); - } - }; - - return ( - <EuiPopover - panelPaddingSize="s" - button={ - <EuiButton - iconType="listAdd" - iconSide="left" - onClick={() => setIsPopoverSelectOpen(!isPopoverSelectOpen)} - > - Add field - </EuiButton> - } - isOpen={isPopoverSelectOpen} - closePopover={() => setIsPopoverSelectOpen(false)} - > - <div style={{ width: 'intrinsic', minWidth: 240 }}> - <EuiPopoverTitle>Select a field</EuiPopoverTitle> - <EuiComboBox - placeholder="Select a section" - singleSelection={{ asPlainText: true }} - options={getSections(standardFields)} - selectedOptions={selectedSection} - onChange={(selected) => { - setSelectedSection(selected); - setSelectedField([]); - }} - isClearable={false} - /> - </div> - {selectField()} - </EuiPopover> - ); -}; - -const PopoverValueContent = ( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - isPopoverValueOpen, - setIsPopoverValueOpen, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources -) => { - const onValueSearchChange = (value, hasMatchingOptions) => { - setValueError( - value.length === 0 || hasMatchingOptions - ? undefined - : `"${value}" is not a valid option` - ); - }; - - const validateFieldValues = () => { - let fieldValues; - if (Array.isArray(searchFields[index].values)) { - fieldValues = []; - searchFields[index].values.forEach((value) => { - if (!!value) { - fieldValues.push(value); - } - }); - } else { - fieldValues = searchFields[index].values; - } - - const updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - fieldValues, - true, - searchFields[index].sources - ) - ); - setSearchFields(updatedSearchFields); - updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); - setFieldCount(updateArrayElement(fieldCount, index)); - if (searchFields[index].sources.length) { - const filteredSources = []; - searchFields[index].sources.forEach((sourceId) => { - let source; - if (selectedSources.length) { - source = selectedSources.find((src) => src.id === sourceId); - } else { - source = availableSources.find((src) => src.id === sourceId); - } - if (source) { - filteredSources.push(source); - } - }); - setAvailableSources(filteredSources); - setSelectedSources(filteredSources); - createPolicyToast(); - } - }; - - const invalidateFieldValues = () => { - const updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - searchFields[index].values, - false, - searchFields[index].sources - ) - ); - setSearchFields(updatedSearchFields); - updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); - }; - - const ValuePopoverFooter = (i) => { - if (i === searchFields[index].values.length - 1) { - return ( - <EuiPopoverFooter> - <EuiButton - size="s" - onClick={() => { - setSearchFields( - updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - [...searchFields[index].values, {}], - false, - searchFields[index].sources - ) - ) - ); - }} - > - Add value - </EuiButton> - <EuiButton - size="s" - style={{ float: 'right' }} - onClick={() => { - validateFieldValues(); - setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)); - }} - > - Validate - </EuiButton> - </EuiPopoverFooter> - ); - } - }; - - const addFieldValue = (i, selectedOption) => { - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { option: selectedOption }) - ) - ); - }; - - const getListFieldValues = () => { - const listFieldValues = []; - standardFields - .find((item) => item.field_name === searchFields[index].name) - .values.split(', ') - .sort() - .forEach((element) => { - listFieldValues.push({ label: element }); - }); - return listFieldValues; - }; - - switch (searchFields[index].type) { - case 'Text': - return ( - <> - <EuiFlexItem> - <EuiFieldText - placeholder={'Type values'} - value={searchFields[index].values} - onChange={(e) => - setSearchFields( - updateSearchFieldValues(searchFields, index, e.target.value) - ) - } - /> - </EuiFlexItem> - <EuiPopoverFooter> - <EuiButton - size="s" - style={{ float: 'right' }} - onClick={() => { - validateFieldValues(); - setIsPopoverValueOpen( - updateArrayElement(isPopoverValueOpen, index, false) - ); - }} - > - Validate - </EuiButton> - </EuiPopoverFooter> - </> - ); - case 'List': - return ( - <> - <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> - <EuiComboBox - placeholder={'Select values'} - options={getListFieldValues()} - selectedOptions={searchFields[index].values} - onChange={(selectedOptions) => { - setValueError(undefined); - setSearchFields( - updateSearchFieldValues(searchFields, index, selectedOptions) - ); - }} - onSearchChange={onValueSearchChange} - /> - </EuiFormRow> - <EuiPopoverFooter> - <EuiButton - size="s" - style={{ float: 'right' }} - onClick={() => { - validateFieldValues(); - setIsPopoverValueOpen( - updateArrayElement(isPopoverValueOpen, index, false) - ); - }} - > - Validate - </EuiButton> - </EuiPopoverFooter> - </> - ); - - case 'Numeric': - const NumericValues = (i) => { - if (!!searchFields[index].values[i].option) { - switch (searchFields[index].values[i].option) { - case 'between': - return ( - <> - <EuiFlexItem> - <EuiFieldText - placeholder={'1st value'} - value={searchFields[index].values[i].value1} - onChange={(e) => { - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - value1: e.target.value, - value2: searchFields[index].values[i].value2, - }) - ) - ); - }} - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiFieldText - placeholder={'2nd value'} - value={searchFields[index].values[i].value2} - onChange={(e) => - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - value1: searchFields[index].values[i].value1, - value2: e.target.value, - }) - ) - ) - } - /> - </EuiFlexItem> - {ValuePopoverFooter(i)} - </> - ); - - default: - return ( - <> - <EuiFlexItem> - <EuiFieldText - placeholder={'Type value'} - value={searchFields[index].values[i].value1} - onChange={(e) => { - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - value1: e.target.value, - value2: searchFields[index].values[i].value2, - }) - ) - ); - }} - /> - </EuiFlexItem> - {ValuePopoverFooter(i)} - </> - ); - } - } - }; - - return ( - <> - {searchFields[index].values.map((value, i) => ( - <div key={i}> - <EuiSelect - hasNoInitialSelection - id="Select an option" - options={NumericOptions} - value={searchFields[index].values[i].option} - onChange={(e) => { - addFieldValue(i, e.target.value); - invalidateFieldValues(); - }} - /> - {NumericValues(i)} - </div> - ))} - </> - ); - - case 'Date': - const SelectDates = (i) => { - if (!!searchFields[index].values[i].option) { - switch (searchFields[index].values[i].option) { - case 'between': - return ( - <> - <form className={datePickerStyles.container} noValidate> - <TextField - label="between" - type="date" - defaultValue={ - !!searchFields[index].values[i].startDate - ? searchFields[index].values[i].startDate - : Date.now() - } - className={datePickerStyles.textField} - InputLabelProps={{ - shrink: true, - }} - onChange={(e) => - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - startDate: e.target.value, - endDate: searchFields[index].values[i].endDate, - }) - ) - ) - } - /> - </form> - <form className={datePickerStyles.container} noValidate> - <TextField - label="and" - type="date" - defaultValue={ - !!searchFields[index].values[i].endDate - ? searchFields[index].values[i].endDate - : Date.now() - } - className={datePickerStyles.textField} - InputLabelProps={{ - shrink: true, - }} - onChange={(e) => - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - startDate: searchFields[index].values[i].startDate, - endDate: e.target.value, - }) - ) - ) - } - /> - </form> - {ValuePopoverFooter(i)} - </> - ); - - default: - return ( - <> - <form className={datePickerStyles.container} noValidate> - <TextField - type="date" - defaultValue={ - !!searchFields[index].values[i].startDate - ? searchFields[index].values[i].startDate - : Date.now() - } - className={datePickerStyles.textField} - InputLabelProps={{ - shrink: true, - }} - onChange={(e) => - setSearchFields( - updateSearchFieldValues( - searchFields, - index, - updateArrayElement(searchFields[index].values, i, { - option: searchFields[index].values[i].option, - startDate: e.target.value, - endDate: Date.now(), - }) - ) - ) - } - /> - </form> - {ValuePopoverFooter(i)} - </> - ); - } - } - }; - - return ( - <> - {searchFields[index].values.map((value, i) => ( - <div key={i}> - <EuiSelect - hasNoInitialSelection - id="Select an option" - options={DateOptions} - value={searchFields[index].values[i].option} - onChange={(e) => { - addFieldValue(i, e.target.value); - invalidateFieldValues(); - }} - /> - {SelectDates(i)} - </div> - ))} - </> - ); - default: - } -}; - -const PopoverValueButton = ( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources -) => { - return ( - <EuiPopover - panelPaddingSize="s" - button={ - <EuiButtonIcon - size="s" - color="primary" - onClick={() => - setIsPopoverValueOpen( - updateArrayElement(isPopoverValueOpen, index, !isPopoverValueOpen[index]) - ) - } - iconType="documentEdit" - title="Give field values" - /> - } - isOpen={isPopoverValueOpen[index]} - closePopover={() => - setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false)) - } - > - {/*<div style={{ width: 240 }}> - <EuiButtonIcon - size="s" - style={{ float: 'right' }} - color="danger" - onClick={() => setIsPopoverValueOpen(updateArrayElement(isPopoverValueOpen, index, false))} - iconType="crossInACircleFilled" - title="Close popover" - /> - </div>*/} - <div style={{ width: 240 }}> - {PopoverValueContent( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - isPopoverValueOpen, - setIsPopoverValueOpen, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources - )} - </div> - </EuiPopover> - ); -}; - -const FieldsPanel = ( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - selectedOperatorId, - setSelectedOperatorId, - fieldCount, - setFieldCount, - availableSources, - setAvailableSources, - selectedSources, - setSelectedSources, - sources, - datePickerStyles, - createPolicyToast -) => { - const countFieldValues = (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 handleRemoveField = (index) => { - const updatedSearchFields = removeArrayElement(searchFields, index); - setSearchFields(updatedSearchFields); - updateSources(updatedSearchFields, sources, setSelectedSources, setAvailableSources); - updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); - }; - - const handleClearValues = (index) => { - let updatedSearchFields = []; - switch (searchFields[index].type) { - case 'Text': - updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - '', - false, - searchFields[index].sources - ) - ); - break; - case 'List': - updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - [], - false, - searchFields[index].sources - ) - ); - break; - default: - updatedSearchFields = updateArrayElement( - searchFields, - index, - new SearchField( - searchFields[index].name, - searchFields[index].type, - [{}], - false, - searchFields[index].sources - ) - ); - } - setSearchFields(updatedSearchFields); - updateSources(updatedSearchFields, sources, setSelectedSources, setAvailableSources); - setFieldCount(updateArrayElement(fieldCount, index)); - updateSearch(setSearch, updatedSearchFields, selectedOperatorId, setSearchCount); - }; - - if (standardFields === []) { - return <h2>Loading user fields...</h2>; - } - - return ( - <> - <EuiTitle size="xs"> - <h2>Field search</h2> - </EuiTitle> - <EuiPanel paddingSize="m"> - <EuiFlexGroup direction="column"> - {searchFields.map((field, index) => ( - <EuiPanel key={'field' + index} paddingSize="s"> - <EuiFlexItem grow={false}> - <EuiFlexGroup direction="row" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiButtonIcon - size="s" - color="danger" - onClick={() => handleRemoveField(index)} - iconType="indexClose" - title="Remove field" - /> - </EuiFlexItem> - <EuiFlexItem> - {field.isValidated ? ( - <> - {field.sources.length ? ( - <EuiHealth color="danger"> - {fieldValuesToString(field).replace(/_|\./g, ' ')} - </EuiHealth> - ) : ( - <EuiHealth color="primary"> - {fieldValuesToString(field).replace(/_|\./g, ' ')} - </EuiHealth> - )} - </> - ) : ( - <> - {field.sources.length ? ( - <EuiHealth color="danger"> - {field.name.replace(/_|\./g, ' ')} - </EuiHealth> - ) : ( - <EuiHealth color="primary"> - {field.name.replace(/_|\./g, ' ')} - </EuiHealth> - )} - </> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {isNaN(fieldCount[index]) ? ( - <></> - ) : ( - <> - <EuiTextColor color="secondary"> - {fieldCount[index]}{' '} - {fieldCount[index] === 1 ? 'result' : 'results'} - </EuiTextColor> - </> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {field.isValidated ? ( - <> - <EuiButtonIcon - size="s" - onClick={() => countFieldValues(field, index)} - iconType="number" - title="Count results" - /> - </> - ) : ( - <></> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {field.isValidated ? ( - <> - <EuiButtonIcon - size="s" - color="danger" - onClick={() => handleClearValues(index)} - iconType="trash" - title="Clear values" - /> - </> - ) : ( - <></> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - {PopoverValueButton( - index, - standardFields, - setStandardFields, - searchFields, - setSearchFields, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - fieldCount, - setFieldCount, - selectedOperatorId, - datePickerStyles, - createPolicyToast, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources - )} - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiPanel> - ))} - </EuiFlexGroup> - <EuiSpacer size="l" /> - {PopoverSelect( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - fieldCount, - setFieldCount, - selectedSources, - setSelectedSources - )} - </EuiPanel> - <EuiSpacer size="s" /> - <EuiRadioGroup - options={Operators} - idSelected={selectedOperatorId} - onChange={(id) => { - setSelectedOperatorId(id); - updateSearch(setSearch, searchFields, id, setSearchCount); - }} - name="operators group" - legend={{ - children: <span>Search option</span>, - }} - /> - </> - ); -}; - -const SourceSelect = ( - availableSources, - selectedSources, - setSelectedSources, - sourceSelectError, - setSourceSelectError -) => { - if (Object.keys(availableSources).length !== 0) { - availableSources.forEach((source) => { - if (source.name) { - source = changeNameToLabel(source); - } - }); - - const onSourceChange = (selectedOptions) => { - setSourceSelectError(undefined); - setSelectedSources(selectedOptions); - }; - - const onSourceSearchChange = (value, hasMatchingOptions) => { - setSourceSelectError( - value.length === 0 || hasMatchingOptions - ? undefined - : `"${value}" is not a valid option` - ); - }; - return ( - <> - <EuiTitle size="xs"> - <h2>Partner sources</h2> - </EuiTitle> - <EuiSpacer size="s" /> - <EuiFlexItem> - <EuiFormRow - error={sourceSelectError} - isInvalid={sourceSelectError !== undefined} - > - <EuiComboBox - placeholder="By default, all sources are selected" - options={availableSources} - selectedOptions={selectedSources} - onChange={onSourceChange} - onSearchChange={onSourceSearchChange} - /> - </EuiFormRow> - </EuiFlexItem> - </> - ); - } else { - return ( - <p> - <EuiIcon type="alert" color="danger" /> No source available ! - </p> - ); - } -}; +import { useTranslation } from 'react-i18next'; +import AdvancedSearch from './AdvancedSearch/AdvancedSearch'; +import BasicSearch from './BasicSearch/BasicSearch'; +import styles from './styles'; const Search = () => { - const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation('search'); + const datePickerStyles = styles(); const [selectedTabNumber, setSelectedTabNumber] = useState(0); - const [userHistory, setUserHistory] = useState({}); - const [advancedSearch, setAdvancedSearch] = useState(false); + const [isAdvancedSearch, setIsAdvancedSearch] = useState(false); const [readOnlyQuery, setReadOnlyQuery] = useState(true); const [selectedField, setSelectedField] = useState([]); const [selectedSection, setSelectedSection] = useState([]); @@ -1543,9 +52,6 @@ const Search = () => { const [isReadOnlyModalOpen, setIsReadOnlyModalOpen] = useState(false); const [isSaveSearchModalOpen, setIsSaveSearchModalOpen] = useState(false); const [selectedSavedSearch, setSelectedSavedSearch] = useState(); - const [historySelectError, setHistorySelectError] = useState(undefined); - const [notificationToasts, setNotificationToasts] = useState([]); - const datePickerStyles = useStyles(); useEffect(() => { fetchPublicFields().then((resultStdFields) => { @@ -1576,262 +82,106 @@ const Search = () => { setStandardFields(removeNullFields(userFields)); } ); - - // policyField => { - // policyField.forEach( }); fetchSources(sessionStorage.getItem('user_id')).then((result) => { setSources(result); setAvailableSources(result); }); - fetchHistory(setUserHistory); }, []); - const createPolicyToast = () => { - const toast = { - title: 'Policy field selected', - color: 'warning', - iconType: 'alert', - toastLifeTimeMs: 15000, - text: ( - <> - <p>You selected a private field.</p> - <p> - Access to this field was granted for specific sources, which means that your - search will be restricted to those. - </p> - <p>Please check the sources list before searching.</p> - </> - ), - }; - setNotificationToasts(notificationToasts.concat(toast)); - }; - - const createEditableQueryToast = () => { - const toast = { - title: 'Proceed with caution', - color: 'warning', - iconType: 'alert', - toastLifeTimeMs: 15000, - text: ( - <> - <p> - Be aware that manually editing the query can spoil search results. The syntax - must be respected : - </p> - <ul> - Fields and their values should be put between brackets : { } - Make - sure every opened bracket is properly closed - </ul> - <ul> - "AND" and "OR" should be capitalized between different fields conditions and - lowercased within a field expression - </ul> - <ul>Make sure to check eventual typing mistakes</ul> - </> - ), - }; - setNotificationToasts(notificationToasts.concat(toast)); - }; - - const removeToast = (removedToast) => { - setNotificationToasts( - notificationToasts.filter((toast) => toast.id !== removedToast.id) - ); - }; - - const onFormSubmit = () => { - setIsLoading(true); - const queriesWithIndices = createBasicQueriesBySource( - standardFields, - basicSearch, - selectedSources, - availableSources - ); - searchQuery(queriesWithIndices).then((result) => { - setSearchResults(result); - setSelectedTabNumber(1); - setIsLoading(false); - }); - }; - const tabsContent = [ { id: 'tab1', - name: 'Compose search', + name: t('tabs.composeSearch'), content: ( <> - {advancedSearch ? ( - <> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiSpacer size="s" /> - <EuiButtonEmpty - onClick={() => { - setAdvancedSearch(!advancedSearch); - }} - > - Switch to basic search - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - <EuiSpacer size="s" /> - {SearchBar( - isLoading, - setIsLoading, - search, - setSearch, - searchResults, - setSearchResults, - searchFields, - setSearchFields, - searchName, - setSearchName, - searchDescription, - setSearchDescription, - readOnlyQuery, - setReadOnlyQuery, - selectedSources, - setSelectedSources, - availableSources, - setAvailableSources, - standardFields, - sources, - setSelectedTabNumber, - searchCount, - setSearchCount, - setFieldCount, - isReadOnlyModalOpen, - setIsReadOnlyModalOpen, - isSaveSearchModalOpen, - setIsSaveSearchModalOpen, - userHistory, - setUserHistory, - selectedSavedSearch, - setSelectedSavedSearch, - historySelectError, - setHistorySelectError, - selectedOperatorId, - createEditableQueryToast - )} - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - <EuiSpacer size="s" /> - {FieldsPanel( - standardFields, - setStandardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, - setSearch, - setSearchCount, - selectedOperatorId, - setSelectedOperatorId, - fieldCount, - setFieldCount, - availableSources, - setAvailableSources, - selectedSources, - setSelectedSources, - sources, - datePickerStyles, - createPolicyToast - )} - <EuiSpacer size="s" /> - {SourceSelect( - availableSources, - selectedSources, - setSelectedSources, - sourceSelectError, - setSourceSelectError - )} - </EuiFlexItem> - </EuiFlexGroup> - <EuiGlobalToastList - toasts={notificationToasts} - dismissToast={removeToast} - toastLifeTimeMs={2500} - /> - </> + {isAdvancedSearch ? ( + <AdvancedSearch + search={search} + setSearch={setSearch} + searchResults={searchResults} + setSearchResults={setSearchResults} + searchFields={searchFields} + setSearchFields={setSearchFields} + searchName={searchName} + setSearchName={setSearchName} + searchDescription={searchDescription} + setSearchDescription={setSearchDescription} + readOnlyQuery={readOnlyQuery} + setReadOnlyQuery={setReadOnlyQuery} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + standardFields={standardFields} + setStandardFields={setStandardFields} + sources={sources} + setSelectedTabNumber={setSelectedTabNumber} + searchCount={searchCount} + setSearchCount={setSearchCount} + setFieldCount={setFieldCount} + isReadOnlyModalOpen={isReadOnlyModalOpen} + setIsReadOnlyModalOpen={setIsReadOnlyModalOpen} + isSaveSearchModalOpen={isSaveSearchModalOpen} + setIsSaveSearchModalOpen={setIsSaveSearchModalOpen} + selectedSavedSearch={selectedSavedSearch} + setSelectedSavedSearch={setSelectedSavedSearch} + selectedOperatorId={selectedOperatorId} + setIsAdvancedSearch={setIsAdvancedSearch} + isAdvancedSearch={isAdvancedSearch} + selectedField={selectedField} + selectedSection={selectedSection} + setSelectedField={setSelectedField} + setSelectedSection={setSelectedSection} + isPopoverSelectOpen={isPopoverSelectOpen} + setIsPopoverSelectOpen={setIsPopoverSelectOpen} + setIsPopoverValueOpen={setIsPopoverValueOpen} + isPopoverValueOpen={isPopoverValueOpen} + valueError={valueError} + setValueError={setValueError} + setSelectedOperatorId={setSelectedOperatorId} + fieldCount={fieldCount} + sourceSelectError={sourceSelectError} + datePickerStyles={datePickerStyles} + setSourceSelectError={setSourceSelectError} + /> ) : ( - <> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiSpacer size="s" /> - <EuiButtonEmpty - onClick={() => { - setAdvancedSearch(!advancedSearch); - }} - > - Switch to advanced search - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - <EuiSpacer size="s" /> - <form onSubmit={onFormSubmit}> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFieldSearch - value={basicSearch} - onChange={(e) => setBasicSearch(e.target.value)} - placeholder="Search..." - fullWidth - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton type="submit" fill isDisabled={advancedSearch}> - Search - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </form> - {isLoading && ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiProgress postion="fixed" size="l" color="accent" /> - </EuiFlexItem> - </EuiFlexGroup> - )} - </EuiFlexItem> - </EuiFlexGroup> - </> + <BasicSearch + isAdvancedSearch={isAdvancedSearch} + setIsAdvancedSearch={setIsAdvancedSearch} + standardFields={standardFields} + availableSources={availableSources} + selectedSources={selectedSources} + basicSearch={basicSearch} + setBasicSearch={setBasicSearch} + setSearchResults={setSearchResults} + setSelectedTabNumber={setSelectedTabNumber} + /> )} </> ), }, { id: 'tab3', - name: 'Results', + name: t('tabs.results'), content: ( <EuiFlexGroup> - <EuiFlexItem>{Results(searchResults, search, basicSearch)}</EuiFlexItem> + <EuiFlexItem> + <Results + searchResults={searchResults} + searchQuery={isAdvancedSearch ? search : basicSearch} + /> + </EuiFlexItem> </EuiFlexGroup> ), }, { id: 'tab2', - name: 'Map', + name: t('tabs.map'), content: ( <EuiFlexGroup> <EuiFlexItem> <EuiSpacer size="l" /> - {/*<a href="https://agroenvgeo.data.inra.fr/mapfishapp/"><img src={map} width="460" height="400" alt='Map' /></a>*/} <SearchMap searchResults={searchResults} /> </EuiFlexItem> </EuiFlexGroup> @@ -1842,12 +192,10 @@ const Search = () => { return ( <> <EuiPageContent> - {' '} - {/*style={{ backgroundColor: "#fafafa" }}*/} <EuiPageContentHeader> <EuiPageContentHeaderSection> <EuiTitle> - <h2>In-Sylva Metadata Search Platform</h2> + <h2>{t('pageTitle')}</h2> </EuiTitle> </EuiPageContentHeaderSection> </EuiPageContentHeader> diff --git a/src/pages/search/styles.js b/src/pages/search/styles.js new file mode 100644 index 0000000000000000000000000000000000000000..66022dc748fea12084c48d6a7c6fc471b22dce14 --- /dev/null +++ b/src/pages/search/styles.js @@ -0,0 +1,15 @@ +import { makeStyles } from '@material-ui/core/styles'; + +const style = makeStyles((theme) => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + textField: { + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + width: 240, + }, +})); + +export default style;