diff --git a/src/api/endpoints/apiService.ts b/src/api/endpoints/apiService.ts index 84866d94..3e2f0387 100644 --- a/src/api/endpoints/apiService.ts +++ b/src/api/endpoints/apiService.ts @@ -79,7 +79,7 @@ export const getSelectedTermLabel = async (searchTerm: string): Promise { +export const createNewEntity = async ({ group, data, session }: { group: string; data: any; session: string }) => { try { const endpoint = `/${group}${API_CONFIG.REAL_API.CREATE_NEW_ENTITY}`; const response = await createPostRequest( @@ -122,5 +122,78 @@ export const createNewEntity = async ({group,data,session}: { group: string; dat status: error?.response?.status, }; } - + +}; + +export const createNewOntology = async ({ + groupname, + token, + ontologyName, + title, + subjects, +}: { + groupname: string; + token: string; + ontologyName: string; + title: string; + subjects: string[]; +}) => { + const endpoint = `/${groupname}/ontologies/uris/${ontologyName}/spec`; + + const data = { + title : title, + subjects : subjects, + }; + + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }; + + try { + const postResponse = await createPostRequest(endpoint, headers)(data); + + // If the POST creates a new location, try fetching it (simulate follow-up GETs from the test) + if (postResponse?.location) { + const getResponse = await fetch(postResponse.location, { + headers: { + Accept: 'application/json', + }, + }); + + // Optionally fetch HTML if needed (like the .html equivalent in the Python test) + const htmlResponse = await fetch(endpoint, { + headers: { + Accept: 'text/html', + }, + }); + + return { + created: true, + data: postResponse, + jsonResponse: await getResponse.json(), + htmlAvailable: htmlResponse.ok, + }; + } + + return { + created: true, + data: postResponse, + }; + } catch (error: any) { + return { + created: false, + error: error?.response?.data || error.message, + }; + } +}; + +export const getNewTokenApi = ({ groupname, data }: { groupname: string, data: any }) => { + const endpoint = `/${groupname}${API_CONFIG.REAL_API.API_NEW_TOKEN}`; + return createPostRequest(endpoint, { "Content-Type" : "application/json" })(data); +}; + +export const retrieveTokenApi = ({ groupname }: { groupname: string }) => { + const endpoint = `/${groupname}${API_CONFIG.REAL_API.API_RETRIEVE_TOKEN}`; + return createGetRequest(endpoint, "application/json")(); }; \ No newline at end of file diff --git a/src/components/SingleOrganization/AddNewOntologyDialog.jsx b/src/components/SingleOrganization/AddNewOntologyDialog.jsx index 19ed7f7f..dbc76619 100644 --- a/src/components/SingleOrganization/AddNewOntologyDialog.jsx +++ b/src/components/SingleOrganization/AddNewOntologyDialog.jsx @@ -1,4 +1,3 @@ -import * as React from "react"; import PropTypes from "prop-types"; import AddIcon from '@mui/icons-material/Add'; import Checkbox from "../common/CustomCheckbox"; @@ -6,6 +5,12 @@ import StatusDialog from "../common/StatusDialog"; import CustomInputBox from "../common/CustomInputBox"; import { Stack, Button, Grid, Box } from "@mui/material"; import CustomizedDialog from "../common/CustomizedDialog"; +import ImportFileTab from "./../TermEditor/ImportFileTab"; +import BasicTabs from "../common/CustomTabs"; +import { useState } from "react"; +import { createNewOntology, getNewTokenApi, retrieveTokenApi } from "../../api/endpoints/apiService"; +import { GlobalDataContext } from "../../contexts/DataContext"; +import { useContext } from "react"; const HeaderRightSideContent = ({ handleClose, onAddNewOntology }) => { return ( @@ -25,14 +30,66 @@ HeaderRightSideContent.propTypes = { } const AddNewOntologyDialog = ({ open, handleClose }) => { - const [openStatusDialog, setOpenStatusDialog] = React.useState(false); - const [newOntology, setNewOntology] = React.useState({ + const [openStatusDialog, setOpenStatusDialog] = useState(false); + const [newOntology, setNewOntology] = useState({ title: "", description: "" }); + const [newOntologyResponse, setNewOntologyResponse] = useState({ + title: "", + description: "", + created : false, + message : "Your ontology “Nervous system” has been added. Click 'Go to Ontology' to go see the result, or add a new ontology." + }); + const [files, setFiles] = useState([]); + const [url, setUrl] = useState(''); + const [tabValue, setTabValue] = useState(0); + const { user } = useContext(GlobalDataContext); + + const handleSubmit = async() => { + const groupname = user?.groupname - const handleSubmit = () => { - console.log("Submit new ontology data!"); + const retrieved_tokens = await retrieveTokenApi({groupname}) + let token = null; + if ( retrieved_tokens?.length > 0 ){ + token = retrieved_tokens?.[retrieved_tokens?.length - 1]?.key; + } + + if ( token === undefined || token === null) { + const newToken = await getNewTokenApi({groupname}); + token = newToken?.key; + } + const ontologyName = newOntology?.title + "_" + Math.random().toString(36).substring(2, 10); + const title = newOntology?.title; + const subjects = files?.[0]?.data?.subjects; + + const result = await createNewOntology({ + groupname, + token, + ontologyName, + title, + subjects, + }); + + let ontologyResponseMessage = "Ontology created successfully!" + + if (result.created) { + console.log('Ontology details:', result.data); + + if (result.jsonResponse) { + console.log('Retrieved JSON:', result.jsonResponse); + } + + if (result.htmlAvailable !== undefined) { + console.log('HTML version available:', result.htmlAvailable); + } + } else { + ontologyResponseMessage = "Failed to create ontology" + console.error('❌ Failed to create ontology:', result.error); + } + + setOpenStatusDialog(true); + setNewOntologyResponse({title : newOntology?.title, description : ontologyResponseMessage, message : ontologyResponseMessage, created : result.created}) } const handleNewOntologyChange = (e) => { @@ -45,8 +102,6 @@ const AddNewOntologyDialog = ({ open, handleClose }) => { const handleAddNewOntology = () => { handleSubmit(); - setOpenStatusDialog(true); - setNewOntology({ title: "", description: "" }) }; const handleCloseStatusDialog = () => { @@ -56,9 +111,56 @@ const AddNewOntologyDialog = ({ open, handleClose }) => { const handleFinishButtonClick = () => { handleClose(); setOpenStatusDialog(false); - setNewOntology({ title: "", description: "" }) } + const handleChangeUrl = (event) => { + setUrl(event.target.value); + } + + const handleFilesSelected = async (newFiles) => { + const fileArray = Array.from(newFiles); + + const readFileContents = (file) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = () => { + let content = reader.result; + + // Try parsing JSON if it's a JSON file + if (file.name.endsWith('.json')) { + try { + content = JSON.parse(content); + } catch (e) { + console.error(`Invalid JSON in file ${file.name}`, e); + content = null; + } + } + + resolve({ + name: file.name, + size: (file.size / 1024).toFixed(2), + progress: 100, + data: content + }); + }; + + reader.onerror = () => reject(reader.error); + reader.readAsText(file); + }); + }; + + const updatedFiles = await Promise.all(fileArray.map(readFileContents)); + + setFiles(prevFiles => { + const prevString = JSON.stringify(prevFiles); + const newString = JSON.stringify(updatedFiles); + return prevString !== newString ? updatedFiles : prevFiles; + }); + }; + + const handleChangeTabs = (_, newValue) => setTabValue(newValue); + return ( <> { /> } > + + + + {tabValue === 0 && ( @@ -101,16 +207,21 @@ const AddNewOntologyDialog = ({ open, handleClose }) => { /> + )} + {tabValue === 1 && } + + ) diff --git a/src/components/SingleOrganization/CreateForkDialog.jsx b/src/components/SingleOrganization/CreateForkDialog.jsx index 0e879396..d2227bc5 100644 --- a/src/components/SingleOrganization/CreateForkDialog.jsx +++ b/src/components/SingleOrganization/CreateForkDialog.jsx @@ -1,16 +1,17 @@ import * as React from "react"; +import { useContext } from "react"; import { debounce } from 'lodash'; import PropTypes from "prop-types"; -import termParser from "../../parsers/termParser"; import CustomInputBox from "../common/CustomInputBox"; -import { getOrganizations } from "../../api/endpoints"; import CustomSelectBox from "../common/CustomSelectBox"; import { useState, useEffect, useCallback } from "react"; import CustomizedDialog from "../common/CustomizedDialog"; import ForkRightIcon from '@mui/icons-material/ForkRight'; import CustomAutocompleteBox from "../common/CustomAutocompleteBox"; import { Stack, Button, Grid, Box, Typography } from "@mui/material"; +import { GlobalDataContext } from "../../contexts/DataContext"; import * as mockApi from "../../api/endpoints/swaggerMockMissingEndpoints"; +import { useOrganizations } from "../../helpers/useOrganizations"; import { vars } from "../../theme/variables"; const { gray800, gray500, gray600 } = vars; @@ -39,32 +40,27 @@ const CreateForkDialog = ({ open, handleClose, onSubmit }) => { const { getMatchTerms } = useMockApi(); // eslint-disable-next-line no-unused-vars const [loading, setLoading] = useState(true); - const [termResults, setTermResults] = useState([]); - const [organizations, setOrganizations] = useState([]); + const [termResults] = useState([]); const [newFork, setNewFork] = React.useState({ term: null, owner: "", name: "" }); + const { user } = useContext(GlobalDataContext); + const groupname = user?.groupname || "base"; + const { + organizations, + } = useOrganizations(groupname); // eslint-disable-next-line react-hooks/exhaustive-deps const fetchTerms = useCallback( - debounce((term) => { + debounce(() => { setLoading(true); - getMatchTerms("base", "i", { filter: "", value: "" }).then(data => { - const parsedData = termParser(data, term); - setTermResults(parsedData.results); - }); + }, 300), [getMatchTerms] ); - const fetchOrganizations = async () => { - const organizations = await getOrganizations("base") - setOrganizations(organizations); - setLoading(false) - } - const handleCreateFork = () => { onSubmit(newFork); }; @@ -90,7 +86,6 @@ const CreateForkDialog = ({ open, handleClose, onSubmit }) => { useEffect(() => { setLoading(true) - fetchOrganizations(); }, []); diff --git a/src/components/SingleOrganization/index.jsx b/src/components/SingleOrganization/index.jsx index fa38eb55..271aacdd 100644 --- a/src/components/SingleOrganization/index.jsx +++ b/src/components/SingleOrganization/index.jsx @@ -71,8 +71,9 @@ const useOrganizationData = (id) => { setLoading(false); } }; - - fetchData(); + if ( id ) { + fetchData(); + } }, [id]); return { organization, organizationCuries, organizationTerms, organizationOntologies, loading }; @@ -93,9 +94,8 @@ const SingleOrganization = () => { const [ontologiesPageOptions, setOntologiesPageOptions] = useState([]); const navigate = useNavigate(); - const id = "1"; // Hardcoded for now - const { organization, organizationTerms, organizationOntologies, loading } = useOrganizationData(id); + const { organization, organizationTerms, organizationOntologies, loading } = useOrganizationData(); useEffect(() => { if (organizationTerms.length > 0) { diff --git a/src/components/TermEditor/ImportFile.jsx b/src/components/TermEditor/ImportFile.jsx index 8638f781..42e79db1 100644 --- a/src/components/TermEditor/ImportFile.jsx +++ b/src/components/TermEditor/ImportFile.jsx @@ -114,7 +114,7 @@ const ImportFile = ({ onFilesSelected }) => { hidden id="browse" onChange={handleFileChange} - accept=".csv" + accept=".csv,.json" multiple /> diff --git a/src/components/common/StatusDialog.jsx b/src/components/common/StatusDialog.jsx index 9ca26c45..15138c47 100644 --- a/src/components/common/StatusDialog.jsx +++ b/src/components/common/StatusDialog.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import { Box } from "@mui/material"; import Button from "@mui/material/Button"; -import { BackgroundPattern } from "../../Icons"; +import { BackgroundPattern, StatusErrorBackgroundPattern } from "../../Icons"; import CustomizedDialog from "./CustomizedDialog"; import Typography from "@mui/material/Typography"; @@ -24,7 +24,7 @@ HeaderRightSideContent.propTypes = { finishButtonEndIcon: PropTypes.node, }; -const StatusDialog = ({ open, handleClose, title, message, subMessage, finishButtonTitle, actionButtonTitle, handleActionButtonClick, finishButtonEndIcon, actionButtonStartIcon }) => { +const StatusDialog = ({ open, handleClose, title, message, subMessage, finishButtonTitle, actionButtonTitle, handleActionButtonClick, finishButtonEndIcon, actionButtonStartIcon, errored }) => { return ( } > - + : + + + } { + console.log('Received response', res); + console.log('Received Response from the Target:', proxyRes.statusCode, req.url); const location = proxyRes.headers['location']; console.log('Received location', location); @@ -95,6 +97,44 @@ export default defineConfig({ res.setHeader('Access-Control-Allow-Credentials', 'true'); }); }, + }, + '^/[^/]+/ontologies/uris/.*/spec': { + target: 'https://uri.olympiangods.org', + changeOrigin: true, + secure: false, + rewrite: path => path, // Keep full path + configure: (proxy) => { + proxy.on('proxyReq', (proxyReq, req) => { + console.log('Proxying ontology spec request:', req.method, req.url); + console.log('Headers:', proxyReq.getHeaders()); + if (req.headers.authorization) { + proxyReq.setHeader('Authorization', req.headers.authorization); + } + }); + + proxy.on('proxyRes', (proxyRes, req, res) => { + console.log('Received response', res); + console.log('Received Response from the Target:', proxyRes.statusCode, req.url); + const location = proxyRes.headers['location']; + console.log('Received location', location); + + if (proxyRes.statusCode === 303 && location) { + // Prevent browser from seeing the actual Location + delete proxyRes.headers['location']; + + // Inject the location into a custom header we can use in Axios + res.setHeader('X-Redirect-Location', location); + } + + // Required for credentialed CORS + const origin = req.headers.origin; + if (origin) { + res.setHeader('Access-Control-Allow-Origin', origin); + } + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Expose-Headers', 'X-Redirect-Location'); + }); + }, } }, },