Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/components/Dashboard/Ontologies/OntologyCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
import {Box, Chip, Grid, Stack, Typography} from "@mui/material";

import { vars } from "../../../theme/variables";
const {gray500, gray700, gray200, brand600, brand200, brand50 } = vars;

const OntologyCard = ({ ontology, onSelect }) => {
const handleClick = () => {
// The cell card overview component is not built yet - the parent shows a
// placeholder dialog for now.
onSelect(ontology);
};

return (
<Grid item xs={12} lg={6} sx={{ cursor: 'pointer' }} onClick={handleClick}>
<Stack spacing={'1rem'} sx={{
borderBottom: `1px solid ${gray200}`,
minHeight: '11.314rem',
p: '1.5rem',
position: 'relative',
'&:hover': {
'&:before': {
position: 'absolute',
top: '1.8rem',
left: '0',
content: '""',
height: '1.5rem',
borderRadius: '3px',
width: '2px',
background: brand600
},
'& .label': {
color: brand600
},
'& .greenChip': {
border: `1px solid ${brand200}`,
background: brand50
},
'& .MuiIconButton-root': { opacity: 1, visibility: 'visible' }
}
}}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Stack direction="row" alignItems="center" spacing={'.75rem'}>
<Typography variant='h6' color={gray700} className='label'>{ontology?.label}</Typography>
{ontology?.badge && <Chip label={ontology.badge} className="greenChip" variant="outlined" />}
</Stack>
</Box>
<Typography variant='body2' color={gray500} sx={{
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
WebkitLineClamp: 2,
textOverflow: 'ellipsis',
}}>
{ontology?.description ? ontology?.description : '-'}
</Typography>
</Stack>
</Grid>
);
};

OntologyCard.propTypes = {
ontology: PropTypes.object.isRequired,
onSelect: PropTypes.func.isRequired
};

export default OntologyCard;
135 changes: 135 additions & 0 deletions src/components/Dashboard/Ontologies/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import OntologyCard from "./OntologyCard";
import { useCallback, useContext, useEffect, useState } from "react";
import { ListIcon, TableChartIcon } from "../../../Icons";
import CustomPagination from "../../common/CustomPagination";
import CustomViewButton from "../../common/CustomViewButton";
import CustomSingleSelect from "../../common/CustomSingleSelect";
import MessageDialog from "../../common/MessageDialog";
import { getOrganizationsOntologies } from "../../../api/endpoints/apiService";
import {Box, ButtonGroup, CircularProgress, Grid, Stack, Typography} from "@mui/material";
import { GlobalDataContext } from "../../../contexts/DataContext";

import { vars } from "../../../theme/variables";
const { gray600 } = vars;

const NOT_IMPLEMENTED_MESSAGE = "The cell card overview is not yet implemented";

const Ontologies = () => {
const { user } = useContext(GlobalDataContext);
const groupname = user?.groupname;
const [loading, setLoading] = useState(false);
const [numberOfVisiblePages, setNumberOfVisiblePages] = useState(8);
const [listView, setListView] = useState('list');
const [ontologies, setOntologies] = useState([]);
const [page, setPage] = useState(1);
const [slicedOntologies, setSlicedOntologies] = useState([]);
const [message, setMessage] = useState('');

const handleNumberOfPagesChange = (v) => {
setNumberOfVisiblePages(v);
setPage(1);
};

const fetchOntologies = useCallback(async () => {
if (!groupname) return;
setLoading(true);
getOrganizationsOntologies(groupname).then(data => {
// Same transform as OntologySearch: derive a readable label, fall back to
// an identifier parsed from the spec URI when there's no title.
const parsed = (Array.isArray(data) ? data : []).map((ontology, index) => {
let fallbackLabel = `Unknown Ontology ${ontology.id || index + 1}`;
if (ontology.uri) {
const uriParts = ontology.uri.split('/');
const specIndex = uriParts.findIndex(part => part === 'spec');
if (specIndex > 0) {
fallbackLabel = `Ontology ${uriParts[specIndex - 1]}`;
}
}
return {
label: ontology.title || fallbackLabel,
badge: groupname,
id: ontology.id || `ontology-${index}`,
description: ontology.uri,
url: ontology.url,
};
});
setOntologies(parsed);
setPage(1);
setLoading(false);
}).catch(err => {
console.log(err);
setLoading(false);
});
}, [groupname]);

useEffect(() => {
fetchOntologies();
}, [fetchOntologies]);

useEffect(() => {
const start = (page - 1) * numberOfVisiblePages;
const end = start + numberOfVisiblePages;
setSlicedOntologies(ontologies.slice(start, end));
}, [ontologies, page, numberOfVisiblePages]);

const handlePageChange = (event, value) => {
setPage(value);
};

const handleSelectOntology = () => {
// Placeholder until the cell card overview component is developed.
setMessage(NOT_IMPLEMENTED_MESSAGE);
};

return (
<Box p='2.5rem 5rem' sx={{
display: 'flex',
flexDirection: 'column',
gap: '1.75rem',
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<Typography fontSize='1.5rem' color={gray600} fontWeight={600}>
My ontologies
</Typography>
<Box display="flex" alignItems="center" gap={2} justifyContent="end">
<Stack direction="row" alignItems="center" gap={1}>
<Typography variant="caption" sx={{ fontSize: '0.875rem', color: gray600 }}>Show on page:</Typography>
<CustomSingleSelect value={numberOfVisiblePages} onChange={handleNumberOfPagesChange} options={['8', '12', '24']} />
</Stack>
<ButtonGroup variant="outlined" aria-label="View mode">
<CustomViewButton
view="list"
listView={listView}
onClick={() => setListView('list')}
icon={<ListIcon />}
/>
<CustomViewButton
view="table"
listView={listView}
disabled
onClick={() => setListView('table')}
icon={<TableChartIcon />}
/>
</ButtonGroup>
</Box>
</Box>
{loading ? <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<CircularProgress />
</Box> : <Grid container spacing='2.75rem'>
{
slicedOntologies.map((ontology, index) => (
<OntologyCard ontology={ontology} onSelect={handleSelectOntology} key={index} />
))
}
</Grid>}
<CustomPagination rowCount={ontologies?.length} rowsPerPage={numberOfVisiblePages} page={page} onPageChange={handlePageChange} />
{message && <MessageDialog title="Cell card overview" open={!!message} handleClose={() => setMessage('')} message={message} />}
</Box>
);
};

export default Ontologies;
2 changes: 2 additions & 0 deletions src/components/Dashboard/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Box} from "@mui/material";
import { useState } from "react";
import EditBulkTermsDialog from "./EditBulkTerms/EditBulkTermsDialog";
import Variants from "./Variants";
import Ontologies from "./Ontologies";
import TermsChange from "./TermsChange";
import Organizations from "./Organizations";
import User from "./User";
Expand All @@ -22,6 +23,7 @@ const Dashboard = () => {
<User />
<Box flexGrow={1} overflow='auto'>
<Variants handleOpenEditBulkTerms={handleOpenEditBulkTerms} />
<Ontologies />
<TermsChange />
<Organizations />
</Box>
Expand Down
35 changes: 32 additions & 3 deletions src/components/SingleOrganization/AddNewOntologyDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import CustomizedDialog from "../common/CustomizedDialog";
import ImportFileTab from "./../TermEditor/ImportFileTab";
import BasicTabs from "../common/CustomTabs";
import { useState, useCallback } from "react";
import { createNewOntology } from "../../api/endpoints/apiService";
import { createNewOntology, getOntologyTerms } from "../../api/endpoints/apiService";
import { API_CONFIG } from "../../config";
import { GlobalDataContext } from "../../contexts/DataContext";
import { useContext } from "react";
Expand Down Expand Up @@ -49,7 +49,8 @@ const AddNewOntologyDialog = ({ open, handleClose, onOntologyAdded, organization
const [files, setFiles] = useState([]);
const [url, setUrl] = useState('');
const [tabValue, setTabValue] = useState(0);
const { user, refreshOntologies } = useContext(GlobalDataContext);
const [setAsActive, setSetAsActive] = useState(false);
const { user, refreshOntologies, setOntologyData } = useContext(GlobalDataContext);

// group used both for the POST endpoint and the immutable URI prefix shown to the user
const groupForUri = organizationName || user?.groupname || "base";
Expand All @@ -64,6 +65,7 @@ const AddNewOntologyDialog = ({ open, handleClose, onOntologyAdded, organization
setFiles([]);
setUrl('');
setTabValue(0);
setSetAsActive(false);
setOpenStatusDialog(false);
setNewOntologyResponse({
title: "",
Expand Down Expand Up @@ -118,6 +120,28 @@ const AddNewOntologyDialog = ({ open, handleClose, onOntologyAdded, organization
// If ontology was created successfully, trigger refresh
if (result.created) {
refreshOntologies();

// Honor the "Set as active ontology" checkbox: push the freshly
// created ontology into global context so the active-ontology
// selector reflects it. The object shape mirrors the transformed
// ontologies produced in OntologySearch (label/badge/id/description/url).
if (setAsActive) {
const ontologyUri = `${API_CONFIG.INTERLEX_URL}/${groupname}/ontologies/uris/${ontologyName}/spec`;
const activeOntology = {
label: title,
badge: groupname,
selected: true,
id: result.newOntologyID || ontologyUri,
description: ontologyUri,
url: result.location,
};
setOntologyData(activeOntology);
// Enrich with member terms once fetched, same as OntologySearch.onSetActive.
getOntologyTerms(ontologyUri)
.then((terms) => setOntologyData({ ...activeOntology, terms }))
.catch(() => setOntologyData({ ...activeOntology, terms: [] }));
}

if (onOntologyAdded) {
onOntologyAdded();
}
Expand Down Expand Up @@ -382,7 +406,12 @@ const AddNewOntologyDialog = ({ open, handleClose, onOntologyAdded, organization
}}
/>
</Stack>
<Checkbox label="Set as active ontology" />
<Checkbox
label="Set as active ontology"
name="setAsActive"
checked={setAsActive}
onChange={(e) => setSetAsActive(e.target.checked)}
/>
</Grid>
<Grid item xs={12} lg={12}>
<CustomFormField
Expand Down
6 changes: 5 additions & 1 deletion src/components/SingleTermView/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,11 @@ const SingleTermView = () => {
</Box>
{versionHash && (
<Box px="5rem" pt="1.5rem">
<Alert severity="info">
<Alert
severity="info"
onClose={() => navigate(`/${group}/${term}/overview`)}
closeText="Back to current term"
>
Viewing a historical version of this term (identity graph <code>{versionHash}</code>). This snapshot is read-only.
</Alert>
</Box>
Expand Down
Loading