diff --git a/public/locales/en/results.json b/public/locales/en/results.json index 61cf048cef51645529f6353f5e08fde90b74541a..baaaa092998cf918fd7cbb01b758a76a4cc257f6 100644 --- a/public/locales/en/results.json +++ b/public/locales/en/results.json @@ -1,10 +1,47 @@ { - "yourQuery": "Your query: {{query}}", - "clickOnRowTip": "Click on a resource row to display full metadata.", + "clickOnRowTip": "Click on a row to display metadata.", "downloadResultsButton": { "JSON": "Download as JSON" }, "table": { - "title": "Search results" + "title": "Search results from query: <strong>{{searchQuery}}</strong>", + "search": "Search among results", + "displaySelectedRowsButton": "Sort selection", + "textLabels": { + "body": { + "noMatch": "No matching records found", + "toolTip": "Sort" + }, + "pagination": { + "next": "Next Page", + "previous": "Previous Page", + "rowsPerPage": "Rows per page:", + "displayRows": "of", + "jumpToPage": "Page:" + }, + "toolbar": { + "search": "Search", + "viewColumns": "View Columns", + "filterTable": "Filter Table" + }, + "filter": { + "all": "All", + "title": "FILTERS", + "reset": "RESET" + }, + "viewColumns": { + "title": "Show Columns", + "titleAria": "Show/Hide Table Columns" + }, + "selectedRows": { + "text": "row(s) selected" + } + } + }, + "flyout": { + "label": "Resource data sheet", + "JSON": { + "title": "Resource JSON data" + } } } diff --git a/public/locales/fr/results.json b/public/locales/fr/results.json index e611299744c96fb46abb224d811621c4df0bd5e8..1237c231591e493969bca0aa7d0b3451f9ea6db2 100644 --- a/public/locales/fr/results.json +++ b/public/locales/fr/results.json @@ -1,10 +1,47 @@ { - "yourQuery": "Votre requête : {{query}}", - "clickOnRowTip": "Clickez sur une ressource (ligne du tableau) pour afficher ses métadonnées.", + "clickOnRowTip": "Clickez sur une ligne du tableau pour afficher ses métadonnées.", "downloadResultsButton": { "JSON": "Télécharger en JSON" }, "table": { - "title": "Résultats de la recherche" + "title": "Résultats de la requête : <strong>{{searchQuery}}</strong>", + "search": "Chercher parmi les résultats", + "displaySelectedRowsButton": "Trier la sélection", + "textLabels": { + "body": { + "noMatch": "Aucun résultat pour cette requête", + "toolTip": "Trier" + }, + "pagination": { + "next": "Page suivante", + "previous": "Page précédente", + "rowsPerPage": "Lignes par page :", + "displayRows": "sur", + "jumpToPage": "Page :" + }, + "toolbar": { + "search": "Rechercher", + "viewColumns": "Visualisation des colonnes", + "filterTable": "Filtrer les résultats" + }, + "filter": { + "all": "Tout", + "title": "FILTRES", + "reset": "REINITIALISER" + }, + "viewColumns": { + "title": "Visualisation des colonnes", + "titleAria": "Montrer/cacher les colonnes" + }, + "selectedRows": { + "text": "ligne(s) sélectionnées" + } + } + }, + "flyout": { + "label": "Fiche de données de la ressource", + "JSON": { + "title": "Données JSON de la ressource" + } } } diff --git a/src/App.js b/src/App.js index b73433f8138b04bf0f5d38a783fbf2a95c48970a..5d03570a71be6a36aa411c1f5608a974927660ad 100644 --- a/src/App.js +++ b/src/App.js @@ -5,13 +5,12 @@ import Layout from './components/Layout'; const App = () => { return ( - <EuiPage restrictWidth={false}> + <EuiPage style={{ padding: '0px' }} restrictWidth={false}> <EuiPageBody> <HashRouter> <Switch> <Route exact path="/" render={() => <Redirect to="/app/home" />} /> <Route component={Layout} /> - <Route component={Error} /> </Switch> </HashRouter> </EuiPageBody> diff --git a/src/Utils.js b/src/Utils.js index d4efd75995e9f25e9f4b5922ace7541d566ba82a..3c4f4727b2209ff47dc4505960004c258bdf839a 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -594,8 +594,8 @@ export const createBasicQueriesBySource = ( sourcesLists.forEach((sourcesArray, index) => { let sourceParam = `"_source": [`; - fieldsLists[index].forEach((fieldname) => { - sourceParam = `${sourceParam} "${fieldname}", `; + fieldsLists[index].forEach((fieldName) => { + sourceParam = `${sourceParam} "${fieldName}", `; }); if (sourceParam.endsWith(', ')) { sourceParam = sourceParam.substring(0, sourceParam.length - 2); diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index d44347a8ca84f3c432f0bff0277ad98eaf33f542..6e571d618929954064d06861039b3b0639c4b278 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -36,13 +36,7 @@ const Header = () => { <EuiHeader> <EuiHeaderSection grow={true}> <EuiHeaderSectionItem> - <img - style={style.logo} - src={logoInSylva} - width="75" - height="45" - alt={t('common:inSylvaLogoAlt')} - /> + <img style={style.logo} src={logoInSylva} alt={t('common:inSylvaLogoAlt')} /> </EuiHeaderSectionItem> <EuiHeaderLinks> {structure.map((link) => ( diff --git a/src/components/Header/styles.js b/src/components/Header/styles.js index 27b176118e227f03aede6c0cc94d7938e23266af..94f8ecf22cfa183f26e33b3f3c7ae6086c4730ee 100644 --- a/src/components/Header/styles.js +++ b/src/components/Header/styles.js @@ -1,9 +1,8 @@ const headerStyle = { logo: { - paddingTop: '10px', - paddingBottom: '-6px', - paddingRight: '10px', - paddingLeft: '10px', + width: '75px', + height: '75px', + padding: '10px', }, languageSwitcherItem: { margin: '10px', diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js index ddfcb8639542d0484050318d8aa67f6103a496a2..af5be085de322c47d3c135e68cab48602f40bc23 100644 --- a/src/components/Layout/Layout.js +++ b/src/components/Layout/Layout.js @@ -4,16 +4,20 @@ import Header from '../../components/Header'; import Search from '../../pages/search'; import Home from '../../pages/home'; import Profile from '../../pages/profile'; +import { EuiPageContent } from '@elastic/eui'; +import styles from './styles.js'; const Layout = () => { return ( <> <Header /> - <Switch> - <Route path="/app/home" component={Home} /> - <Route path="/app/search" component={Search} /> - <Route path="/app/profile" component={Profile} /> - </Switch> + <EuiPageContent style={styles.pageContent}> + <Switch> + <Route path="/app/home" component={Home} /> + <Route path="/app/search" component={Search} /> + <Route path="/app/profile" component={Profile} /> + </Switch> + </EuiPageContent> </> ); }; diff --git a/src/components/Layout/styles.js b/src/components/Layout/styles.js new file mode 100644 index 0000000000000000000000000000000000000000..787354eaa2957de1cc981112fa072887a75d71ca --- /dev/null +++ b/src/components/Layout/styles.js @@ -0,0 +1,9 @@ +const styles = { + pageContent: { + borderRadius: 0, + border: 0, + boxShadow: 'none', + }, +}; + +export default styles; diff --git a/src/context/InSylvaKeycloakClient.js b/src/context/InSylvaKeycloakClient.js index c5980429eac474a69ea0cfb484c05fcc6cb3e91c..5a20fd65b1658b6b64c5bffe202cbbdaa517a61f 100644 --- a/src/context/InSylvaKeycloakClient.js +++ b/src/context/InSylvaKeycloakClient.js @@ -2,17 +2,11 @@ import { getLoginUrl } from '../Utils'; class InSylvaKeycloakClient { async post(path, requestContent) { - // const access_token = sessionStorage.getItem("access_token"); const headers = { 'Content-Type': 'application/x-www-form-urlencoded', // "Access-Control-Allow-Methods": "GET,HEAD,PUT,PATCH,POST,DELETE", // "Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization" }; - /* - if (access_token) { - headers["Authorization"] = "Bearer " + access_token - } - */ let formBody = []; for (const property in requestContent) { const encodedKey = encodeURIComponent(property); @@ -26,18 +20,14 @@ class InSylvaKeycloakClient { body: formBody, mode: 'cors', }); - if (response.ok === true) { - // ok - } else { + if (!response.ok) { await this.logout(); sessionStorage.removeItem('user_id'); sessionStorage.removeItem('access_token'); sessionStorage.removeItem('refresh_token'); window.location.replace(getLoginUrl() + '?requestType=search'); } - if (response.statusText === 'No Content') { - // ok - } else { + if (response.statusText !== 'No Content') { return await response.json(); } } @@ -58,11 +48,11 @@ class InSylvaKeycloakClient { return { token }; } - async logout({ realm = this.realm, client_id = this.client_id }) { + async logout() { const refresh_token = sessionStorage.getItem('refresh_token'); - const path = `/auth/realms/${realm}/protocol/openid-connect/logout`; if (refresh_token) { - await this.post(`${path}`, { + const client_id = this.client_id; + await this.post(`/auth/realms/${this.realm}/protocol/openid-connect/logout`, { client_id, refresh_token, }); diff --git a/src/context/UserContext.js b/src/context/UserContext.js index 2d7b83dda5f22f5b484641bbebde7a92a2804728..d7578beb7e379f791fa08656d458dcb272e4193c 100644 --- a/src/context/UserContext.js +++ b/src/context/UserContext.js @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { createContext, useContext, useReducer } from 'react'; import { InSylvaGatekeeperClient } from './InSylvaGatekeeperClient'; import { InSylvaKeycloakClient } from './InSylvaKeycloakClient'; import { getLoginUrl } from '../Utils'; -const UserStateContext = React.createContext(); -const UserDispatchContext = React.createContext(); +const UserStateContext = createContext(null); +const UserDispatchContext = createContext(null); const igClient = new InSylvaGatekeeperClient(); igClient.baseUrl = process.env.REACT_APP_IN_SYLVA_GATEKEEPER_PORT @@ -46,7 +46,7 @@ function userReducer(state, action) { } function UserProvider({ children }) { - const [state, dispatch] = React.useReducer(userReducer, { + const [state, dispatch] = useReducer(userReducer, { isAuthenticated: !!sessionStorage.getItem('access_token'), }); @@ -60,7 +60,7 @@ function UserProvider({ children }) { } function useUserState() { - const context = React.useContext(UserStateContext); + const context = useContext(UserStateContext); if (context === undefined) { throw new Error('useUserState must be used within a UserProvider'); } @@ -68,7 +68,7 @@ function useUserState() { } function useUserDispatch() { - const context = React.useContext(UserDispatchContext); + const context = useContext(UserDispatchContext); if (context === undefined) { throw new Error('useUserDispatch must be used within a UserProvider'); } @@ -85,7 +85,7 @@ async function checkUserLogin(userId, accessToken, refreshToken) { // Load the user result filters from Result_Filter(userId) endpoints // Load the user policies from Policy(userId) endpoint if (!sessionStorage.getItem('token_refresh_time')) { - sessionStorage.setItem('token_refresh_time', Date.now()); + sessionStorage.setItem('token_refresh_time', Date.now().toString()); } // dispatch({ type: "USER_LOGGED_IN" }); } else { @@ -101,7 +101,7 @@ async function refreshToken() { }); if (result) { sessionStorage.setItem('access_token', result.token.access_token); - sessionStorage.setItem('token_refresh_time', Date.now()); + sessionStorage.setItem('token_refresh_time', Date.now().toString()); // dispatch({ type: "USER_LOGGED_IN" }); } else { // dispatch({ type: "LOGIN_FAILURE" }); @@ -113,7 +113,7 @@ async function refreshToken() { } async function signOut() { - await ikcClient.logout({}); + await ikcClient.logout(); sessionStorage.removeItem('user_id'); sessionStorage.removeItem('access_token'); sessionStorage.removeItem('refresh_token'); diff --git a/src/pages/home/Home.js b/src/pages/home/Home.js index 28040d15303ee4a05db3c9a67c483022dbea0a0f..1be5b26d3dc9ecbb638f16d2ee895aedd5a85722 100644 --- a/src/pages/home/Home.js +++ b/src/pages/home/Home.js @@ -1,6 +1,5 @@ import React from 'react'; import { - EuiPageContent, EuiPageContentHeader, EuiPageContentHeaderSection, EuiTitle, @@ -11,29 +10,25 @@ const Home = () => { const { t } = useTranslation('home'); return ( - <> - <EuiPageContent> - <EuiPageContentHeader> - <EuiPageContentHeaderSection> - <EuiTitle> - <h2>{t('pageTitle')}</h2> - </EuiTitle> - <br /> - <p>{t('searchToolDescription.part1')}</p> - <br /> - <p>{t('searchToolDescription.part2')}</p> - <br /> - <p>{t('searchToolDescription.part3')}</p> - <br /> - <p>{t('searchToolDescription.part4')}</p> - <br /> - <p>{t('searchToolDescription.part5')}</p> - <br /> - <p>{t('searchToolDescription.part6')}</p> - </EuiPageContentHeaderSection> - </EuiPageContentHeader> - </EuiPageContent> - </> + <EuiPageContentHeader> + <EuiPageContentHeaderSection> + <EuiTitle> + <h2>{t('pageTitle')}</h2> + </EuiTitle> + <br /> + <p>{t('searchToolDescription.part1')}</p> + <br /> + <p>{t('searchToolDescription.part2')}</p> + <br /> + <p>{t('searchToolDescription.part3')}</p> + <br /> + <p>{t('searchToolDescription.part4')}</p> + <br /> + <p>{t('searchToolDescription.part5')}</p> + <br /> + <p>{t('searchToolDescription.part6')}</p> + </EuiPageContentHeaderSection> + </EuiPageContentHeader> ); }; diff --git a/src/pages/maps/SearchMap.js b/src/pages/maps/SearchMap.js index 9f5fe8040bcac8d953d6d76064a8fdfe5fe39161..51d6693e80b4d0c5998ffebf61efbc0ae29fef81 100644 --- a/src/pages/maps/SearchMap.js +++ b/src/pages/maps/SearchMap.js @@ -88,7 +88,7 @@ const pointBaseStyle = new Style({ }), }); -const SearchMap = ({ searchResults }) => { +const SearchMap = ({ searchResults, selectedPointsIds, setSelectedPointsIds }) => { const { t } = useTranslation('maps'); // ref for handling zoomHelperText display const timerRef = useRef(null); @@ -104,7 +104,6 @@ const SearchMap = ({ searchResults }) => { { label: t('maps:layersTable.sylvoEcoRegions'), value: 'sylvoEcoRegions' }, ]; const [selectedFilterOptions, setSelectedFilterOptions] = useState([filterOptions[0]]); - const resultPointsSource = new VectorSource({}); const [zoomHelperTextFeature, setZoomHelperTextFeature] = useState( new Feature({ geometry: new Point(center), @@ -135,6 +134,7 @@ const SearchMap = ({ searchResults }) => { selectedPointsLayer: new VectorLayer({ name: 'selectedPointsSource', source: new VectorSource({}), + visible: true, }), regions: new VectorLayer({ name: 'regions', @@ -202,7 +202,7 @@ const SearchMap = ({ searchResults }) => { new VectorLayer({ name: 'queryResults', visible: true, - source: resultPointsSource, + source: new VectorSource({}), }), mapFilters['selectedPointsLayer'], new VectorLayer({ @@ -232,6 +232,7 @@ const SearchMap = ({ searchResults }) => { const initMapLayersVisibility = () => { let initialMapLayersVisibility = new Array(mapLayers.length).fill(false); initialMapLayersVisibility[getLayerIndex('osm-layer')] = true; + initialMapLayersVisibility[getLayerIndex('selectedPointsSource')] = true; initialMapLayersVisibility[getLayerIndex('queryResults')] = true; return initialMapLayersVisibility; }; @@ -334,8 +335,14 @@ const SearchMap = ({ searchResults }) => { // Create event listeners for dragBox when it changes useEffect(() => { - dragBox.on('boxstart', onBoxStartCallback); - dragBox.on('boxend', onBoxEndCallback); + // Remove previous DragBox instances before adding the new one + map.getInteractions().forEach((interaction) => { + if (interaction?.box_?.element_?.className === 'ol-box ol-dragbox') { + map.removeInteraction(interaction); + } + }); + dragBox.on('boxstart', () => onBoxStartCallback()); + dragBox.on('boxend', () => onBoxEndCallback()); map.addInteraction(dragBox); }, [dragBox]); @@ -378,23 +385,26 @@ const SearchMap = ({ searchResults }) => { } }, [selectedFilterOptions]); - const createPointFeature = (name, coordinates, radius, isSelected) => { + // pointData.id set to 0 means that the point is a selected point + const createPointFeature = (pointData, coordinates, radius, isSelected) => { const pointFeature = new Feature({ geometry: new Circle(coordinates, radius), }); - pointFeature.set('nom', name); + pointFeature.set('id', pointData.id); + pointFeature.set('nom', pointData.name); pointFeature.set('isSelected', isSelected); pointFeature.setStyle(pointStyle); return pointFeature; }; - // On new selection start, remove all previously selected features + // On new selection start, unselect features const onBoxStartCallback = () => { clearSelectedFeatures(); }; // On new selection end, add all previously selected features const onBoxEndCallback = () => { + let newSelectedPointsIds = selectedPointsIds; const boxExtent = dragBox.getGeometry().getExtent(); // if the extent crosses the antimeridian process each world separately const worldExtent = map.getView().getProjection().getExtent(); @@ -432,10 +442,21 @@ const SearchMap = ({ searchResults }) => { const coords = pointGeom.getCenter(); if (polygonGeometry.intersectsCoordinate(coords)) { const pointName = pointsFeatures[point].get('nom'); + const pointId = pointsFeatures[point].get('id'); if (!getSelectedPointsNames(selectedPointsSource).includes(pointName)) { // Add new selected features - const pointFeature = createPointFeature(pointName, coords, 5000, true); + const pointFeature = createPointFeature( + { + id: 0, + name: pointName, + }, + coords, + 5000, + true + ); selectedPointsSource.addFeature(pointFeature); + // Add point id to selected points Ids + newSelectedPointsIds.push(pointId); } } } @@ -449,16 +470,22 @@ const SearchMap = ({ searchResults }) => { if (polygonGeometry.intersectsCoordinate(coords)) { // Remove previously selected features selectedPointsSource.removeFeature(selectedPointsFeatures[point]); + newSelectedPointsIds = newSelectedPointsIds.splice( + newSelectedPointsIds.indexOf(selectedPointsFeatures[point].get('id')), + 1 + ); } } } } } // Update selected features list + setSelectedPointsIds(newSelectedPointsIds); if (!selectionToolMode) { let tmpSelectedFeatures = selectedFeatures; setSelectedFeatures(null); tmpSelectedFeatures.extend(boxFeatures); + console.log(tmpSelectedFeatures); setSelectedFeatures(tmpSelectedFeatures); } else { clearSelectedFeatures(); @@ -466,20 +493,23 @@ const SearchMap = ({ searchResults }) => { } }; - // Create points from search results const processData = () => { if (!searchResults) { return; } + const resultPointsSource = map + .getLayers() + .item(getLayerIndex('queryResults')) + .getSource(); + // Create points from search results searchResults.forEach((result) => { const geoPoint = result.experimental_site.geo_point; if (geoPoint && geoPoint.longitude && geoPoint.latitude) { - const pointIdentifier = - result.resource.identifier + - '--' + - result.context.related_experimental_network_title; const pointFeature = createPointFeature( - pointIdentifier, + { + id: result.id, + name: result?.resource?.identifier, + }, proj.fromLonLat([geoPoint.longitude, geoPoint.latitude]), 3500, false @@ -487,6 +517,29 @@ const SearchMap = ({ searchResults }) => { resultPointsSource.addFeature(pointFeature); } }); + const selectedPointsSource = mapFilters.selectedPointsLayer.getSource(); + // Create selected points from id list + selectedPointsIds.forEach((id) => { + const point = searchResults.find((result) => result.id === id); + if (point) { + const geoPoint = point.experimental_site.geo_point; + const pointName = point.resource.identifier; + if (geoPoint && geoPoint.longitude && geoPoint.latitude) { + if (!getSelectedPointsNames(selectedPointsSource).includes(pointName)) { + const pointFeature = createPointFeature( + { + id: 0, + name: pointName, + }, + proj.fromLonLat([geoPoint.longitude, geoPoint.latitude]), + 5000, + true + ); + selectedPointsSource.addFeature(pointFeature); + } + } + } + }); }; const clearSelectedFeatures = () => { @@ -494,7 +547,6 @@ const SearchMap = ({ searchResults }) => { if (tmpSelectedFeatures) { tmpSelectedFeatures.clear(); } - setSelectedFeatures(null); setSelectedFeatures(tmpSelectedFeatures); }; @@ -517,6 +569,7 @@ const SearchMap = ({ searchResults }) => { } }; + // Clear timer ref on unmount useEffect(() => { return () => clearTimeout(timerRef.current); }, []); @@ -561,8 +614,9 @@ const SearchMap = ({ searchResults }) => { .getLayers() .item(getLayerIndex('selectedPointsSource')); let selectedPointsList = <></>; + let selectedPoints = 0; if (selectedPointsLayer) { - const selectedPoints = getSelectedPointsNames(selectedPointsLayer.getSource()); + selectedPoints = getSelectedPointsNames(selectedPointsLayer.getSource()); if (selectedPoints.length === 0) { selectedPointsList = <p>{t('maps:selectedPointsList.empty')}</p>; } else { @@ -571,10 +625,13 @@ const SearchMap = ({ searchResults }) => { }); } } + return ( <div style={styles.selectedPointsListContainer}> <EuiTitle size="s"> - <h3>{t('maps:selectedPointsList.title')}</h3> + <h3> + {t('maps:selectedPointsList.title')} ({selectedPoints.length}) + </h3> </EuiTitle> <EuiSpacer size="m" /> <EuiText style={styles.selectedPointsList}>{selectedPointsList}</EuiText> @@ -598,6 +655,7 @@ const SearchMap = ({ searchResults }) => { clearSelectedFeatures(); // Remove displayed points from layer's source map.getLayers().item(getLayerIndex('selectedPointsSource')).getSource().clear(); + setSelectedPointsIds([]); }; return ( diff --git a/src/pages/maps/styles.js b/src/pages/maps/styles.js index 08132d53ca5c6d48c6260a52981bf538d8805f67..08c1ead22bc5f4407d73a10377dde0bac1c23a91 100644 --- a/src/pages/maps/styles.js +++ b/src/pages/maps/styles.js @@ -24,8 +24,8 @@ const styles = { width: '100%', display: 'flex', justifyContent: 'start', - minHeight: '80vh', - maxHeight: '80vh', + minHeight: '60vh', + maxHeight: '60vh', }, mapContainer: { minWidth: '60vw', @@ -54,7 +54,7 @@ const styles = { layersTable: { width: '60vw', cursor: 'pointer', - margin: '10px', + marginTop: '10px', }, layersTableCells: { padding: '10px', diff --git a/src/pages/profile/Profile.js b/src/pages/profile/Profile.js index 89bd3a7edde33dcf0540186c86bba5aedbae42ff..72c213856125829e5423fe6d0a2d51f9a2b18c61 100644 --- a/src/pages/profile/Profile.js +++ b/src/pages/profile/Profile.js @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; import { - EuiPageContent, EuiPageContentHeader, EuiPageContentHeaderSection, EuiPageContentBody, @@ -175,94 +174,92 @@ const Profile = () => { return ( <> - <EuiPageContent> - <EuiPageContentHeader> - <EuiPageContentHeaderSection> - <EuiTitle> - <h2>{t('pageTitle')}</h2> - </EuiTitle> - </EuiPageContentHeaderSection> - </EuiPageContentHeader> - <EuiPageContentBody> - <EuiForm component="form"> - <EuiTitle size="s"> - <h3>{t('groups.groupsList')}</h3> - </EuiTitle> - <EuiFormRow fullWidth label=""> - <EuiBasicTable items={groups} columns={groupColumns} /> - </EuiFormRow> - <EuiSpacer size="l" /> - <EuiTitle size="s"> - <h3>{t('requestsList.requestsList')}</h3> - </EuiTitle> - <EuiFormRow fullWidth label=""> - <EuiBasicTable items={userRequests} columns={requestsColumns} /> - </EuiFormRow> - <EuiSpacer size="l" /> - <EuiTitle size="s"> - <h3>{t('groupRequests.requestGroupAssignment')}</h3> - </EuiTitle> - {getUserGroupLabels() ? ( - <p - style={styles.currentRoleOrGroupText} - >{`${t('groupRequests.currentGroups')} ${getUserGroupLabels()}`}</p> - ) : ( - <p>{t('groupRequests.noGroup')}</p> - )} - <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> - <EuiComboBox - placeholder={'Select groups'} - options={groups} - selectedOptions={userGroups} - onChange={(selectedOptions) => { - setValueError(undefined); - setUserGroups(selectedOptions); - }} - onSearchChange={onValueSearchChange} - /> - </EuiFormRow> - <EuiSpacer size="m" /> - <EuiButton - onClick={() => { - onSendGroupRequest(); + <EuiPageContentHeader> + <EuiPageContentHeaderSection> + <EuiTitle> + <h2>{t('pageTitle')}</h2> + </EuiTitle> + </EuiPageContentHeaderSection> + </EuiPageContentHeader> + <EuiPageContentBody> + <EuiForm component="form"> + <EuiTitle size="s"> + <h3>{t('groups.groupsList')}</h3> + </EuiTitle> + <EuiFormRow fullWidth label=""> + <EuiBasicTable items={groups} columns={groupColumns} /> + </EuiFormRow> + <EuiSpacer size="l" /> + <EuiTitle size="s"> + <h3>{t('requestsList.requestsList')}</h3> + </EuiTitle> + <EuiFormRow fullWidth label=""> + <EuiBasicTable items={userRequests} columns={requestsColumns} /> + </EuiFormRow> + <EuiSpacer size="l" /> + <EuiTitle size="s"> + <h3>{t('groupRequests.requestGroupAssignment')}</h3> + </EuiTitle> + {getUserGroupLabels() ? ( + <p + style={styles.currentRoleOrGroupText} + >{`${t('groupRequests.currentGroups')} ${getUserGroupLabels()}`}</p> + ) : ( + <p>{t('groupRequests.noGroup')}</p> + )} + <EuiFormRow error={valueError} isInvalid={valueError !== undefined}> + <EuiComboBox + placeholder={'Select groups'} + options={groups} + selectedOptions={userGroups} + onChange={(selectedOptions) => { + setValueError(undefined); + setUserGroups(selectedOptions); }} - fill - > - {t('common:validationActions.send')} - </EuiButton> - <EuiSpacer size="l" /> - <EuiTitle size="s"> - <h3>{t('roleRequests.requestRoleAssignment')}</h3> - </EuiTitle> - {userRole ? ( - <p - style={styles.currentRoleOrGroupText} - >{`${t('roleRequests.currentRole')} ${userRole}`}</p> - ) : ( - <></> - )} - <EuiFormRow> - <EuiSelect - hasNoInitialSelection - options={roles} - value={selectedRole} - onChange={(e) => { - setSelectedRole(e.target.value); - }} - /> - </EuiFormRow> - <EuiSpacer size="m" /> - <EuiButton - onClick={() => { - onSendRoleRequest(); + onSearchChange={onValueSearchChange} + /> + </EuiFormRow> + <EuiSpacer size="m" /> + <EuiButton + onClick={() => { + onSendGroupRequest(); + }} + fill + > + {t('common:validationActions.send')} + </EuiButton> + <EuiSpacer size="l" /> + <EuiTitle size="s"> + <h3>{t('roleRequests.requestRoleAssignment')}</h3> + </EuiTitle> + {userRole ? ( + <p + style={styles.currentRoleOrGroupText} + >{`${t('roleRequests.currentRole')} ${userRole}`}</p> + ) : ( + <></> + )} + <EuiFormRow> + <EuiSelect + hasNoInitialSelection + options={roles} + value={selectedRole} + onChange={(e) => { + setSelectedRole(e.target.value); }} - fill - > - {t('common:validationActions.send')} - </EuiButton> - </EuiForm> - </EuiPageContentBody> - </EuiPageContent> + /> + </EuiFormRow> + <EuiSpacer size="m" /> + <EuiButton + onClick={() => { + onSendRoleRequest(); + }} + fill + > + {t('common:validationActions.send')} + </EuiButton> + </EuiForm> + </EuiPageContentBody> </> ); }; diff --git a/src/pages/results/ResourceFlyout.js b/src/pages/results/ResourceFlyout.js new file mode 100644 index 0000000000000000000000000000000000000000..bd7522b49a5f5d3e9a26490d78ecd1b48b4d71f9 --- /dev/null +++ b/src/pages/results/ResourceFlyout.js @@ -0,0 +1,44 @@ +import React, { Fragment } from 'react'; +import { EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; +import { useTranslation } from 'react-i18next'; +import JsonView from '@in-sylva/json-view'; + +const ResourceFlyout = ({ + resourceFlyoutData, + setResourceFlyoutData, + isResourceFlyoutOpen, + setIsResourceFlyoutOpen, +}) => { + const { t } = useTranslation('results'); + + const closeResourceFlyout = () => { + setResourceFlyoutData({}); + setIsResourceFlyoutOpen(false); + }; + + return ( + isResourceFlyoutOpen && ( + <EuiFlyout + onClose={() => closeResourceFlyout()} + aria-labelledby={t('results:flyout.label')} + > + <EuiFlyoutBody> + <EuiText size="s"> + <Fragment> + <JsonView + src={resourceFlyoutData} + name={t('results:flyout.JSON.title')} + collapsed={true} + iconStyle={'triangle'} + enableClipboard={false} + displayDataTypes={false} + /> + </Fragment> + </EuiText> + </EuiFlyoutBody> + </EuiFlyout> + ) + ); +}; + +export default ResourceFlyout; diff --git a/src/pages/results/Results.js b/src/pages/results/Results.js index b647f7738dbe1b6beff02d77d7e6bd933048dc74..3584081fabcbfa6d66e69d1b6a3f34996b4bd6ac 100644 --- a/src/pages/results/Results.js +++ b/src/pages/results/Results.js @@ -1,235 +1,38 @@ -import React, { Fragment, useEffect, useState } from 'react'; -import { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiIcon, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -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'; -import download from 'downloadjs'; +import React, { useState } from 'react'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { useTranslation } from 'react-i18next'; +import ResultsTableMUI from './ResultsTableMUI'; +import ResourceFlyout from './ResourceFlyout'; +import ResultsDownload from './ResultsDownload'; -const getMuiTheme = () => - createTheme({ - overrides: { - MUIDataTable: { - root: { - backgroundColor: '#fafbfc', - }, - paper: { - boxShadow: 'none', - }, - }, - }, - }); - -const changeFlyoutState = (array, index, value, defaultValue) => { - let newArray = new Array(array.length).fill(defaultValue); - newArray[index] = value; - return newArray; -}; - -const Results = ({ searchResults, searchQuery }) => { +const Results = ({ searchResults, searchQuery, selectedRowsIds, setSelectedRowsIds }) => { const { t } = useTranslation('results'); - const [resultsCol, setResultsCol] = useState([]); - const [results, setResults] = useState([]); - const [isFlyoutOpen, setIsFlyoutOpen] = useState([false]); - const [searchQueryText, setSearchQueryText] = useState(''); - - useEffect(() => { - processData(searchResults); - setSearchQueryText(searchQuery); - }, [searchResults]); - - const updateTableCell = (tableContent, value, colIndex, rowIndex) => { - const updatedRow = updateArrayElement(tableContent[rowIndex], colIndex, value); - return updateArrayElement(tableContent, rowIndex, updatedRow); - }; - - const closeAllFlyouts = (tableContent) => { - const updatedResults = []; - tableContent.forEach((tableRow, index) => { - updatedResults.push(updateArrayElement(tableRow, 0)); - }); - 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 ( - <> - <EuiFlyout - onClose={() => { - const updatedTable = updateTableCell( - closeAllFlyouts(results), - recordFlyout(record, recordIndex, false), - 0, - recordIndex - ); - const updatedArray = changeFlyoutState( - isFlyoutOpen, - recordIndex, - false, - false - ); - setIsFlyoutOpen(updatedArray); - setResults(updatedTable); - }} - aria-labelledby={recordIndex} - > - <EuiFlyoutBody> - <EuiText size="s"> - <Fragment> - <JsonView - name="In-Sylva metadata" - collapsed={true} - iconStyle={'triangle'} - src={record} - enableClipboard={false} - displayDataTypes={false} - /> - </Fragment> - </EuiText> - </EuiFlyoutBody> - </EuiFlyout> - <EuiIcon type="eye" color="danger" /> - </> - ); - } - }; - - const resultsGridOptions = { - print: false, - download: false, - filter: true, - filterType: 'dropdown', - responsive: 'standard', - selectableRows: 'none', - selectableRowsOnClick: true, - onRowSelectionChange: (rowsSelected, allRows) => {}, - onRowClick: (rowData, rowState) => {}, - onCellClick: (val, colMeta) => { - if (searchResults && colMeta.colIndex !== 0) { - const updatedTable = updateTableCell( - closeAllFlyouts(results), - recordFlyout( - searchResults[colMeta.dataIndex], - colMeta.dataIndex, - !isFlyoutOpen[colMeta.dataIndex] - ), - 0, - colMeta.dataIndex - ); - const updatedArray = changeFlyoutState( - isFlyoutOpen, - colMeta.dataIndex, - !isFlyoutOpen[colMeta.dataIndex], - false - ); - setIsFlyoutOpen(updatedArray); - setResults(updatedTable); - } - }, - }; - - const downloadResults = () => { - if (searchResults) { - download( - `{"metadataRecords": ${JSON.stringify(searchResults, null, '\t')}}`, - 'InSylvaSearchResults.json', - 'application/json' - ); - } - }; + const [isResourceFlyoutOpen, setIsResourceFlyoutOpen] = useState(false); + const [resourceFlyoutData, setResourceFlyoutData] = useState({}); return ( <> - <EuiSpacer size="s" /> - <EuiFlexGroup justifyContent="spaceAround"> - <EuiSpacer size="s" /> - <EuiFlexItem grow={false}> - <EuiTitle size="xs"> - <h2>{t('results:yourQuery', { query: searchQueryText })}</h2> - </EuiTitle> - </EuiFlexItem> - <EuiSpacer size="s" /> - </EuiFlexGroup> + <ResourceFlyout + resourceFlyoutData={resourceFlyoutData} + setResourceFlyoutData={setResourceFlyoutData} + isResourceFlyoutOpen={isResourceFlyoutOpen} + setIsResourceFlyoutOpen={setIsResourceFlyoutOpen} + /> <EuiFlexGroup> <EuiFlexItem> <EuiCallOut size="s" title={t('results:clickOnRowTip')} iconType="search" /> </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton fill onClick={() => downloadResults()}> - {t('results:downloadResultsButton.JSON')} - </EuiButton> - </EuiFlexItem> + <ResultsDownload searchResults={searchResults} /> </EuiFlexGroup> - <MuiThemeProvider theme={getMuiTheme()}> - <MUIDataTable - title={t('results:table.title')} - data={results} - columns={resultsCol} - options={resultsGridOptions} - /> - </MuiThemeProvider> + <EuiSpacer size={'m'} /> + <ResultsTableMUI + searchResults={searchResults} + searchQuery={searchQuery} + selectedRowsIds={selectedRowsIds} + setSelectedRowsIds={setSelectedRowsIds} + setResourceFlyoutData={setResourceFlyoutData} + setIsResourceFlyoutOpen={setIsResourceFlyoutOpen} + /> </> ); }; diff --git a/src/pages/results/ResultsDownload.js b/src/pages/results/ResultsDownload.js new file mode 100644 index 0000000000000000000000000000000000000000..7ed397818e778b6002540841f35743628c07c628 --- /dev/null +++ b/src/pages/results/ResultsDownload.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { EuiButton, EuiFlexItem } from '@elastic/eui'; +import download from 'downloadjs'; +import { useTranslation } from 'react-i18next'; + +const ResultsDownload = ({ searchResults }) => { + const { t } = useTranslation('results'); + + const downloadResults = () => { + if (!searchResults) { + return; + } + download( + `{"metadataRecords": ${JSON.stringify(searchResults, null, '\t')}}`, + 'InSylvaSearchResults.json', + 'application/json' + ); + }; + + return ( + <EuiFlexItem grow={false}> + <EuiButton fill onClick={() => downloadResults()}> + {t('results:downloadResultsButton.JSON')} + </EuiButton> + </EuiFlexItem> + ); +}; + +export default ResultsDownload; diff --git a/src/pages/results/ResultsTable.js b/src/pages/results/ResultsTable.js new file mode 100644 index 0000000000000000000000000000000000000000..415a67298ac34c8f3f2606e1be931f61205e4c3e --- /dev/null +++ b/src/pages/results/ResultsTable.js @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; +import { EuiBasicTable } from '@elastic/eui'; + +const buildColumnName = (name) => { + name = name.split('.').join(' '); + name = name.charAt(0).toUpperCase() + name.slice(1); + return name; +}; + +const ResultsTable = ({ searchResults }) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + + function getValueFromPath(obj, path) { + return path.split('.').reduce((acc, part, i, arr) => { + if (Array.isArray(acc)) { + // The next part should be a key to match within array objects + const key = arr[i + 1]; + if (key) { + const found = acc.find((item) => item.type === key); + arr.splice(i + 1, 1); // Remove the key from the path array + return found ? found.value : undefined; + } + } + return acc && acc[part]; + }, obj); + } + + function getValues(obj, paths) { + return paths.reduce((acc, path) => { + acc[path] = getValueFromPath(obj, path); + return acc; + }, {}); + } + + const buildRows = (results, columns) => { + let dataRows = []; + results.forEach((result) => { + let row = getValues(result, columns); + dataRows.push(row); + }); + return dataRows; + }; + + const buildColumns = (userColumns) => { + let dataColumns = []; + dataColumns.push({ + field: 'isRowOpen', + name: 'Open', + sortable: false, + truncateText: true, + }); + userColumns.forEach((userColumn) => { + dataColumns.push({ + field: userColumn, + name: buildColumnName(userColumn), + sortable: true, + truncateText: true, + }); + }); + return dataColumns; + }; + + const buildSearchResultsTable = (results) => { + if (!results && results.length > 0) { + return; + } + // TODO: get columns settings from user to replace hard-coded array + const userColumns = [ + 'id', + 'resource.title', + 'resource.lineage', + 'resource.identifier', + 'resource.date.publication', + 'resource.date.revision', + 'resource.INSPIRE_type', + ]; + const columns = buildColumns(userColumns); + const rows = buildRows(results, userColumns); + return { + rows, + columns, + }; + }; + + const { rows, columns } = buildSearchResultsTable(searchResults); + + const paginationOptions = { + pageSizeOptions: [5, 10, 20, 0], + pageIndex, + pageSize, + totalItemCount: rows?.length, + }; + + const onTableChange = ({ page }) => { + if (page) { + setPageIndex(page.index); + setPageSize(page.size); + } + }; + + return ( + <EuiBasicTable + items={rows} + columns={columns} + pagination={paginationOptions} + onChange={onTableChange} + /> + ); +}; + +export default ResultsTable; diff --git a/src/pages/results/ResultsTableMUI.js b/src/pages/results/ResultsTableMUI.js new file mode 100644 index 0000000000000000000000000000000000000000..b28999146840743dd62dc1121d17a593a812b7cb --- /dev/null +++ b/src/pages/results/ResultsTableMUI.js @@ -0,0 +1,228 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import MUIDataTable from 'mui-datatables'; +import { createTheme, ThemeProvider } from '@material-ui/core'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +const getMuiTheme = () => + createTheme({ + overrides: { + MuiTableRow: { + root: { + '&:hover': { + cursor: 'pointer', + }, + }, + }, + }, + }); + +const ResultsTableMUI = ({ + searchResults, + searchQuery, + selectedRowsIds, + setSelectedRowsIds, + setIsResourceFlyoutOpen, + setResourceFlyoutData, +}) => { + const { t } = useTranslation('results'); + const [selectedRows, setSelectedRows] = useState([]); + + useEffect(() => { + setSelectedRows(selectedRowsIds.map((id) => getRowIdFromResourceData(id))); + }, [selectedRowsIds]); + + // Build each row in table from search results data + const buildRows = (results) => { + let dataRows = []; + if (results.length === 0) { + return dataRows; + } + results.forEach((result, index) => { + let row = { + id: result.id, + }; + for (const fieldName in result.resource) { + if (typeof result.resource[fieldName] === 'string') { + row[fieldName] = result.resource[fieldName]; + } + } + dataRows.push(row); + }); + return dataRows; + }; + + const buildColumnName = (name) => { + name = name.split('_').join(' '); + name = name.charAt(0).toUpperCase() + name.slice(1); + return name; + }; + + const buildColumns = (results) => { + if (!results[0]?.resource) { + return []; + } + let dataColumns = []; + dataColumns.push({ + name: 'id', + label: 'ID', + options: { + display: 'excluded', + }, + }); + // TODO: get columns settings from user to replace hard-coded array + for (const fieldName in results[0].resource) { + if (typeof results[0].resource[fieldName] === 'string') { + dataColumns.push({ + name: fieldName, + label: buildColumnName(fieldName), + options: { + display: true, + }, + }); + } + } + return dataColumns; + }; + + const buildResultsTable = (results) => { + if (!results && results.length > 0) { + return; + } + return { + rows: buildRows(results), + columns: buildColumns(results), + }; + }; + + const { rows, columns } = useMemo( + () => buildResultsTable(searchResults), + [searchResults] + ); + + const getResourceDataFromRowId = (id) => { + return searchResults.find((result) => result.id === id); + }; + + const getRowIdFromResourceData = (id) => { + for (let index = 0; index < rows.length; index++) { + if (rows[index].id === id) { + return index; + } + } + }; + + const onRowSelectionCallback = (selectedRow, allSelectedRows) => { + setSelectedRowsIds( + allSelectedRows.map((row) => { + return rows[row.dataIndex].id; + }) + ); + }; + + // Open resource flyout on row click (any cell) + const onCellClickCallback = (cellData, cellState) => { + if (!searchResults || searchResults.length === 0) { + return; + } + const resourceData = getResourceDataFromRowId(rows[cellState.dataIndex].id); + // Extract all values except for id to avoid displaying it to user + setResourceFlyoutData((({ id, ...rest }) => rest)(resourceData)); + setIsResourceFlyoutOpen(true); + }; + + const sortBySelectedRows = () => { + // TODO add sorting by row selection + /*return data.sort((a, b) => { + const isSelectedA = selectedRowsIds.includes(a.data[0]); + const isSelectedB = selectedRowsIds.includes(b.data[0]); + if (isSelectedA && !isSelectedB) { + return -1; + } else if (!isSelectedA && isSelectedB) { + return 1; + } else { + // If both rows are either selected or not, maintain their order + return 0; + } + });*/ + }; + + // Remove the delete rows button and display custom sort button + const CustomSelectToolbar = () => { + return ( + <EuiFlexGroup style={{ marginLeft: '10px' }}> + <EuiFlexItem grow={false}> + <EuiButton onClick={() => sortBySelectedRows()}> + {t('results:table.displaySelectedRowsButton')} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ); + }; + + // Translations for mui-datatables component texts + const textLabels = { + body: { + noMatch: t('results:table.textLabels.body.noMatch'), + toolTip: t('results:table.textLabels.body.toolTip'), + }, + pagination: { + next: t('results:table.textLabels.pagination.next'), + previous: t('results:table.textLabels.pagination.previous'), + rowsPerPage: t('results:table.textLabels.pagination.rowsPerPage'), + displayRows: t('results:table.textLabels.pagination.displayRows'), + jumpToPage: t('results:table.textLabels.pagination.jumpToPage'), + }, + toolbar: { + search: t('results:table.textLabels.toolbar.search'), + viewColumns: t('results:table.textLabels.toolbar.viewColumns'), + filterTable: t('results:table.textLabels.toolbar.filterTable'), + }, + filter: { + all: t('results:table.textLabels.filter.all'), + title: t('results:table.textLabels.filter.title'), + reset: t('results:table.textLabels.filter.reset'), + }, + viewColumns: { + title: t('results:table.textLabels.viewColumns.title'), + titleAria: t('results:table.textLabels.viewColumns.titleAria'), + }, + selectedRows: { + text: t('results:table.textLabels.selectedRows.text'), + }, + }; + + const tableOptions = { + print: false, + download: false, + filter: true, + filterType: 'textField', + responsive: 'standard', + selectableRows: 'multiple', + selectableRowsOnClick: false, + rowsSelected: selectedRows, + rowsPerPage: 15, + rowsPerPageOptions: [15, 30, 50, 100], + jumpToPage: true, + searchPlaceholder: t('results:table.search'), + elevation: 0, // remove the boxShadow style + customToolbarSelect: () => <CustomSelectToolbar />, + selectToolbarPlacement: 'above', + onRowSelectionChange: onRowSelectionCallback, + onCellClick: onCellClickCallback, + textLabels, + }; + + return ( + <ThemeProvider theme={getMuiTheme()}> + <MUIDataTable + title={<Trans i18nKey={'results:table.title'} components={{ searchQuery }} />} + data={rows} + columns={columns} + options={tableOptions} + /> + </ThemeProvider> + ); +}; + +export default ResultsTableMUI; diff --git a/src/pages/search/AdvancedSearch/AdvancedSearch.js b/src/pages/search/AdvancedSearch/AdvancedSearch.js index cd4b8a08ae977919cf27a639518b757db83b9dc5..ce4b0156c34e546d3a337cfd54136e9967fe9841 100644 --- a/src/pages/search/AdvancedSearch/AdvancedSearch.js +++ b/src/pages/search/AdvancedSearch/AdvancedSearch.js @@ -46,6 +46,7 @@ import { DateOptions, NumericOptions, Operators } from '../Data'; import TextField from '@material-ui/core/TextField'; import { addUserHistory, fetchUserHistory } from '../../../actions/user'; import { useTranslation } from 'react-i18next'; +import styles from './styles'; const updateSources = ( searchFields, @@ -184,11 +185,10 @@ const HistorySelect = ({ setFieldCount, userHistory, setUserHistory, - selectedSavedSearch, - setSelectedSavedSearch, }) => { - const [historySelectError, setHistorySelectError] = useState(undefined); const { t } = useTranslation('search'); + const [historySelectError, setHistorySelectError] = useState(undefined); + const [selectedSavedSearch, setSelectedSavedSearch] = useState(undefined); useEffect(() => { fetchHistory(setUserHistory); @@ -248,12 +248,6 @@ const SearchBar = ({ setSearchResults, searchFields, setSearchFields, - searchName, - setSearchName, - searchDescription, - setSearchDescription, - readOnlyQuery, - setReadOnlyQuery, selectedSources, setSelectedSources, availableSources, @@ -264,16 +258,15 @@ const SearchBar = ({ searchCount, setSearchCount, setFieldCount, - isSaveSearchModalOpen, - setIsSaveSearchModalOpen, - selectedSavedSearch, - setSelectedSavedSearch, - selectedOperatorId, createEditableQueryToast, }) => { const { t } = useTranslation(['search', 'common']); - const [userHistory, setUserHistory] = useState({}); const [isLoading, setIsLoading] = useState(false); + const [userHistory, setUserHistory] = useState({}); + const [isSaveSearchModalOpen, setIsSaveSearchModalOpen] = useState(false); + const [searchDescription, setSearchDescription] = useState(''); + const [searchName, setSearchName] = useState(''); + const [readOnlyQuery, setReadOnlyQuery] = useState(true); const closeSaveSearchModal = () => { setIsSaveSearchModalOpen(false); @@ -307,7 +300,9 @@ const SearchBar = ({ availableSources ); getQueryCount(queriesWithIndices).then((result) => { - if (result || result === 0) setSearchCount(result); + if (result || result === 0) { + setSearchCount(result); + } }); } }; @@ -343,9 +338,7 @@ const SearchBar = ({ <EuiFieldText name="searchName" value={searchName} - onChange={(e) => { - setSearchName(e.target.value); - }} + onChange={(e) => setSearchName(e.target.value)} /> </EuiFormRow> <EuiFormRow @@ -363,7 +356,6 @@ const SearchBar = ({ </EuiFormRow> </EuiForm> </EuiModalBody> - <EuiModalFooter> <EuiButtonEmpty onClick={() => { @@ -388,6 +380,7 @@ const SearchBar = ({ return ( <> + {isSaveSearchModalOpen && <SaveSearchModal />} <EuiFlexGroup> <EuiFlexItem> <EuiTextArea @@ -397,6 +390,13 @@ const SearchBar = ({ placeholder={t('search:advancedSearch.textQueryPlaceholder')} fullWidth /> + {isLoading && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiProgress postion="fixed" size="l" color="accent" /> + </EuiFlexItem> + </EuiFlexGroup> + )} </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButton @@ -437,7 +437,6 @@ const SearchBar = ({ > {t('search:advancedSearch.searchHistory.saveSearch')} </EuiButton> - {isSaveSearchModalOpen && <SaveSearchModal />} <EuiSpacer size="s" /> <EuiSwitch compressed @@ -452,13 +451,6 @@ const SearchBar = ({ /> </EuiFlexItem> </EuiFlexGroup> - {isLoading && ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiProgress postion="fixed" size="l" color="accent" /> - </EuiFlexItem> - </EuiFlexGroup> - )} <EuiSpacer size="s" /> <EuiFlexGroup> <EuiFlexItem> @@ -467,15 +459,11 @@ const SearchBar = ({ setAvailableSources={setAvailableSources} setSelectedSources={setSelectedSources} setSearch={setSearch} - searchFields={searchFields} - selectedOperatorId={selectedOperatorId} - userHistory={userHistory} - setUserHistory={setUserHistory} setSearchFields={setSearchFields} setSearchCount={setSearchCount} setFieldCount={setFieldCount} - selectedSavedSearch={selectedSavedSearch} - setSelectedSavedSearch={setSelectedSavedSearch} + userHistory={userHistory} + setUserHistory={setUserHistory} /> </EuiFlexItem> </EuiFlexGroup> @@ -483,18 +471,11 @@ const SearchBar = ({ ); }; -const PopoverSelect = ({ - standardFields, - searchFields, - setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, -}) => { +const PopoverSelect = ({ standardFields, searchFields, setSearchFields }) => { const { t } = useTranslation('search'); + const [isPopoverSelectOpen, setIsPopoverSelectOpen] = useState(false); + const [selectedField, setSelectedField] = useState([]); + const [selectedSection, setSelectedSection] = useState([]); const handleAddField = () => { if (!!selectedField[0]) { @@ -607,8 +588,6 @@ const PopoverValueContent = ({ standardFields, searchFields, setSearchFields, - valueError, - setValueError, setSearch, setSearchCount, fieldCount, @@ -616,7 +595,6 @@ const PopoverValueContent = ({ isPopoverValueOpen, setIsPopoverValueOpen, selectedOperatorId, - datePickerStyles, createPolicyToast, selectedSources, setSelectedSources, @@ -624,6 +602,8 @@ const PopoverValueContent = ({ setAvailableSources, }) => { const { t } = useTranslation(['search', 'common']); + const datePickerStyles = styles(); + const [valueError, setValueError] = useState(undefined); const onValueSearchChange = (value, hasMatchingOptions) => { if (value.length === 0 || hasMatchingOptions) { @@ -1063,16 +1043,11 @@ const PopoverValueButton = ({ standardFields, searchFields, setSearchFields, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, setSearch, setSearchCount, fieldCount, setFieldCount, selectedOperatorId, - datePickerStyles, createPolicyToast, selectedSources, setSelectedSources, @@ -1080,6 +1055,7 @@ const PopoverValueButton = ({ setAvailableSources, }) => { const { t } = useTranslation('search'); + const [isPopoverValueOpen, setIsPopoverValueOpen] = useState([false]); return ( <EuiPopover @@ -1111,8 +1087,6 @@ const PopoverValueButton = ({ standardFields={standardFields} searchFields={searchFields} setSearchFields={setSearchFields} - valueError={valueError} - setValueError={setValueError} setSearch={setSearch} setSearchCount={setSearchCount} fieldCount={fieldCount} @@ -1120,7 +1094,6 @@ const PopoverValueButton = ({ isPopoverValueOpen={isPopoverValueOpen} setIsPopoverValueOpen={setIsPopoverValueOpen} selectedOperatorId={selectedOperatorId} - datePickerStyles={datePickerStyles} createPolicyToast={createPolicyToast} selectedSources={selectedSources} setSelectedSources={setSelectedSources} @@ -1134,20 +1107,8 @@ const PopoverValueButton = ({ const FieldsPanel = ({ standardFields, - setStandardFields, searchFields, setSearchFields, - selectedField, - setSelectedField, - selectedSection, - setSelectedSection, - isPopoverSelectOpen, - setIsPopoverSelectOpen, - isPopoverValueOpen, - setIsPopoverValueOpen, - valueError, - setValueError, - search, setSearch, setSearchCount, selectedOperatorId, @@ -1159,7 +1120,6 @@ const FieldsPanel = ({ selectedSources, setSelectedSources, sources, - datePickerStyles, createPolicyToast, }) => { const { t } = useTranslation('search'); @@ -1321,20 +1281,13 @@ const FieldsPanel = ({ <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} @@ -1350,19 +1303,8 @@ const FieldsPanel = ({ <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" /> @@ -1384,14 +1326,9 @@ const FieldsPanel = ({ ); }; -const SourceSelect = ({ - availableSources, - selectedSources, - setSelectedSources, - sourceSelectError, - setSourceSelectError, -}) => { +const SourceSelect = ({ availableSources, selectedSources, setSelectedSources }) => { const { t } = useTranslation('search'); + const [sourceSelectError, setSourceSelectError] = useState(undefined); if (Object.keys(availableSources).length === 0) { return ( @@ -1445,16 +1382,7 @@ const SourceSelect = ({ const AdvancedSearch = ({ search, setSearch, - searchResults, setSearchResults, - searchFields, - setSearchFields, - searchName, - setSearchName, - searchDescription, - setSearchDescription, - readOnlyQuery, - setReadOnlyQuery, selectedSources, setSelectedSources, availableSources, @@ -1463,38 +1391,15 @@ const AdvancedSearch = ({ 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 [selectedOperatorId, setSelectedOperatorId] = useState('0'); + const [searchFields, setSearchFields] = useState([]); + const [fieldCount, setFieldCount] = useState([]); + const [searchCount, setSearchCount] = useState(); const createPolicyToast = () => { const toast = { @@ -1543,7 +1448,6 @@ const AdvancedSearch = ({ <> <EuiFlexGroup> <EuiFlexItem grow={false}> - <EuiSpacer size="s" /> <EuiButtonEmpty onClick={() => { setIsAdvancedSearch(!isAdvancedSearch); @@ -1559,16 +1463,9 @@ const AdvancedSearch = ({ <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} @@ -1579,15 +1476,6 @@ const AdvancedSearch = ({ 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> @@ -1600,16 +1488,6 @@ const AdvancedSearch = ({ 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} @@ -1622,7 +1500,6 @@ const AdvancedSearch = ({ selectedSources={selectedSources} setSelectedSources={setSelectedSources} sources={sources} - datePickerStyles={datePickerStyles} createPolicyToast={createPolicyToast} /> <EuiSpacer size="s" /> @@ -1630,8 +1507,6 @@ const AdvancedSearch = ({ availableSources={availableSources} selectedSources={selectedSources} setSelectedSources={setSelectedSources} - sourceSelectError={sourceSelectError} - setSourceSelectError={setSourceSelectError} /> </EuiFlexItem> </EuiFlexGroup> diff --git a/src/pages/search/styles.js b/src/pages/search/AdvancedSearch/styles.js similarity index 100% rename from src/pages/search/styles.js rename to src/pages/search/AdvancedSearch/styles.js diff --git a/src/pages/search/BasicSearch/BasicSearch.js b/src/pages/search/BasicSearch/BasicSearch.js index 9a75f6e7568792959266579126977122af39778a..397091ade583db73a62cd91f5e1353964949b301 100644 --- a/src/pages/search/BasicSearch/BasicSearch.js +++ b/src/pages/search/BasicSearch/BasicSearch.js @@ -36,10 +36,10 @@ const BasicSearch = ({ ); searchQuery(queriesWithIndices).then((result) => { setSearchResults(result); - setSelectedTabNumber(1); if (isLoading) { setIsLoading(false); } + setSelectedTabNumber(1); }); }; @@ -47,7 +47,6 @@ const BasicSearch = ({ <> <EuiFlexGroup> <EuiFlexItem grow={false}> - <EuiSpacer size="s" /> <EuiButtonEmpty onClick={() => { setIsAdvancedSearch(!isAdvancedSearch); @@ -67,9 +66,16 @@ const BasicSearch = ({ value={basicSearch} onChange={(e) => setBasicSearch(e.target.value)} placeholder={t('basicSearch.searchInputPlaceholder')} - fullWidth autoFocus={true} + fullWidth /> + {isLoading && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiProgress postion="fixed" size="l" color="accent" /> + </EuiFlexItem> + </EuiFlexGroup> + )} </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButton type="submit" fill isDisabled={isAdvancedSearch}> @@ -78,13 +84,6 @@ const BasicSearch = ({ </EuiFlexItem> </EuiFlexGroup> </form> - {isLoading && ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiProgress postion="fixed" size="l" color="accent" /> - </EuiFlexItem> - </EuiFlexGroup> - )} </EuiFlexItem> </EuiFlexGroup> </> diff --git a/src/pages/search/Search.js b/src/pages/search/Search.js index 61bd93e712548ff2481e0473f84334777f2c7262..5f92324661616fc050f13403cf4942cc00f3febb 100644 --- a/src/pages/search/Search.js +++ b/src/pages/search/Search.js @@ -2,14 +2,9 @@ import React, { useState, useEffect } from 'react'; import { EuiTabbedContent, EuiPageContentBody, - EuiForm, EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiPageContent, - EuiPageContentHeader, - EuiTitle, - EuiPageContentHeaderSection, } from '@elastic/eui'; import Results from '../results/Results'; import SearchMap from '../maps/SearchMap'; @@ -22,36 +17,19 @@ import { import { useTranslation } from 'react-i18next'; import AdvancedSearch from './AdvancedSearch/AdvancedSearch'; import BasicSearch from './BasicSearch/BasicSearch'; -import styles from './styles'; const Search = () => { const { t } = useTranslation('search'); - const datePickerStyles = styles(); const [selectedTabNumber, setSelectedTabNumber] = useState(0); const [isAdvancedSearch, setIsAdvancedSearch] = useState(false); - const [readOnlyQuery, setReadOnlyQuery] = useState(true); - const [selectedField, setSelectedField] = useState([]); - const [selectedSection, setSelectedSection] = useState([]); - const [isPopoverSelectOpen, setIsPopoverSelectOpen] = useState(false); - const [isPopoverValueOpen, setIsPopoverValueOpen] = useState([false]); const [selectedSources, setSelectedSources] = useState([]); + const [sources, setSources] = useState([]); const [availableSources, setAvailableSources] = useState([]); - const [sourceSelectError, setSourceSelectError] = useState(undefined); - const [valueError, setValueError] = useState(undefined); const [search, setSearch] = useState(''); - const [searchName, setSearchName] = useState(''); - const [searchDescription, setSearchDescription] = useState(''); const [basicSearch, setBasicSearch] = useState(''); - const [selectedOperatorId, setSelectedOperatorId] = useState('0'); - const [searchFields, setSearchFields] = useState([]); const [standardFields, setStandardFields] = useState([]); - const [sources, setSources] = useState([]); const [searchResults, setSearchResults] = useState([]); - const [searchCount, setSearchCount] = useState(); - const [fieldCount, setFieldCount] = useState([]); - const [isReadOnlyModalOpen, setIsReadOnlyModalOpen] = useState(false); - const [isSaveSearchModalOpen, setIsSaveSearchModalOpen] = useState(false); - const [selectedSavedSearch, setSelectedSavedSearch] = useState(); + const [selectedResultsRowsIds, setSelectedResultsRowsIds] = useState([]); useEffect(() => { fetchPublicFields().then((resultStdFields) => { @@ -94,95 +72,71 @@ const Search = () => { id: 'tab1', name: t('tabs.composeSearch'), content: ( - <> - {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} - /> - ) : ( - <BasicSearch - isAdvancedSearch={isAdvancedSearch} - setIsAdvancedSearch={setIsAdvancedSearch} - standardFields={standardFields} - availableSources={availableSources} - selectedSources={selectedSources} - basicSearch={basicSearch} - setBasicSearch={setBasicSearch} - setSearchResults={setSearchResults} - setSelectedTabNumber={setSelectedTabNumber} - /> - )} - </> + <EuiFlexGroup> + <EuiFlexItem> + <EuiSpacer size={'l'} /> + {isAdvancedSearch ? ( + <AdvancedSearch + search={search} + setSearch={setSearch} + setSearchResults={setSearchResults} + selectedSources={selectedSources} + setSelectedSources={setSelectedSources} + availableSources={availableSources} + setAvailableSources={setAvailableSources} + standardFields={standardFields} + setStandardFields={setStandardFields} + sources={sources} + setSelectedTabNumber={setSelectedTabNumber} + isAdvancedSearch={isAdvancedSearch} + setIsAdvancedSearch={setIsAdvancedSearch} + /> + ) : ( + <BasicSearch + isAdvancedSearch={isAdvancedSearch} + setIsAdvancedSearch={setIsAdvancedSearch} + standardFields={standardFields} + availableSources={availableSources} + selectedSources={selectedSources} + basicSearch={basicSearch} + setBasicSearch={setBasicSearch} + setSearchResults={setSearchResults} + setSelectedTabNumber={setSelectedTabNumber} + /> + )} + </EuiFlexItem> + </EuiFlexGroup> ), }, { - id: 'tab3', + id: 'tab2', name: t('tabs.results'), content: ( <EuiFlexGroup> <EuiFlexItem> + <EuiSpacer size="l" /> <Results searchResults={searchResults} searchQuery={isAdvancedSearch ? search : basicSearch} + selectedRowsIds={selectedResultsRowsIds} + setSelectedRowsIds={setSelectedResultsRowsIds} /> </EuiFlexItem> </EuiFlexGroup> ), }, { - id: 'tab2', + id: 'tab3', name: t('tabs.map'), content: ( <EuiFlexGroup> <EuiFlexItem> <EuiSpacer size="l" /> - <SearchMap searchResults={searchResults} /> + <SearchMap + searchResults={searchResults} + selectedPointsIds={selectedResultsRowsIds} + setSelectedPointsIds={setSelectedResultsRowsIds} + /> </EuiFlexItem> </EuiFlexGroup> ), @@ -190,28 +144,15 @@ const Search = () => { ]; return ( - <> - <EuiPageContent> - <EuiPageContentHeader> - <EuiPageContentHeaderSection> - <EuiTitle> - <h2>{t('pageTitle')}</h2> - </EuiTitle> - </EuiPageContentHeaderSection> - </EuiPageContentHeader> - <EuiPageContentBody> - <EuiForm> - <EuiTabbedContent - tabs={tabsContent} - selectedTab={tabsContent[selectedTabNumber]} - onTabClick={(tab) => { - setSelectedTabNumber(tabsContent.indexOf(tab)); - }} - /> - </EuiForm> - </EuiPageContentBody> - </EuiPageContent> - </> + <EuiPageContentBody> + <EuiTabbedContent + tabs={tabsContent} + selectedTab={tabsContent[selectedTabNumber]} + onTabClick={(tab) => { + setSelectedTabNumber(tabsContent.indexOf(tab)); + }} + /> + </EuiPageContentBody> ); };