From 8dd3a584010e692ed0b32ae6498ea7657203f8b5 Mon Sep 17 00:00:00 2001 From: ddelpiano Date: Thu, 24 Apr 2025 15:40:36 +0200 Subject: [PATCH 1/5] changes discussed at the sprint meeting --- src/components/GraphViewer/Graph.jsx | 112 ++++++++++++---------- src/components/SearchResults/ListView.jsx | 56 ++++++++--- src/components/SearchResults/index.jsx | 8 +- src/parsers/termParser.tsx | 10 +- 4 files changed, 117 insertions(+), 69 deletions(-) diff --git a/src/components/GraphViewer/Graph.jsx b/src/components/GraphViewer/Graph.jsx index 295a2838..eb5aeb1e 100644 --- a/src/components/GraphViewer/Graph.jsx +++ b/src/components/GraphViewer/Graph.jsx @@ -2,7 +2,9 @@ import * as d3 from "d3"; import PropTypes from "prop-types"; import { Box } from "@mui/material"; import { useMemo, useEffect } from "react"; -import { getGraphStructure , OBJECT, SUBJECT, PREDICATE, ROOT} from "./GraphStructure"; +import { getGraphStructure, PREDICATE, ROOT} from "./GraphStructure"; +import { vars } from "../../theme/variables"; +const { gray600, white } = vars; const MARGIN = { top: 60, right: 60, bottom: 60, left: 60 }; @@ -59,67 +61,72 @@ const Graph = ({ width, height, predicate }) => { const allNodes = dendrogram.descendants().map((node) => { let textOffset = 0; - if ( node.data.type === OBJECT ) { - textOffset = 40; - } else if ( node.data.type === SUBJECT ) { - textOffset = 40; - } else if ( node.data.type === PREDICATE ) { - textOffset = 40; + if (node.data.type === PREDICATE || node.data.type === ROOT) { + textOffset = -40; + } else { + textOffset = 5; } - + const truncatedName = node.data.name.length > 25 ? `${node.data.name.substring(0, 25)}...` : node.data.name; - + return ( - - {( - - {truncatedName} - - )} + + + {truncatedName} + ); }); const allEdges = dendrogram.descendants().map((node, index) => { if (!node.parent) { - return; + // Add a black circle at the root + return ( + + ); } - + const line = d3 .line() .x(d => d[0]) .y(d => d[1]) - .curve(d3.curveBundle.beta(.75)); - - const start = [boundsWidth - node.parent.y, node.parent.x] - const end = [boundsWidth - node.y, node.x] + .curve(d3.curveBundle.beta(0.75)); + + const start = [node.parent.y, node.parent.x]; + const end = [node.y, node.x]; const radius = 5; - + const points = [ start, - [start[0] + radius, end[1]], - end + [start[0] - radius, end[1]], + end, ]; - + return ( ); @@ -132,22 +139,27 @@ const Graph = ({ width, height, predicate }) => { pointerEvents: "none", opacity: 0, zIndex: 1000, + background: gray600, + color: white, + padding: "0.5rem", + borderRadius: "0.5rem", }}> - - - - - + + + + + { {searchResult.label || searchResult.name} - + {searchResult.ontologyIsActive ? ( { const InfoSection = ({ searchResult }) => { const infoItems = [ - { label: 'Preferred ID', value: searchResult.ilx.replace('_', ':').toUpperCase() }, - { label: 'IDs', value: searchResult.existing_ids.flatMap(item => item.curie) }, - { label: 'Type', value: searchResult.type }, - { label: 'Score', value: searchResult.status }, - { label: 'Organization', value: searchResult.organization }, + { label: 'ID', value: searchResult.ilx}, + { label: 'Preferred ID', value: searchResult.existing_ids}, + { label: 'Synonyms', value: searchResult.synonyms }, + { label: 'Score', value: searchResult.score }, ]; + const getText = (value) => { + return ({value}) + } + + const getChip = (value) => { + return () + } + + const getChips = (value) => { + return ( + + {value.map((val, index) => ( + + ))} + + ); + }; + + const getValue = (label, value) => { + if (label === 'ID') { + return getText(value.replace('_', ':').toUpperCase()); + } else if (label === 'Preferred ID') { + const id = value.find((id) => id.preferred === "1"); + return getChip(id.curie); + } else if (label === 'Synonyms') { + return getChips(value.map((synonym) => synonym.literal)); + } else if (label === 'Score') { + return getText(value); + } + } + return ( { }} > {infoItems.map(({ label, value }) => ( - + {label} - {label === 'IDs' - ? (value.map((val, index) => ( ))) - : {value} - } + {getValue(label, value)} ))} diff --git a/src/components/SearchResults/index.jsx b/src/components/SearchResults/index.jsx index dcb86057..5fcfaa75 100644 --- a/src/components/SearchResults/index.jsx +++ b/src/components/SearchResults/index.jsx @@ -43,16 +43,16 @@ const SearchResults = () => { return results.filter(item => { for (let category in checkedLabels) { if (!checkedLabels[category]) continue; // Skip empty categories - + const selectedLabels = Object.entries(checkedLabels[category]) .filter(([, isChecked]) => isChecked) .map(([label]) => label); - + if (selectedLabels.length === 0) continue; - + const categoryLower = category.toLowerCase(); const itemValue = item[categoryLower] || item[categoryLower === 'type' ? 'Type' : categoryLower]; - + if (!selectedLabels.includes(itemValue)) { return false; } diff --git a/src/parsers/termParser.tsx b/src/parsers/termParser.tsx index 77fe3c5f..f6797438 100644 --- a/src/parsers/termParser.tsx +++ b/src/parsers/termParser.tsx @@ -155,13 +155,15 @@ export const termParser = (data, searchTerm, start?, end?) => { } }; -export const elasticSearhParser = (data) => { +export const elasticSearchParser = (data) => { + // TODO add the score to the results let terms : Terms; if ( Array.isArray(data) ){ terms = data?.map( term => { let newTerm : Term = {} as Term - term = term._source - return term + newTerm = term._source + newTerm['score'] = term._score + return newTerm }) // We are receiving an unknown amout of terms from server, we need to control @@ -177,4 +179,4 @@ export const elasticSearhParser = (data) => { } }; -export default termParser; \ No newline at end of file +export default termParser; From 6803dddd281582d9990f1db5bbe84322817eee54 Mon Sep 17 00:00:00 2001 From: ddelpiano Date: Thu, 8 May 2025 20:19:22 +0200 Subject: [PATCH 2/5] orcid auth, user data stored, handling cookie and more, part 1 --- src/App.jsx | 29 +++++++++++++++++++++++- src/api/endpoints/apiService.ts | 9 +++++++- src/api/endpoints/index.ts | 4 ++-- src/components/Auth/Login.jsx | 39 +++++++++++++++++++++------------ src/components/Auth/utils.ts | 18 +++++++++++++++ src/config.js | 5 +++++ 6 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 src/components/Auth/utils.ts diff --git a/src/App.jsx b/src/App.jsx index 1946418c..8605ba93 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useContext } from "react"; import CssBaseline from "@mui/material/CssBaseline"; import { Box, ThemeProvider } from "@mui/material"; import { @@ -26,6 +26,11 @@ import SingleOrganization from "./components/SingleOrganization"; import TermActivity from "./components/term_activity/TermActivity"; import OrganizationsCurieEditor from "./components/CurieEditor/OrganizationCurieEditor"; import { handleOrcidLogin } from "./api/endpoints"; +import { GlobalDataContext } from "./contexts/DataContext"; +import { useCookies } from 'react-cookie' +import { API_CONFIG } from "./config"; +import { local } from "d3"; + const PageContainer = ({ children }) => { return ( @@ -36,6 +41,28 @@ const PageContainer = ({ children }) => { }; function MainContent() { + const [cookies, setCookie, removeCookie] = useCookies(['session']) + const cookiesInfo = JSON.parse(localStorage.getItem(API_CONFIG.SESSION_DATA.COOKIE)); + const { user, setUserData } = useContext(GlobalDataContext); + + // check if cookie is expired + if (!user) { + const sessionCookie = cookies.session; + const expires = new Date(cookiesInfo.expires); + const today = new Date(); + if (sessionCookie === cookiesInfo?.value && expires > today) { + const userData = JSON.parse(localStorage.getItem(API_CONFIG.SESSION_DATA.SETTINGS)); + setUserData({ + name: userData['groupname'], + id: userData['orcid'], + email: userData?.emails[0]?.email, + role: userData['own-role'], + groupname: userData['groupname'], + settings: userData + }); + } + } + return ( (API_CONFIG.REAL_API.SIGNIN, "application/x-www-form-urlencoded") + export const register = createPostRequest(API_CONFIG.REAL_API.NEWUSER_ILX, "application/x-www-form-urlencoded") + +export const getUserSettings = (group: string) => { + const endpoint = `/${group}${API_CONFIG.REAL_API.USER_SETTINGS}`; + return createGetRequest(endpoint, "application/json")(); +}; + export const createNewOrganization = ({group, data} : {group: string, data: any}) => { const endpoint = `/${group}${API_CONFIG.REAL_API.CREATE_NEW_ORGANIZATION}`; return createPostRequest(endpoint, "application/json")(data); @@ -28,4 +35,4 @@ export const createNewOrganization = ({group, data} : {group: string, data: any} export const getOrganizations = (group: string) => { const endpoint = `/${group}${API_CONFIG.REAL_API.GET_ORGANIZATIONS}`; return createGetRequest(endpoint, "application/json")(); -}; \ No newline at end of file +}; diff --git a/src/api/endpoints/index.ts b/src/api/endpoints/index.ts index cba7a7d1..05aa1fcb 100644 --- a/src/api/endpoints/index.ts +++ b/src/api/endpoints/index.ts @@ -3,7 +3,7 @@ import * as mockApi from './../../api/endpoints/swaggerMockMissingEndpoints'; import * as api from "./../../api/endpoints/interLexURIStructureAPI"; import { TERM, ONTOLOGY, ORGANIZATION } from '../../model/frontend/types' import curieParser from '../../parsers/curieParser'; -import termParser, { elasticSearhParser, getTerm } from '../../parsers/termParser'; +import termParser, { elasticSearchParser, getTerm } from '../../parsers/termParser'; import axios from 'axios'; import { API_CONFIG } from '../../config'; @@ -180,7 +180,7 @@ export const elasticSearch = async (query) => { } } }); - return elasticSearhParser(result?.hits?.hits) + return elasticSearchParser(result?.hits?.hits) } catch (error) { console.error("ElasticSearch Query Failed:", error); } diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx index 698abcdf..f992edfb 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -21,6 +21,7 @@ import { API_CONFIG } from "../../config"; import { GlobalDataContext } from "../../contexts/DataContext"; import * as yup from "yup"; import { useCookies } from 'react-cookie' +import { requestUserSettings } from "./utils"; const schema = yup.object().shape({ @@ -45,22 +46,32 @@ const Login = () => { let eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; let eventer = window[eventMethod]; let messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message"; - eventer(messageEvent, function (e) { + eventer(messageEvent, async function (e) { if (!e.data || !e.data.orcid_meta) return; - // TODO: get the session cookie when here and add it to our domain. - // also store the user info once logged from here in the local storage for future usage. - const { code, orcid_meta, cookies } = e.data; - const _cookies = JSON.parse(cookies); - // create a cookie with the name "session" and the value of the session cookie - const sessionCookie = _cookies.find(cookie => cookie.name === "session"); - if (sessionCookie) { - let expires = new Date() - expires.setTime(expires.getTime() + (2 * 24 * 60 * 60 * 1000)); // 2 days - setCookie('session', sessionCookie.value, { path: '/', domain: '.localhost', secure: false, sameSite: false, expires, httpOnly: false }); - } - + const { code, cookies, groupname } = e.data; if (code === 200 || code === 302) { - setUserData({ name: orcid_meta.name, id: orcid_meta.orcid }); + const _cookies = JSON.parse(cookies); + const sessionCookie = _cookies.find(cookie => cookie.name === "session"); + let expires = new Date() + if (sessionCookie) { + expires.setTime(expires.getTime() + (2 * 24 * 60 * 60 * 1000)); // 2 days + setCookie('session', sessionCookie.value, { path: '/', domain: '.localhost', secure: false, sameSite: false, expires, httpOnly: false }); + } + const userData = await requestUserSettings(groupname); + localStorage.setItem(API_CONFIG.SESSION_DATA.SETTINGS, JSON.stringify(userData)); + localStorage.setItem(API_CONFIG.SESSION_DATA.COOKIE, JSON.stringify({ + name: 'session', + value: sessionCookie.value, + expires: expires + })); + setUserData({ + name: userData['groupname'], + id: userData['orcid'], + email: userData?.emails[0]?.email, + role: userData['own-role'], + groupname: userData['groupname'], + settings: userData + }); navigate("/") } else if (code === 401) { setErrors((prev) => ({ diff --git a/src/components/Auth/utils.ts b/src/components/Auth/utils.ts new file mode 100644 index 00000000..261a48ad --- /dev/null +++ b/src/components/Auth/utils.ts @@ -0,0 +1,18 @@ +import { API_CONFIG } from "../../config"; +import { getUser } from "../../api/endpoints"; +import { getUserSettings } from "../../api/endpoints/apiService"; + +export const requestUserSettings = async (group: string) => { + try { + const response = await getUserSettings(group); + if (response.status === 200) { + const data = response.settings; + return data; + } else { + throw new Error(`Error fetching user settings: ${response.statusText}`); + } + } catch (error) { + console.error("Error fetching user settings:", error); + throw error; + } +}; diff --git a/src/config.js b/src/config.js index e76315a7..17b67a54 100644 --- a/src/config.js +++ b/src/config.js @@ -27,9 +27,14 @@ export const API_CONFIG = { ORCID_SIGNIN: "/u/ops/orcid-login", NEWUSER_ILX: "/u/ops/user-new", NEWUSER_ORCID: "/u/ops/orcid-new", + USER_SETTINGS: "/priv/settings", CREATE_NEW_ORGANIZATION: "/priv/org-new", GET_ORGANIZATIONS: "/priv/role-other" }, + SESSION_DATA: { + SETTINGS: "settings", + COOKIE: "session", + }, OLYMPIAN_GODS : "https://uri.olympiangods.org", BASE_SCICRUNCH_URL: "/api/elasticsearch?apikey=", SCICRUNCH_KEY: import.meta.env.VITE_SCICRUNCH_API_KEY, From b89fbe253581c9d59f05351d19ad1c2674582127 Mon Sep 17 00:00:00 2001 From: ddelpiano Date: Thu, 8 May 2025 20:25:32 +0200 Subject: [PATCH 3/5] fix lint issue --- src/App.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 8605ba93..228d8bd2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -29,8 +29,6 @@ import { handleOrcidLogin } from "./api/endpoints"; import { GlobalDataContext } from "./contexts/DataContext"; import { useCookies } from 'react-cookie' import { API_CONFIG } from "./config"; -import { local } from "d3"; - const PageContainer = ({ children }) => { return ( @@ -41,7 +39,7 @@ const PageContainer = ({ children }) => { }; function MainContent() { - const [cookies, setCookie, removeCookie] = useCookies(['session']) + const [cookies] = useCookies(['session']) const cookiesInfo = JSON.parse(localStorage.getItem(API_CONFIG.SESSION_DATA.COOKIE)); const { user, setUserData } = useContext(GlobalDataContext); From c24f00c7536aad56671a43f50f734a34a1adcf6d Mon Sep 17 00:00:00 2001 From: ddelpiano Date: Thu, 8 May 2025 22:11:31 +0200 Subject: [PATCH 4/5] last bits of the user login logic --- src/App.jsx | 2 ++ src/api/endpoints/apiService.ts | 5 +++++ src/components/Auth/Login.jsx | 3 ++- src/components/Header/index.jsx | 10 ++++++---- src/components/organizations/index.jsx | 4 +++- src/config.js | 3 ++- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 228d8bd2..c3d80d0e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -58,6 +58,8 @@ function MainContent() { groupname: userData['groupname'], settings: userData }); + } else { + setUserData({}); } } diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index fb6fdc39..f27e2e0a 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -36,3 +36,8 @@ export const getOrganizations = (group: string) => { const endpoint = `/${group}${API_CONFIG.REAL_API.GET_ORGANIZATIONS}`; return createGetRequest(endpoint, "application/json")(); }; + +export const userLogout = (group: string) => { + const endpoint = `/${group}${API_CONFIG.REAL_API.LOGOUT}`; + return createGetRequest(endpoint, "application/json")(); +}; diff --git a/src/components/Auth/Login.jsx b/src/components/Auth/Login.jsx index f992edfb..9d7aedec 100644 --- a/src/components/Auth/Login.jsx +++ b/src/components/Auth/Login.jsx @@ -106,11 +106,12 @@ const Login = () => { if (!result.data || !result.data?.orcid_meta) { setErrors((prev) => ({ ...prev, - auth: "Interlex API is not returning the user information, please contact the support at support@interlex.org", + auth: "Interlex API is not returning the user information, reminder to ask Tom to send the groupname back so that we can query the priv/setting endpoint to get the rest of the info required", })); } else { const { code, orcid_meta } = result.data; if (code === 200 || code === 302) { + // TODO: the backend should return the groupname, for now is just returning a message. setUserData({ name: orcid_meta.name, id: orcid_meta.orcid }); } navigate("/") diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 9c089dd8..953ee456 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -34,6 +34,7 @@ import { GlobalDataContext } from "../../contexts/DataContext"; import EditBulkTermsDialog from "../Dashboard/EditBulkTerms/EditBulkTermsDialog"; import ModeEditOutlineOutlinedIcon from "@mui/icons-material/ModeEditOutlineOutlined"; import PersonOutlineIcon from '@mui/icons-material/PersonOutline'; +import { userLogout } from "../../api/endpoints/apiService"; import { vars } from "../../theme/variables"; const { gray200, white, gray100, gray600 } = vars; @@ -220,9 +221,10 @@ const Header = () => { const handleMenuClick = (e, menu) => { if (menu.label === 'Log out') { - // TODO: call logout endpoint {group}/priv/logout also - // TODO: flush the userinfo from the localstorage - setUserData(null, null); + userLogout(); + localStorage.removeItem('session'); + localStorage.removeItem('settings'); + setUserData({}); navigate('/'); } if (menu.href) { @@ -250,7 +252,7 @@ const Header = () => { React.useEffect(() => { console.log("Stored user in context ", user) - if(user) { + if(user !== null && user?.groupname !== undefined) { setIsLoggedIn(true) } else { setIsLoggedIn(false) diff --git a/src/components/organizations/index.jsx b/src/components/organizations/index.jsx index d61411a1..75a2c8ba 100644 --- a/src/components/organizations/index.jsx +++ b/src/components/organizations/index.jsx @@ -4,6 +4,7 @@ import { Box, Typography, CircularProgress, Stack, Button, Link } from "@mui/mat import BasicDialog from "../common/BasicDialog"; import GroupAddOutlinedIcon from "@mui/icons-material/GroupAddOutlined"; import { createNewOrganization, getOrganizations } from "../../api/endpoints/apiService"; +import { GlobalDataContext } from "../../contexts/DataContext"; import { vars } from "../../theme/variables"; const { gray600, gray700, brand700, brand800 } = vars; @@ -18,13 +19,14 @@ const linkStyles = { } const Organizations = () => { + const { user } = GlobalDataContext(); const [organizations, setOrganizations] = useState([]); const [loading, setLoading] = useState(false); const [open, setOpen] = useState(false); const [message, setMessage] = useState(); // TODO: change this to be dynamic when we get the response from api call - const groupname = "dariodippi" + const groupname = user?.groupname || "base"; const fetchOrganizations = async() => { setLoading(true); diff --git a/src/config.js b/src/config.js index 17b67a54..0b296f38 100644 --- a/src/config.js +++ b/src/config.js @@ -29,7 +29,8 @@ export const API_CONFIG = { NEWUSER_ORCID: "/u/ops/orcid-new", USER_SETTINGS: "/priv/settings", CREATE_NEW_ORGANIZATION: "/priv/org-new", - GET_ORGANIZATIONS: "/priv/role-other" + GET_ORGANIZATIONS: "/priv/role-other", + LOGOUT: "/priv/logout", }, SESSION_DATA: { SETTINGS: "settings", From dbfc87640300b359f4986e97abe56e4380a9ff7d Mon Sep 17 00:00:00 2001 From: ddelpiano Date: Thu, 8 May 2025 22:13:57 +0200 Subject: [PATCH 5/5] last bits of the user login logic 2 --- src/components/organizations/index.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/organizations/index.jsx b/src/components/organizations/index.jsx index 75a2c8ba..9849c655 100644 --- a/src/components/organizations/index.jsx +++ b/src/components/organizations/index.jsx @@ -30,7 +30,7 @@ const Organizations = () => { const fetchOrganizations = async() => { setLoading(true); - + try { const response = await getOrganizations(groupname) if(response.length > 0){ @@ -46,6 +46,7 @@ const Organizations = () => { useEffect( () => { setLoading(true) fetchOrganizations(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleOpen = () => {