From 44d87cd1e13e79384df9b7be01b84ef28d518ff8 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Mon, 8 Jan 2024 12:58:02 +0100 Subject: [PATCH 01/76] #8 add settings ui --- package.json | 1 + .../NodeDetailView/NodeDetailView.js | 14 +++- src/components/NodeDetailView/factory.js | 11 ++- .../NodeDetailView/settings/Settings.js | 46 +++++++++++ .../NodeDetailView/settings/SettingsGroup.js | 61 +++++++++++++++ .../NodeDetailView/settings/SettingsItem.js | 78 +++++++++++++++++++ .../settings/SettingsListItems.js | 72 +++++++++++++++++ src/theme.js | 18 +++++ 8 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 src/components/NodeDetailView/settings/Settings.js create mode 100644 src/components/NodeDetailView/settings/SettingsGroup.js create mode 100644 src/components/NodeDetailView/settings/SettingsItem.js create mode 100644 src/components/NodeDetailView/settings/SettingsListItems.js diff --git a/package.json b/package.json index 48b1431..b98a9e7 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "n3": "^1.13.0", "puppeteer": "13.5.1", "react": "^17.0.2", + "react-beautiful-dnd": "^13.1.1", "react-dom": "^17.0.2", "react-hot-loader": "^4.13.0", "react-redux": "^7.2.4", diff --git a/src/components/NodeDetailView/NodeDetailView.js b/src/components/NodeDetailView/NodeDetailView.js index 1791097..0c22528 100644 --- a/src/components/NodeDetailView/NodeDetailView.js +++ b/src/components/NodeDetailView/NodeDetailView.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useState} from "react"; import { Box } from "@material-ui/core"; import NodeFooter from "./Footers/Footer"; import DetailsFactory from './factory'; @@ -8,8 +8,11 @@ import { IconButton, Tooltip, Link } from '@material-ui/core'; import HelpIcon from '@material-ui/icons/Help'; import { subject_key, protocols_key, contributors_key } from '../../constants'; import config from "./../../config/app.json"; +import {TuneRounded} from "@material-ui/icons"; const NodeDetailView = (props) => { + const [showSettingsContent, setShowSettingsContent] = useState(false); + var otherDetails = undefined; const factory = new DetailsFactory(); const nodeSelected = useSelector(state => state.sdsState.instance_selected); @@ -87,6 +90,9 @@ const NodeDetailView = (props) => { text: nodeSelected.graph_node.name }; } + const toggleContent = () => { + setShowSettingsContent(!showSettingsContent); + }; return ( @@ -95,8 +101,12 @@ const NodeDetailView = (props) => { {/**{ nodeDetails.getHeader() }*/} { otherDetails } - { nodeDetails.getDetail() } + { showSettingsContent ? nodeDetails.getSettings() : nodeDetails.getDetail() } + + { !showSettingsContent && + + } ); }; diff --git a/src/components/NodeDetailView/factory.js b/src/components/NodeDetailView/factory.js index ec4ca8d..413e772 100644 --- a/src/components/NodeDetailView/factory.js +++ b/src/components/NodeDetailView/factory.js @@ -12,9 +12,8 @@ import SampleDetails from './Details/SampleDetails'; import DatasetDetails from './Details/DatasetDetails'; import SubjectDetails from './Details/SubjectDetails'; import ProtocolDetails from './Details/ProtocolDetails'; -import CollectionDetails from './Details/CollectionDetails'; import GroupDetails from './Details/GroupDetails'; - +import Settings from "./settings/Settings" var DetailsFactory = function () { this.createDetails = function (node) { let details = null; @@ -147,6 +146,14 @@ const Dataset = function (node) { ) } + + nodeDetail.getSettings = () => { + return ( + <> + + + ) + } return nodeDetail; }; diff --git a/src/components/NodeDetailView/settings/Settings.js b/src/components/NodeDetailView/settings/Settings.js new file mode 100644 index 0000000..787de8e --- /dev/null +++ b/src/components/NodeDetailView/settings/Settings.js @@ -0,0 +1,46 @@ +import React from "react"; +import { Box, Button, Typography } from "@material-ui/core"; +import SettingsGroup from "./SettingsGroup"; +import FolderIcon from "@material-ui/icons/Folder"; +const Settings = props => { + return ( + + + + + First List Title + + + + + + + + + ); +}; + +export default Settings; diff --git a/src/components/NodeDetailView/settings/SettingsGroup.js b/src/components/NodeDetailView/settings/SettingsGroup.js new file mode 100644 index 0000000..7a5be27 --- /dev/null +++ b/src/components/NodeDetailView/settings/SettingsGroup.js @@ -0,0 +1,61 @@ +import React, { useState } from "react"; +import { Box } from "@material-ui/core"; +import { DragDropContext, Droppable } from "react-beautiful-dnd"; +import SettingsListItems from "./SettingsListItems"; +const SettingsGroup = props => { + const [items, setItems] = useState([ + { id: "1", primary: "Created on", disabled: false, visible: true }, + { id: "2", primary: "Remote ID", disabled: false, visible: true }, + { id: "3", primary: "Mimetype", disabled: false, visible: true }, + { id: "4", primary: "Dataset", disabled: false, visible: true }, + { id: "5", primary: "Dataset Path", disabled: false, visible: true } + ]); + + const handleDragEnd = result => { + if (!result.destination) return; + + const itemsCopy = [...items]; + const [reorderedItem] = itemsCopy.splice(result.source.index, 1); + itemsCopy.splice(result.destination.index, 0, reorderedItem); + + setItems(itemsCopy); + }; + + const toggleItemDisabled = itemId => { + const itemIndex = items.findIndex(item => item.id === itemId); + + if (itemIndex === -1) return; + + const updatedItems = [...items]; + const [toggledItem] = updatedItems.splice(itemIndex, 1); // Remove the item from its current position + + // If the item is currently disabled + if (toggledItem.disabled) { + // Move the item to the top of the list by unshifting it + updatedItems.unshift({ ...toggledItem, disabled: false, visible: true }); + } else { + // Toggle the disabled and visible properties + updatedItems.push({ ...toggledItem, disabled: true, visible: false }); + } + + setItems(updatedItems); + }; + + return ( + + + + {provided => ( + + )} + + + + ); +}; + +export default SettingsGroup; diff --git a/src/components/NodeDetailView/settings/SettingsItem.js b/src/components/NodeDetailView/settings/SettingsItem.js new file mode 100644 index 0000000..85120ed --- /dev/null +++ b/src/components/NodeDetailView/settings/SettingsItem.js @@ -0,0 +1,78 @@ +import React from "react"; +import { + Typography, + ListItemText, + ListItem, + ListItemSecondaryAction, + IconButton +} from "@material-ui/core"; +import ReorderIcon from "@material-ui/icons/Reorder"; +import RemoveCircleOutlineIcon from "@material-ui/icons/RemoveCircleOutline"; +import AddCircleOutlineIcon from "@material-ui/icons/AddCircleOutline"; +import VisibilityOffRoundedIcon from "@material-ui/icons/VisibilityOffRounded"; +const SettingsItem = props => { + const { item, toggleItemDisabled } = props; + + return ( + + {item.visible ? ( + + ) : ( + + )} + + {item.primary} + + } + /> + + + toggleItemDisabled(item.id)} + > + {item.disabled ? ( + + ) : ( + + )} + + + + ); +}; + +export default SettingsItem; diff --git a/src/components/NodeDetailView/settings/SettingsListItems.js b/src/components/NodeDetailView/settings/SettingsListItems.js new file mode 100644 index 0000000..07529a1 --- /dev/null +++ b/src/components/NodeDetailView/settings/SettingsListItems.js @@ -0,0 +1,72 @@ +import React, { useState } from "react"; +import { + Box, + Typography, + List, + ListItemText, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + IconButton, + ListSubheader, + Button +} from "@material-ui/core"; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import ReorderIcon from "@material-ui/icons/Reorder"; +import RemoveCircleOutlineIcon from "@material-ui/icons/RemoveCircleOutline"; +import AddCircleOutlineIcon from "@material-ui/icons/AddCircleOutline"; +import VisibilityIcon from "@material-ui/icons/Visibility"; +import { TuneRounded } from "@material-ui/icons"; +import FolderIcon from "@material-ui/icons/Folder"; +import VisibilityOffRoundedIcon from "@material-ui/icons/VisibilityOffRounded"; +import { SPARC_DATASETS } from "../../../constants"; +import SettingsItem from "./SettingsItem"; +const SettingsListItems = props => { + const { provided, items, toggleItemDisabled } = props; + + return ( + + + + Title + + + + } + > + {items.map((item, index) => ( + + {provided => ( + + + + )} + + ))} + {provided.placeholder} + + ); +}; + +export default SettingsListItems; diff --git a/src/theme.js b/src/theme.js index db4a58c..9f1b5de 100644 --- a/src/theme.js +++ b/src/theme.js @@ -414,6 +414,24 @@ const theme = createTheme({ display: 'none', }, }, + '& .overlay-button-container': { + position: 'sticky', + bottom: 0, + zIndex: 1000, + padding: '20px 0', + background: 'linear-gradient(180deg, rgb(255 255 255 / 87%) 8%, #FFF 100%)', + display: 'flex', + justifyContent: 'center', + }, + '& .overlay-button': { + padding: '10px 20px', + width: '3rem', + height: '3rem', + backgroundColor: 'rgba(46, 58, 89, 0.10)', + color: '#2E3A59', + borderRadius: '50%', + cursor: 'pointer', + }, '.dialog': { '&_body': { background: dialogBodyBgColor, From 3cbe5348f0109b30064aac029c07971ee3f343be Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 10 Jan 2024 19:33:03 +0100 Subject: [PATCH 02/76] #sdsv-7 Expand Metadata for Dataset Panel --- .../NodeDetailView/Details/DatasetDetails.js | 153 +++--------- .../NodeDetailView/NodeDetailView.js | 97 +++----- src/utils/graphModel.js | 235 ++++++++++++++---- 3 files changed, 248 insertions(+), 237 deletions(-) diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index a63ddd5..70ab84c 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -10,142 +10,51 @@ import SimpleLinkedChip from './Views/SimpleLinkedChip'; import USER from "../../../images/user.svg"; import SimpleLabelValue from './Views/SimpleLabelValue'; import { detailsLabel } from '../../../constants'; +import { rdfTypes } from "../../../utils/graphModel"; const DatasetDetails = (props) => { const { node } = props; - const nodes = window.datasets[node.dataset_id].splinter.nodes; - let title = ""; - let label = ""; - let idDetails = ""; - let description = ""; - // both tree and graph nodes are present, extract data from both - if (node?.tree_node && node?.graph_node) { - idDetails = node.graph_node?.id + detailsLabel; - label = node?.graph_node.attributes?.label?.[0]; - title = node?.graph_node.attributes?.title?.[0]; - description = node?.graph_node.attributes?.description?.[0]; - // the below is the case where we have data only from the tree/hierarchy - } else if (node?.tree_node) { - label = node?.tree_node?.basename; - idDetails = node?.tree_node?.id + detailsLabel; - // the below is the case where we have data only from the graph - } else { - idDetails = node.graph_node?.id + detailsLabel; - label = node.graph_node?.attributes?.label?.[0]; - } - - let latestUpdate = "Not defined." - if (node?.graph_node?.attributes?.latestUpdate !== undefined) { - latestUpdate = new Date(node.graph_node.attributes?.latestUpdate?.[0]) - } + let datasetPropertiesModel = {...rdfTypes["Dataset"].properties}; - let contactPerson = []; - if (node.graph_node.attributes?.hasResponsiblePrincipalInvestigator !== undefined) { - node.graph_node.attributes?.hasResponsiblePrincipalInvestigator.map(user => { - const contributor = nodes.get(user); - contactPerson.push({ - name: contributor?.name, - designation: 'Principal Investigator', - img: USER - }); - return user; - }); + const isValidUrl = (urlString) => { + var urlPattern = new RegExp('^(https?:\\/\\/)?' + // validate protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string + '(\\#[-a-z\\d_]*)?$', 'i'); // validate fragment locator + return !!urlPattern.test(urlString); } - if (node.graph_node.attributes?.hasContactPerson !== undefined) { - node.graph_node.attributes?.hasContactPerson.map(user => { - const contributor = nodes.get(user); - contactPerson.push({ - name: contributor?.name, - designation: 'Contributor', - img: USER - }); - return user; - }); - } - - const DETAILS_LIST = [ - { - title: 'Error Index', - value: node.graph_node.attributes?.errorIndex - }, - { - title: 'Template Schema Version', - value: node.graph_node.attributes?.hasDatasetTemplateSchemaVersion - }, - { - title: 'Experiment Modality', - value: node.graph_node.attributes?.hasExperimentalModality - } - ]; - return ( + - - { node.graph_node.attributes?.hasDoi && node.graph_node.attributes?.hasDoi?.[0] !== "" - ? ( - Label - - ) - : () - } - - - - - - About - - - - - Protocol Techniques - - + {datasetPropertiesModel?.map( property => { + if ( property.visible ){ + const propValue = node.graph_node.attributes[property.property]?.[0]; + if ( isValidUrl(propValue) ){ + return ( + {property.label} + + ) + } - - - { - DETAILS_LIST?.map((item, index) => ( - - {item?.title} - {item?.value} - - )) + if ( typeof propValue === "object" ){ + return ( + {property.label} + + ) } - - - { node.graph_node.attributes?.hasExperimentalApproach !== undefined - ? ( - ) - : <> - } + if ( typeof propValue === "string" ){ + return () + } - { node.graph_node.attributes?.hasDoi !== undefined - ? ( - Links - - ) - : <> - } - { node.graph_node.attributes?.hasAdditionalFundingInformation !== undefined - ? ( - ) - : <> - } - { node.graph_node.attributes?.statusOnPlatform !== undefined - ? ( - ) - : <> - } - { node.graph_node.attributes?.hasLicense !== undefined - ? ( - ) - : <> - } + return (<> ) + } + })} ); diff --git a/src/components/NodeDetailView/NodeDetailView.js b/src/components/NodeDetailView/NodeDetailView.js index 1791097..48843be 100644 --- a/src/components/NodeDetailView/NodeDetailView.js +++ b/src/components/NodeDetailView/NodeDetailView.js @@ -1,13 +1,9 @@ -import React from "react"; import { Box } from "@material-ui/core"; import NodeFooter from "./Footers/Footer"; import DetailsFactory from './factory'; import { useSelector } from 'react-redux' import Breadcrumbs from "./Details/Views/Breadcrumbs"; -import { IconButton, Tooltip, Link } from '@material-ui/core'; -import HelpIcon from '@material-ui/icons/Help'; import { subject_key, protocols_key, contributors_key } from '../../constants'; -import config from "./../../config/app.json"; const NodeDetailView = (props) => { var otherDetails = undefined; @@ -22,71 +18,38 @@ const NodeDetailView = (props) => { } }; var path = [] - if (nodeSelected.tree_node.path !== undefined && nodeSelected.tree_node !== null) { - path = [...nodeSelected.tree_node.path] - path.shift(); - otherDetails = path.reverse().map( singleNode => { - const tree_node = window.datasets[nodeSelected.dataset_id].splinter.tree_map.get(singleNode); - const new_node = { - dataset_id: nodeSelected.dataset_id, - graph_node: tree_node.graph_reference, - tree_node: tree_node - } - // I don't like the check on primary and derivative below since this depends on the data - // but it's coming as a feature request, so I guess it can stay there. - if (new_node.tree_node.id !== subject_key - && new_node.tree_node.id !== contributors_key - && new_node.tree_node.id !== protocols_key - && new_node.tree_node.basename !== 'primary' - && new_node.tree_node.basename !== 'derivative') { - links.pages.push({ - id: singleNode, - title: tree_node.text, - href: '#' - }); - return factory.createDetails(new_node).getDetail() - } - return <> ; - }); - links.current = { - id: nodeSelected.tree_node.id, - text: nodeSelected.tree_node.text - }; - } else { - path = []; - var latestNodeVisited = nodeSelected; - while ( latestNodeVisited.graph_node.parent !== undefined ) { - path.push(latestNodeVisited.graph_node.parent.id); - latestNodeVisited = { - tree_node: undefined, - graph_node: latestNodeVisited.graph_node.parent - }; + var latestNodeVisited = nodeSelected; + while ( latestNodeVisited.graph_node.parent !== undefined ) { + path.push(latestNodeVisited.graph_node.parent.id); + latestNodeVisited = { + tree_node: undefined, + graph_node: latestNodeVisited.graph_node.parent }; + }; - otherDetails = path.reverse().map( singleNode => { - const graph_node = window.datasets[nodeSelected.dataset_id].splinter.nodes.get(singleNode); - const new_node = { - dataset_id: nodeSelected.dataset_id, - graph_node: graph_node, - tree_node: graph_node.tree_reference - } - if (new_node.graph_node.id !== subject_key - && new_node.graph_node.id !== contributors_key - && new_node.graph_node.id !== protocols_key) { - links.pages.push({ - id: singleNode, - title: graph_node.name, - href: '#' - }); - return factory.createDetails(new_node).getDetail() - } - return <> ; - }); - links.current = { - id: nodeSelected.graph_node.id, - text: nodeSelected.graph_node.name - }; - } + otherDetails = path.reverse().map( singleNode => { + const graph_node = window.datasets[nodeSelected.dataset_id].splinter.nodes.get(singleNode); + const new_node = { + dataset_id: nodeSelected.dataset_id, + graph_node: graph_node, + tree_node: graph_node.tree_reference + } + if (new_node.graph_node.id !== subject_key + && new_node.graph_node.id !== contributors_key + && new_node.graph_node.id !== protocols_key) { + links.pages.push({ + id: singleNode, + title: graph_node.name, + href: '#' + }); + return factory.createDetails(new_node).getDetail() + } + return <> ; + }); + links.current = { + id: nodeSelected.graph_node.id, + text: nodeSelected.graph_node.name + }; return ( diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index 197fb8d..fd0cb21 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -77,125 +77,264 @@ export const rdfTypes = { "image": "./images/graph/dataset.svg", "key": "Dataset", "properties": [ - { - "type": "rdfs", - "key": "label", - "property": "label", - "label": "To be filled" - }, - { - "type": "TEMP", - "key": "hasUriHuman", - "property": "hasUriHuman", - "label": "To be filled" - }, { "type": "dc", "key": "title", "property": "title", - "label": "To be filled" + "label": "Title", + "visible" : true + }, + { + "type": "rdfs", + "key": "label", + "property": "label", + "label": "Label", + "visible" : true }, { "type": "dc", "key": "description", "property": "description", - "label": "To be filled" + "label": "Description", + "visible" : true + }, + { + "type": "TEMP", + "key": "contentsWereUpdatedAtTime", + "property": "latestUpdate", + "label": "Contents Updated On", + "visible" : true }, { "type": "isAbout", "key": "", "property": "isAbout", - "label": "To be filled" + "label": "About", + "visible" : true }, { "type": "TEMP", - "key": "contentsWereUpdatedAtTime", - "property": "latestUpdate", - "label": "To be filled" + "key": "protocolEmploysTechnique", + "property": "protocolEmploysTechnique", + "label": "Protocol Employs Technique", + "visible" : true }, { "type": "TEMP", "key": "errorIndex", "property": "errorIndex", - "label": "To be filled" + "label": "Error Index", + "visible" : true }, { "type": "TEMP", - "key": "hasAwardNumber", - "property": "hasAwardNumber", - "label": "To be filled" + "key": "hasDatasetTemplateSchemaVersion", + "property": "hasDatasetTemplateSchemaVersion", + "label": "Template Schema Version", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasExperimentalModality", + "property": "hasExperimentalModality", + "label": "Experimental Modality", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasExperimentalApproach", + "property": "hasExperimentalApproach", + "label": "Experimental Approach", + "visible" : true }, { "type": "TEMP", "key": "hasDoi", "property": "hasDoi", - "label": "To be filled" + "label": "DOI", + "visible" : true }, { "type": "TEMP", - "key": "hasDatasetTemplateSchemaVersion", - "property": "hasDatasetTemplateSchemaVersion", - "label": "To be filled" + "key": "hasAdditionalFundingInformation", + "property": "hasAdditionalFundingInformation", + "label": "Additional Funding Information", + "visible" : true }, { "type": "TEMP", - "key": "hasExperimentalModality", - "property": "hasExperimentalModality", - "label": "To be filled" + "key": "statusOnPlatform", + "property": "statusOnPlatform", + "label": "Status On Platform", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasLicense", + "property": "hasLicense", + "label": "License", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasUriHuman", + "property": "hasUriHuman", + "label": "URI Human", + "visible" : true + }, + { + "type": "TEMP", + "key": "curationIndex", + "property": "curationIndex", + "label": "Curation Index", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasAwardNumber", + "property": "hasAwardNumber", + "label": "Award Number", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasExpectedNumberOfSamples", + "property": "hasExpectedNumberOfSamples", + "label": "Expected Number of Samples", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasExpectedNumberOfSubjects", + "property": "hasExpectedNumberOfSubjects", + "label": "Expected Number of Subjects", + "visible" : true }, { "type": "TEMP", "key": "hasResponsiblePrincipalInvestigator", "property": "hasResponsiblePrincipalInvestigator", - "label": "To be filled" + "label": "Responsible Principal Investigator", + "visible" : true }, { "type": "TEMP", "key": "hasUriApi", "property": "hasUriApi", - "label": "To be filled" + "label": "URI API", + "visible" : true }, { "type": "TEMP", "key": "hasProtocol", "property": "hasProtocol", - "label": "To be filled" + "label": "Protocol", + "visible" : true }, { "type": "TEMP", "key": "hasUriHuman", "property": "hasUriHuman", - "label": "To be filled" + "label": "URI Human", + "visible" : true }, { "type": "TEMP", - "key": "protocolEmploysTechnique", - "property": "protocolEmploysTechnique", - "label": "To be filled" + "key": "hasNumberOfContributors", + "property": "hasNumberOfContributors", + "label": "Number of Contributors", + "visible" : true }, { "type": "TEMP", - "key": "hasAdditionalFundingInformation", - "property": "hasAdditionalFundingInformation", - "label": "To be filled" + "key": "hasNumberOfDirectories", + "property": "hasNumberOfDirectories", + "label": "Number of Directories", + "visible" : true }, { "type": "TEMP", - "key": "hasExperimentalApproach", - "property": "hasExperimentalApproach", - "label": "To be filled" + "key": "hasNumberOfFiles", + "property": "hasNumberOfFiles", + "label": "Number of Files", + "visible" : true }, { "type": "TEMP", - "key": "hasLicense", - "property": "hasLicense", - "label": "To be filled" + "key": "hasNumberOfPerformances", + "property": "hasNumberOfPerformances", + "label": "Number of Performances", + "visible" : true }, { "type": "TEMP", - "key": "statusOnPlatform", - "property": "statusOnPlatform", - "label": "To be filled" + "key": "hasNumberOfSamples", + "property": "hasNumberOfSamples", + "label": "Number of Samples", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasNumberOfSubjects", + "property": "hasNumberOfSubjects", + "label": "Number of Subjects", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasPathErrorReport", + "property": "hasPathErrorReport", + "label": "Path Error Report", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasSizeInBytes", + "property": "hasSizeInBytes", + "label": "Size In Bytes", + "visible" : true + }, + { + "type": "TEMP", + "key": "milestoneCompletionDate", + "property": "milestoneCompletionDate", + "label": "Milestone Completion Date", + "visible" : true + }, + { + "type": "TEMP", + "key": "speciesCollectedFrom", + "property": "speciesCollectedFrom", + "label": "Species Collected From", + "visible" : true + }, + { + "type": "TEMP", + "key": "submissionIndex", + "property": "submissionIndex", + "label": "Submission Index", + "visible" : true + }, + { + "type": "TEMP", + "key": "unclassifiedIndex", + "property": "unclassifiedIndex", + "label": "Unclassified Index", + "visible" : true + }, + { + "type": "TEMP", + "key": "wasCreatedAtTime", + "property": "wasCreatedAtTime", + "label": "Created At", + "visible" : true + }, + { + "type": "TEMP", + "key": "wasUpdatedAtTime", + "property": "wasUpdatedAtTime", + "label": "Updated Last On", + "visible" : true } ] }, From e475812fce30014b96c071e0942ce6ef04ac7eb8 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 10 Jan 2024 19:34:18 +0100 Subject: [PATCH 03/76] Fix typo --- src/components/NodeDetailView/Details/DatasetDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index 70ab84c..00957f5 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -29,7 +29,7 @@ const DatasetDetails = (props) => { return ( - + {datasetPropertiesModel?.map( property => { if ( property.visible ){ From 7bc54fd475c914468477761f8fbc6c4880be0af4 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 10 Jan 2024 21:03:38 +0100 Subject: [PATCH 04/76] Merge fixes --- src/components/NodeDetailView/Details/DatasetDetails.js | 2 +- src/utils/graphModel.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index 00957f5..68d838a 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -15,7 +15,7 @@ import { rdfTypes } from "../../../utils/graphModel"; const DatasetDetails = (props) => { const { node } = props; - let datasetPropertiesModel = {...rdfTypes["Dataset"].properties}; + let datasetPropertiesModel = [...rdfTypes["Dataset"].properties]; const isValidUrl = (urlString) => { var urlPattern = new RegExp('^(https?:\\/\\/)?' + // validate protocol diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index a734986..e893c1e 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -98,6 +98,13 @@ export const rdfTypes = { "label": "Description", "visible" : true }, + { + "type": "TEMP", + "key": "hasUriPublished", + "property": "hasUriPublished", + "label": "Published URI", + "visible" : true + }, { "type": "TEMP", "key": "contentsWereUpdatedAtTime", From 81d54bb1cb709a71193126683359b141e43c8fea Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 10 Jan 2024 21:47:29 +0100 Subject: [PATCH 05/76] Metadata panel details header --- .../NodeDetailView/Details/GroupDetails.js | 2 +- .../NodeDetailView/Details/SubjectDetails.js | 25 ++----------------- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/components/NodeDetailView/Details/GroupDetails.js b/src/components/NodeDetailView/Details/GroupDetails.js index dbfad60..50e92c9 100644 --- a/src/components/NodeDetailView/Details/GroupDetails.js +++ b/src/components/NodeDetailView/Details/GroupDetails.js @@ -27,7 +27,7 @@ const GroupDetails = (props) => { return ( - + ); diff --git a/src/components/NodeDetailView/Details/SubjectDetails.js b/src/components/NodeDetailView/Details/SubjectDetails.js index fa19c57..fb74576 100644 --- a/src/components/NodeDetailView/Details/SubjectDetails.js +++ b/src/components/NodeDetailView/Details/SubjectDetails.js @@ -29,17 +29,6 @@ const SubjectDetails = (props) => { title = node.tree_node.basename; } - const DETAILS_LIST = [ - { - title: 'Weight Unit', - value: node.graph_node.attributes?.weightUnit - }, - { - title: 'Weight Value', - value: node.graph_node.attributes?.weightValue - } - ]; - const getGroupNode = (groupName, node)=> { let n = node.graph_node.parent; let match = false; @@ -57,19 +46,9 @@ const SubjectDetails = (props) => { } return ( - + - { node.graph_node.attributes?.hasUriHuman && node.graph_node.attributes?.hasUriHuman[0] !== "" - ? ( - {"Subject Details"} - Label - - ) - : (( - Subject Details - - )) - } + { node.graph_node?.attributes?.publishedURI && node.graph_node?.attributes?.publishedURI !== "" ? ( From ea783bc28c7551aa5f46820ee5a63a04f85cd818 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Thu, 11 Jan 2024 09:40:02 +0100 Subject: [PATCH 06/76] #sdsv-7 move url check method to utils --- .../NodeDetailView/Details/DatasetDetails.js | 11 +---------- src/components/NodeDetailView/Details/utils.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index 68d838a..ded88b8 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -11,22 +11,13 @@ import USER from "../../../images/user.svg"; import SimpleLabelValue from './Views/SimpleLabelValue'; import { detailsLabel } from '../../../constants'; import { rdfTypes } from "../../../utils/graphModel"; +import { isValidUrl } from './utils'; const DatasetDetails = (props) => { const { node } = props; let datasetPropertiesModel = [...rdfTypes["Dataset"].properties]; - const isValidUrl = (urlString) => { - var urlPattern = new RegExp('^(https?:\\/\\/)?' + // validate protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name - '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string - '(\\#[-a-z\\d_]*)?$', 'i'); // validate fragment locator - return !!urlPattern.test(urlString); - } - return ( diff --git a/src/components/NodeDetailView/Details/utils.js b/src/components/NodeDetailView/Details/utils.js index 1ad8c7e..d9219b8 100644 --- a/src/components/NodeDetailView/Details/utils.js +++ b/src/components/NodeDetailView/Details/utils.js @@ -18,3 +18,13 @@ export const simpleValue = (label, value) => { return (<> ); } } + +export const isValidUrl = (urlString) => { + var urlPattern = new RegExp('^(https?:\\/\\/)?' + // validate protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string + '(\\#[-a-z\\d_]*)?$', 'i'); // validate fragment locator + return !!urlPattern.test(urlString); +} \ No newline at end of file From 93fa79cae4c11026d5643795f9edf3db0c62753c Mon Sep 17 00:00:00 2001 From: jrmartin Date: Thu, 11 Jan 2024 17:47:38 +0100 Subject: [PATCH 07/76] #sdsv-8 Add flag to control settings panel visibility to redux store --- src/components/NodeDetailView/NodeDetailView.js | 14 ++++++-------- src/components/NodeDetailView/settings/Settings.js | 14 ++++++++++++-- src/redux/actions.js | 7 ++++++- src/redux/initialState.js | 5 ++++- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/components/NodeDetailView/NodeDetailView.js b/src/components/NodeDetailView/NodeDetailView.js index 0c22528..758d374 100644 --- a/src/components/NodeDetailView/NodeDetailView.js +++ b/src/components/NodeDetailView/NodeDetailView.js @@ -1,21 +1,20 @@ -import React, {useState} from "react"; import { Box } from "@material-ui/core"; import NodeFooter from "./Footers/Footer"; import DetailsFactory from './factory'; -import { useSelector } from 'react-redux' import Breadcrumbs from "./Details/Views/Breadcrumbs"; -import { IconButton, Tooltip, Link } from '@material-ui/core'; -import HelpIcon from '@material-ui/icons/Help'; +import { IconButton } from '@material-ui/core'; import { subject_key, protocols_key, contributors_key } from '../../constants'; -import config from "./../../config/app.json"; import {TuneRounded} from "@material-ui/icons"; +import { useSelector, useDispatch } from 'react-redux' +import { toggleSettingsPanelVisibility } from '../../redux/actions'; const NodeDetailView = (props) => { - const [showSettingsContent, setShowSettingsContent] = useState(false); + const dispatch = useDispatch(); var otherDetails = undefined; const factory = new DetailsFactory(); const nodeSelected = useSelector(state => state.sdsState.instance_selected); + const showSettingsContent = useSelector(state => state.sdsState.settings_panel_visible); const nodeDetails = factory.createDetails(nodeSelected); let links = { pages: [], @@ -91,7 +90,7 @@ const NodeDetailView = (props) => { }; } const toggleContent = () => { - setShowSettingsContent(!showSettingsContent); + dispatch(toggleSettingsPanelVisibility(!showSettingsContent)); }; return ( @@ -99,7 +98,6 @@ const NodeDetailView = (props) => { - {/**{ nodeDetails.getHeader() }*/} { otherDetails } { showSettingsContent ? nodeDetails.getSettings() : nodeDetails.getDetail() } diff --git a/src/components/NodeDetailView/settings/Settings.js b/src/components/NodeDetailView/settings/Settings.js index 787de8e..b3d13ae 100644 --- a/src/components/NodeDetailView/settings/Settings.js +++ b/src/components/NodeDetailView/settings/Settings.js @@ -1,8 +1,18 @@ -import React from "react"; import { Box, Button, Typography } from "@material-ui/core"; import SettingsGroup from "./SettingsGroup"; import FolderIcon from "@material-ui/icons/Folder"; +import { useSelector, useDispatch } from 'react-redux' +import { toggleSettingsPanelVisibility } from '../../../redux/actions'; + + const Settings = props => { + const dispatch = useDispatch(); + const showSettingsContent = useSelector(state => state.sdsState.settings_panel_visible); + + const save = () => { + dispatch(toggleSettingsPanelVisibility(!showSettingsContent)); + }; + return ( { justifyContent: "center" }} > - diff --git a/src/redux/actions.js b/src/redux/actions.js index 5fb0672..8a4de92 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -4,13 +4,13 @@ export const SET_DATASET_LIST = 'SET_DATASET_LIST' export const SELECT_INSTANCE = 'SELECT_INSTANCE' export const TRIGGER_ERROR = 'TRIGGER_ERROR' export const SELECT_GROUP = 'SELECT_GROUP' +export const TOGGLE_METADATA_SETTINGS = 'TOGGLE_METADATA_SETTINGS' export const addDataset = dataset => ({ type: ADD_DATASET, data: { dataset: dataset }, }); - export const deleteDataset = dataset_id => ({ type: DELETE_DATASET, data: { dataset_id: dataset_id }, @@ -44,4 +44,9 @@ export const selectGroup = instance => ({ export const triggerError = message => ({ type: TRIGGER_ERROR, data: { error_message: message }, +}); + +export const toggleSettingsPanelVisibility = visible => ({ + type: TOGGLE_METADATA_SETTINGS, + data: { visible: visible }, }); \ No newline at end of file diff --git a/src/redux/initialState.js b/src/redux/initialState.js index 8e8a69f..306097b 100644 --- a/src/redux/initialState.js +++ b/src/redux/initialState.js @@ -19,7 +19,8 @@ export const sdsInitialState = { tree_node: null, source: "" }, - layout : {} + layout : {}, + settings_panel_visible : false } }; @@ -107,6 +108,8 @@ export default function sdsClientReducer(state = {}, action) { break; case LayoutActions.layoutActions.SET_LAYOUT: return { ...state, layout : action.data.layout}; + case Actions.TOGGLE_METADATA_SETTINGS: + return { ...state, settings_panel_visible : action.data.visible}; default: return state; } From b13fdc4ccd8091adfc5963dbbae7b80e5b0db9cf Mon Sep 17 00:00:00 2001 From: jrmartin Date: Thu, 11 Jan 2024 22:34:13 +0100 Subject: [PATCH 08/76] #SDSV-8 Add settings to all other nodes that needed it --- .../NodeDetailView/NodeDetailView.js | 2 +- src/components/NodeDetailView/factory.js | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/components/NodeDetailView/NodeDetailView.js b/src/components/NodeDetailView/NodeDetailView.js index d5fd9aa..3b9250e 100644 --- a/src/components/NodeDetailView/NodeDetailView.js +++ b/src/components/NodeDetailView/NodeDetailView.js @@ -66,7 +66,7 @@ const NodeDetailView = (props) => { { otherDetails } - { showSettingsContent ? nodeDetails.getSettings() : nodeDetails.getDetail() } + { showSettingsContent && nodeDetails.getSettings ? nodeDetails.getSettings() : nodeDetails.getDetail() } { !showSettingsContent && diff --git a/src/components/NodeDetailView/factory.js b/src/components/NodeDetailView/factory.js index 413e772..9fa0e22 100644 --- a/src/components/NodeDetailView/factory.js +++ b/src/components/NodeDetailView/factory.js @@ -84,6 +84,14 @@ const Collection = function (node) { ) } + + nodeDetail.getSettings = () => { + return ( + <> + + + ) + } return nodeDetail; }; @@ -115,6 +123,14 @@ const Group = function (node) { ) } + + nodeDetail.getSettings = () => { + return ( + <> + + + ) + } return nodeDetail; }; @@ -216,6 +232,14 @@ const Sample = function (node) { ) } + + nodeDetail.getSettings = () => { + return ( + <> + + + ) + } return nodeDetail; }; @@ -247,6 +271,14 @@ const Subject = function (node) { ) } + + nodeDetail.getSettings = () => { + return ( + <> + + + ) + } return nodeDetail; }; @@ -278,6 +310,14 @@ const File = function (node) { ) } + + nodeDetail.getSettings = () => { + return ( + <> + + + ) + } return nodeDetail; }; From f0f7b53fe5f99c8d6c0ce4f993107b71f1aa24e0 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 12 Jan 2024 22:48:24 +0100 Subject: [PATCH 09/76] #SDSV-21 Add metadata to redux store --- .../NodeDetailView/Details/SampleDetails.js | 69 ++--- .../NodeDetailView/Details/SubjectDetails.js | 123 +++----- .../NodeDetailView/NodeDetailView.js | 1 + src/config/app.json | 6 +- src/utils/Splinter.js | 22 +- src/utils/graphModel.js | 293 ++++++++++++------ 6 files changed, 288 insertions(+), 226 deletions(-) diff --git a/src/components/NodeDetailView/Details/SampleDetails.js b/src/components/NodeDetailView/Details/SampleDetails.js index 68801df..5051f4b 100644 --- a/src/components/NodeDetailView/Details/SampleDetails.js +++ b/src/components/NodeDetailView/Details/SampleDetails.js @@ -1,54 +1,49 @@ -import React from "react"; import { Box, Typography, } from "@material-ui/core"; import SimpleLabelValue from './Views/SimpleLabelValue'; +import SimpleLinkedChip from './Views/SimpleLinkedChip'; import Links from './Views/Links'; -import { iterateSimpleValue } from './utils'; import { detailsLabel } from '../../../constants'; +import { rdfTypes } from "../../../utils/graphModel"; +import { isValidUrl } from './utils'; + const SampleDetails = (props) => { const { node } = props; - let title = ""; - let idDetails = ""; - // both tree and graph nodes are present, extract data from both - if (node?.tree_node && node?.graph_node) { - idDetails = node.tree_node.id + detailsLabel; - title = node.tree_node.basename; - // the below is the case where we have data only from the tree/hierarchy - } else if (node?.graph_node) { - idDetails = node.graph_node.id + detailsLabel; - title = node.graph_node.attributes?.label ? node.graph_node.attributes?.label[0] : ""; - // the below is the case where we have data only from the graph - } else { - idDetails = node.tree_node.id + detailsLabel; - title = "Undefined Sample name"; - } + let samplePropertiesModel = [...rdfTypes["Sample"].properties]; return ( - + - { node.graph_node.attributes?.hasUriHuman && node.graph_node.attributes?.hasUriHuman[0] !== "" - ? ( - {"Sample Details"} - Label - ) - : () - } - - { node.graph_node?.attributes?.publishedURI && node.graph_node?.attributes?.publishedURI !== "" - ? ( - SPARC Portal Link - - ) - : <> - } - - { iterateSimpleValue('Assigned group', node?.graph_node?.attributes?.hasAssignedGroup) } - { iterateSimpleValue('Digital artifact', node?.graph_node?.attributes?.hasDigitalArtifactThatIsAboutIt) } - { iterateSimpleValue('Extracted from Anatomical region', node?.graph_node?.attributes?.wasExtractedFromAnatomicalRegion) } + + + {samplePropertiesModel?.map( property => { + if ( property.visible ){ + const propValue = node.graph_node.attributes[property.property]?.[0]; + if ( isValidUrl(propValue) ){ + return ( + {property.label} + + ) + } + + else if ( typeof propValue === "object" ){ + return ( + {property.label} + + ) + } + + else if ( typeof propValue === "string" ){ + return () + } + + return (<> ) + } + })} ); diff --git a/src/components/NodeDetailView/Details/SubjectDetails.js b/src/components/NodeDetailView/Details/SubjectDetails.js index fb74576..fb8315e 100644 --- a/src/components/NodeDetailView/Details/SubjectDetails.js +++ b/src/components/NodeDetailView/Details/SubjectDetails.js @@ -1,33 +1,17 @@ -import React from "react"; import { Box, Typography, } from "@material-ui/core"; -import SimpleChip from './Views/SimpleChip'; import SimpleLinkedChip from './Views/SimpleLinkedChip'; import SimpleLabelValue from './Views/SimpleLabelValue'; import Links from './Views/Links'; -import { iterateSimpleValue, simpleValue } from './utils'; import { detailsLabel } from '../../../constants'; +import { rdfTypes } from "../../../utils/graphModel"; +import { isValidUrl } from './utils'; const SubjectDetails = (props) => { const { node } = props; - node.graph_node.dataset_id = node.dataset_id; - let title = ""; - let idDetails = ""; - // both tree and graph nodes are present, extract data from both - if (node.tree_node && node.graph_node) { - idDetails = node?.tree_node?.id + detailsLabel; - title = node?.tree_node?.basename; - // the below is the case where we have data only from the tree/hierarchy - } else if (node?.graph_node) { - idDetails = node?.graph_node?.id + detailsLabel; - title = node.graph_node.name; - // the below is the case where we have data only from the graph - } else { - idDetails = node.tree_node.id + detailsLabel; - title = node.tree_node.basename; - } + let subjectPropertiesModel = [...rdfTypes["Subject"].properties]; const getGroupNode = (groupName, node)=> { let n = node.graph_node.parent; @@ -35,83 +19,58 @@ const SubjectDetails = (props) => { while ( n && !match ) { if ( n.name === groupName ) { match = true; - }else { + } else { n = n.parent; } } - n.dataset_id = node.dataset_id; + if ( n?.dataset_id ) { + n.dataset_id = node.dataset_id; + } else { + console.log("Empty node found ", node) + console.log("Empty n found ", n) + console.log("Empty groupName ", groupName) + } return n; } return ( - + - + + + {subjectPropertiesModel?.map( property => { + if ( property.visible ){ + const propValue = node.graph_node.attributes[property.property]?.[0]; + if ( property.isGroup ){ + return ( + {property.label} + + ) + } - { node.graph_node?.attributes?.publishedURI && node.graph_node?.attributes?.publishedURI !== "" - ? ( - SPARC Portal Link - - ) - : <> - } + else if ( isValidUrl(propValue) ){ + return ( + {property.label} + + ) + } - { node.graph_node?.attributes?.hasAgeCategory - ? ( - Age Category - - ) - : <> - } - { (node.graph_node.attributes?.ageValue && node.graph_node.attributes?.ageUnit) - ? simpleValue('Age', node.graph_node.attributes?.ageValue + ' ' + node.graph_node.attributes?.ageUnit) - : (node.graph_node.attributes?.ageBaseUnit && node.graph_node.attributes?.ageBaseValue) - ? simpleValue('Age', node.graph_node.attributes?.ageBaseValue + ' ' + node.graph_node.attributes?.ageBaseUnit) - : <> - } + else if ( typeof propValue === "object" ){ + return ( + {property.label} + + ) + } - { (node.graph_node.attributes?.weightUnit && node.graph_node.attributes?.weightValue) - ? simpleValue('Weight', node.graph_node.attributes?.weightValue + ' ' + node.graph_node.attributes?.weightUnit) - : <> - } + else if ( typeof propValue === "string" ){ + return () + } - { node.graph_node?.attributes?.biologicalSex - ? ( - Biological Sex - - ) - : <> - } - { node.graph_node?.attributes?.specimenHasIdentifier && node.graph_node?.attributes?.specimenHasIdentifier !== "" - ? ( - Specimen identifier - - ) - : <> - } - { node.graph_node?.attributes?.subjectSpecies - ? ( - Species - - ) - : <> - } - { node.graph_node?.attributes?.subjectStrain - ? ( - Strains - - ) - : <> - } - { node.graph_node?.attributes?.hasAssignedGroup && node.graph_node?.attributes?.hasAssignedGroup.length > 0 - ? ( - Assigned Groups - - ) - : <> - } + return (<> ) + } + })} ); diff --git a/src/components/NodeDetailView/NodeDetailView.js b/src/components/NodeDetailView/NodeDetailView.js index 3b9250e..819ab41 100644 --- a/src/components/NodeDetailView/NodeDetailView.js +++ b/src/components/NodeDetailView/NodeDetailView.js @@ -35,6 +35,7 @@ const NodeDetailView = (props) => { otherDetails = path.reverse().map( singleNode => { const graph_node = window.datasets[nodeSelected.dataset_id].splinter.nodes.get(singleNode); + console.log("dataset id ", nodeSelected.dataset_id) const new_node = { dataset_id: nodeSelected.dataset_id, graph_node: graph_node, diff --git a/src/config/app.json b/src/config/app.json index 03fae6e..334a64b 100644 --- a/src/config/app.json +++ b/src/config/app.json @@ -7,9 +7,9 @@ "groups" : { "order" : { - "subjectSpecies" : { "tag" : "Subject Species", "icon" : "./images/graph/species.svg"}, - "subjectStrain" : { "tag" : "Subject Strain", "icon" : "./images/graph/strains.svg"}, - "biologicalSex" : { "tag" : "Subject Sex", "icon" : "./images/graph/sex.svg"}, + "animalSubjectIsOfSpecies" : { "tag" : "Subject Species", "icon" : "./images/graph/species.svg"}, + "animalSubjectIsOfStrain" : { "tag" : "Subject Strain", "icon" : "./images/graph/strains.svg"}, + "hasBiologicalSex" : { "tag" : "Subject Sex", "icon" : "./images/graph/sex.svg"}, "hasAgeCategory" : { "tag" : "Subject Age", "icon" : "./images/graph/age.svg"} } }, diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 6285ce5..303a553 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -519,7 +519,6 @@ class Splinter { } const groupID = parent.id + "_" + target_node.attributes[key]?.[0].replace(/\s/g, ""); - if ( this.nodes.get(groupID) === undefined ) { let name = target_node.attributes[key]?.[0]; @@ -720,22 +719,25 @@ class Splinter { } if (node.type === rdfTypes.Subject.key) { - if (node.attributes?.specimenHasIdentifier !== undefined) { - let source = this.nodes.get(node.attributes.specimenHasIdentifier[0]); + if (node.attributes?.animalSubjectIsOfStrain !== undefined) { + let source = this.nodes.get(node.attributes.animalSubjectIsOfStrain[0]); if ( source !== undefined ) { - node.attributes.specimenHasIdentifier[0] = source.attributes.label[0]; + console.log("source speciment", source ) + node.attributes.animalSubjectIsOfStrain[0] = source.attributes.label[0]; } } - if (node.attributes?.subjectSpecies !== undefined) { - let source = this.nodes.get(node.attributes.subjectSpecies[0]); + if (node.attributes?.animalSubjectIsOfSpecies !== undefined) { + let source = this.nodes.get(node.attributes.animalSubjectIsOfSpecies[0]); if ( source !== undefined ) { - node.attributes.subjectSpecies[0] = source.attributes.label[0]; + console.log("source species ",source ) + node.attributes.animalSubjectIsOfSpecies[0] = source.attributes.label[0]; } } - if (node.attributes?.biologicalSex !== undefined) { - let source = this.nodes.get(node.attributes.biologicalSex[0]); + if (node.attributes?.hasBiologicalSex !== undefined) { + let source = this.nodes.get(node.attributes.hasBiologicalSex[0]); if ( source !== undefined ) { - node.attributes.biologicalSex[0] = source.attributes.label[0]; + console.log("source biological sex ", source ) + node.attributes.hasBiologicalSex[0] = source.attributes.label[0]; } } diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index e893c1e..66d0fde 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -51,13 +51,15 @@ export const rdfTypes = { "type": "rdfs", "key": "label", "property": "label", - "label": "To be filled" + "label": "To be filled", + "visible" : true }, { "type": "TEMP", "key": "hasUriHuman", "property": "hasUriHuman", - "label": "To be filled" + "label": "To be filled", + "visible" : true } ] }, @@ -69,7 +71,8 @@ export const rdfTypes = { "type": "rdfs", "key": "label", "property": "label", - "label": "To be filled" + "label": "Label", + "visible" : true } ] }, @@ -351,15 +354,24 @@ export const rdfTypes = { "properties": [ { "type": "rdfs", - "key": "label", - "property": "label", - "label": "To be filled" + "key": "identifier", + "property": "identifier", + "label": "Label", + "visible" : true }, { "type": "TEMP", - "key": "hasUriHuman", - "property": "hasUriHuman", - "label": "To be filled" + "key": "publishedURI", + "property": "publishedURI", + "label": "Human URI", + "visible" : true + }, + { + "type": "TEMP", + "key": "size", + "property": "size", + "label": "size", + "visible" : true } ] }, @@ -367,101 +379,128 @@ export const rdfTypes = { "image": "./images/graph/folder.svg", "key": "Subject", "properties": [ - { - "type": "sparc", - "key": "animalSubjectIsOfSpecies", - "property": "subjectSpecies", - "label": "to be filled" - }, { "type": "TEMP", - "key": "hasFolderAboutIt", - "property": "hasFolderAboutIt", - "label": "Folder that contains collection and files about the sample" - }, - { - "type": "sparc", - "key": "animalSubjectIsOfStrain", - "property": "subjectStrain", - "label": "to be filled" + "key": "localId", + "property": "localId", + "label": "Label", + "visible" : true }, { "type": "TEMP", - "key": "hasAge", - "property": "age", - "label": "to be filled" + "key": "hasUriHuman", + "property": "hasUriHuman", + "label": "URI Human", + "visible" : true }, { "type": "TEMP", "key": "hasAgeCategory", "property": "hasAgeCategory", - "label": "to be filled" + "label": "Age Category", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasAge", + "property": "hasAge", + "label": "Age", + "visible" : true }, { "type": "TEMP", "key": "hasAgeMin", "property": "hasAgeMin", - "label": "to be filled" + "label": "Age Min", + "visible" : true }, { "type": "TEMP", "key": "hasAgeMax", "property": "hasAgeMax", - "label": "to be filled" + "label": "Age Max", + "visible" : true }, { - "type": "TEMP", - "key": "hasAssignedGroup", - "property": "hasAssignedGroup", - "label": "to be filled" + "type": "sparc", + "key": "hasBiologicalSex", + "property": "hasBiologicalSex", + "label": "Biological Sex", + "visible" : true, + "isGroup" : true }, { "type": "sparc", - "key": "hasBiologicalSex", - "property": "biologicalSex", - "label": "to be filled" + "key": "specimenHasIdentifier", + "property": "specimenHasIdentifier", + "label": "Specimen has Identifier", + "visible" : true, + "isGroup" : true }, { - "type": "TEMP", - "key": "localId", - "property": "identifier", - "label": "to be filled" + "type": "sparc", + "key": "animalSubjectIsOfSpecies", + "property": "animalSubjectIsOfSpecies", + "label": "Subject Species", + "visible" : true, + "isGroup" : true + }, + { + "type": "sparc", + "key": "animalSubjectIsOfStrain", + "property": "animalSubjectIsOfStrain", + "label": "Subject Strain", + "visible" : true, + "isGroup" : true }, { "type": "TEMP", - "key": "hasDerivedInformationAsParticipant", - "property": "hasDerivedInformationAsParticipant", - "label": "to be filled" + "key": "hasAssignedGroup", + "property": "hasAssignedGroup", + "label": "Assigned Group", + "visible" : true }, { - "type": "sparc", - "key": "specimenHasIdentifier", - "property": "specimenHasIdentifier", - "label": "to be filled" + "type": "TEMP", + "key": "hasGenotype", + "property": "hasGenotype", + "label": "Genotype", + "visible" : true }, { "type": "TEMP", - "key": "participantInPerformanceOf", - "property": "participantInPerformanceOf", - "label": "to be filled" + "key": "experimental_file", + "property": "experimental_file", + "label": "Experimental File", + "visible" : true }, { - "type": "rdfs", - "key": "label", - "property": "label", - "label": "To be filled" + "type": "TEMP", + "key": "reference_atlas", + "property": "reference_atlas", + "label": "Reference Atlas", + "visible" : true }, { "type": "TEMP", - "key": "hasUriHuman", - "property": "hasUriHuman", - "label": "To be filled" + "key": "hasFolderAboutIt", + "property": "hasFolderAboutIt", + "label": "Folder About It", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasDerivedInformationAsParticipant", + "property": "hasDerivedInformationAsParticipant", + "label": "Derived Information as Participant", + "visible" : true }, { "type": "TEMP", - "key": "publishedURI", - "property": "publishedURI", - "label": "to be filled" + "key": "participantInPerformanceOf", + "property": "participantInPerformanceOf", + "label": "Participant In Performance Of", + "visible" : true }, ], "additional_properties": [ @@ -515,65 +554,89 @@ export const rdfTypes = { "image": "./images/graph/folder.svg", "key": "Sample", "properties": [ + { + "type": "rdfs", + "key": "label", + "property": "label", + "label": "Label", + "visible" : true + }, + { + "type": "TEMP", + "key": "hasUriHuman", + "property": "hasUriHuman", + "label": "Human URI", + "visible" : true + }, { "type": "TEMP", "key": "hasFolderAboutIt", "property": "hasFolderAboutIt", - "label": "Folder that contains collection and files about the sample" + "label": "Related Folder", + "visible" : true }, { "type": "TEMP", "key": "wasDerivedFromSubject", "property": "derivedFrom", - "label": "Derived from the subject" + "label": "Derived from Subject", + "visible" : true }, { "type": "TEMP", "key": "localId", - "property": "identifier", - "label": "Unique instance identifier" + "property": "localId", + "label": "Local ID", + "visible" : true }, { "type": "TEMP", "key": "hasAssignedGroup", "property": "hasAssignedGroup", - "label": "to be filled" + "label": "Assigned Group", + "visible" : true }, { "type": "TEMP", "key": "hasDerivedInformationAsParticipant", "property": "hasDerivedInformationAsParticipant", - "label": "to be filled" + "label": "Derived Information as Participant", + "visible" : true }, { "type": "TEMP", "key": "hasDigitalArtifactThatIsAboutIt", "property": "hasDigitalArtifactThatIsAboutIt", - "label": "Unique instance identifier" - }, - { - "type": "TEMP", - "key": "participantInPerformanceOf", - "property": "participantInPerformanceOf", - "label": "Unique instance identifier" + "label": "Digital Artifact", + "visible" : true }, { "type": "TEMPRAW", "key": "wasExtractedFromAnatomicalRegion", "property": "wasExtractedFromAnatomicalRegion", - "label": "Unique instance identifier" + "label": "Extracted From Anatomical Region", + "visible" : true }, { - "type": "rdfs", - "key": "label", - "property": "label", - "label": "To be filled" + "type": "TEMPRAW", + "key": "sample_anatomical_location", + "property": "sample_anatomical_location", + "label": "Sample Anatomical Location", + "visible" : true + }, + { + "type": "TEMPRAW", + "key": "sample_type", + "property": "sample_type", + "label": "Sample Type", + "visible" : true }, { "type": "TEMP", - "key": "hasUriHuman", - "property": "hasUriHuman", - "label": "To be filled" + "key": "participantInPerformanceOf", + "property": "participantInPerformanceOf", + "label": "Participant in Performance Of", + "visible" : true } ] }, @@ -582,29 +645,61 @@ export const rdfTypes = { "key": "Person", "properties": [ { - "type": "sparc", - "key": "firstName", - "property": "firstName", - "label": "To be filled" + "type": "rdfs", + "key": "label", + "property": "label", + "label": "Name", + "visible" : true }, { "type": "sparc", "key": "lastName", "property": "lastName", - "label": "To be filled" + "label": "Last Name", + "visible" : false + }, + { + "type": "sparc", + "key": "firstName", + "property": "firstName", + "label": "First Name", + "visible" : false }, { "type": "TEMP", "key": "middleName", "property": "middleName", - "label": "To be filled" + "label": "Middle Name", + "visible" : false + }, + { + "type": "sparc", + "key": "hasORCIDId", + "property": "hasORCIDId", + "label": "ORCID Id", + "visible" : false }, { "type": "TEMP", "key": "hasAffiliation", "property": "hasAffiliation", - "label": "To be filled" + "label": "Affiliation", + "visible" : true }, + { + "type": "TEMP", + "key": "hasDataRemoteUserId", + "property": "hasDataRemoteUserId", + "label": "Data Remote User ID", + "visible" : true + }, + { + "type": "TEMP", + "key": "contributorTo", + "property": "contributorTo", + "label": "Contributor To", + "visible" : true + } ] }, "Protocol": { @@ -615,19 +710,29 @@ export const rdfTypes = { "type": "rdfs", "key": "label", "property": "label", - "label": "To be filled" + "label": "Label", + "visible" : true + }, + { + "type": "TEMP", + "key": "protocolHasNumberOfSteps", + "property": "protocolHasNumberOfSteps", + "label": "Number of Steps", + "visible" : true }, { "type": "TEMP", "key": "hasUriHuman", "property": "hasUriHuman", - "label": "To be filled" + "label": "Human URI", + "visible" : true }, { "type": "TEMP", - "key": "protocolHasNumberOfSteps", - "property": "protocolHasNumberOfSteps", - "label": "To be filled" + "key": "hasDoi", + "property": "hasDoi", + "label": "DOI", + "visible" : true } ] }, From 94b7993405492cd8cb5c64a066b1bb3254c9cd6c Mon Sep 17 00:00:00 2001 From: jrmartin Date: Mon, 15 Jan 2024 17:26:42 +0100 Subject: [PATCH 10/76] #sdsv-21 Move metadata properties model to redux store --- src/App.js | 2 - src/components/GraphViewer/GraphViewer.js | 1 - .../NodeDetailView/Details/DatasetDetails.js | 9 +- .../NodeDetailView/Details/FileDetails.js | 117 +++++------------- .../NodeDetailView/Details/GroupDetails.js | 56 ++++++--- .../NodeDetailView/Details/SampleDetails.js | 4 +- .../NodeDetailView/Details/SubjectDetails.js | 12 +- .../NodeDetailView/NodeDetailView.js | 1 - src/redux/initialState.js | 10 +- src/utils/Splinter.js | 7 +- src/utils/graphModel.js | 37 ++++-- 11 files changed, 116 insertions(+), 140 deletions(-) diff --git a/src/App.js b/src/App.js index 3abe8b4..4af5cd2 100644 --- a/src/App.js +++ b/src/App.js @@ -59,7 +59,6 @@ const App = () => { tree: await splinter.getTree(), splinter: splinter }; - console.log("Graph ", _dataset.graph); dispatch(addDataset(_dataset)); dispatch(addWidget({ @@ -132,7 +131,6 @@ const App = () => { }; const splinter = new DatasetsListSplinter(undefined, file.data); let graph = await splinter.getGraph(); - console.log("Graph ", graph); let datasets = graph.nodes.filter((node) => node?.attributes?.hasDoi); const match = datasets.find( node => node.attributes?.hasDoi?.[0]?.includes(doi)); if ( match ) { diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 787f68d..b80a3de 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -194,7 +194,6 @@ const GraphViewer = (props) => { } const graph = { nodes : visibleNodes, links : visibleLinks, levelsMap : levelsMap, hierarchyVariant : maxLevel * 20 }; - console.log("Graph ", graph); return graph; }; diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index ded88b8..1b0edc6 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -1,22 +1,19 @@ import React from "react"; import { Box, - Typography, - List, - ListItemText, + Typography } from "@material-ui/core"; import Links from './Views/Links'; import SimpleLinkedChip from './Views/SimpleLinkedChip'; -import USER from "../../../images/user.svg"; import SimpleLabelValue from './Views/SimpleLabelValue'; import { detailsLabel } from '../../../constants'; -import { rdfTypes } from "../../../utils/graphModel"; import { isValidUrl } from './utils'; +import { useSelector } from 'react-redux' const DatasetDetails = (props) => { const { node } = props; - let datasetPropertiesModel = [...rdfTypes["Dataset"].properties]; + const datasetPropertiesModel = useSelector(state => state.sdsState.metadata_model.dataset); return ( diff --git a/src/components/NodeDetailView/Details/FileDetails.js b/src/components/NodeDetailView/Details/FileDetails.js index af9c009..d578bfd 100644 --- a/src/components/NodeDetailView/Details/FileDetails.js +++ b/src/components/NodeDetailView/Details/FileDetails.js @@ -1,106 +1,51 @@ -import React from "react"; import { Box, - Typography, - List, - ListItemText, + Typography } from "@material-ui/core"; import Links from './Views/Links'; import SimpleLinkedChip from './Views/SimpleLinkedChip'; import SimpleLabelValue from './Views/SimpleLabelValue'; import { detailsLabel } from '../../../constants'; +import { useSelector } from 'react-redux' +import { isValidUrl } from './utils'; const FileDetails = (props) => { const { node } = props; - - let title = ""; - let idDetails = ""; - // both tree and graph nodes are present, extract data from both - if (node?.tree_node && node?.graph_node) { - title = node.tree_node.basename; - idDetails = node.tree_node.id + detailsLabel; - // the below is the case where we have data only from the tree/hierarchy - } else if (node.graph_node) { - idDetails = node.graph_node.id + detailsLabel; - title = node.graph_node.attributes?.label?.[0]; - // the below is the case where we have data only from the graph - } else { - title = node.tree_node.basename; - idDetails = node.tree_node.id + detailsLabel; - } - - let latestUpdate = "Not defined." - if (node?.graph_node.attributes?.updated !== undefined) { - latestUpdate = node?.attributes?.updated; - } - - const DETAILS_LIST = [ - { - title: 'Mimetype', - value: node?.graph_node?.attributes?.mimetype - }, - { - title: 'Size Bytes', - value: node?.graph_node?.attributes?.size - } - ]; + const filePropertiesModel = useSelector(state => state.sdsState.metadata_model.file); return ( - + - { node.graph_node?.attributes?.identifier - ? ( - {"File Details"} - - ) - : () - } - { node.graph_node?.attributes?.publishedURI && node.graph_node?.attributes?.publishedURI !== "" - ? ( - Published Dataset File - - ) - : <> - } - {latestUpdate ? - - : (<> ) - } - { node?.tree_node?.uri_human !== undefined - ? ( - - ) - : (<> ) - } + + + {filePropertiesModel?.map( property => { + if ( property.visible ){ + const propValue = node.graph_node.attributes[property.property]; + if ( isValidUrl(propValue) ){ + return ( + {property.label} + + ) + } - { node?.tree_node?.checksums !== undefined - ? (<> - - - ) - : (<> ) - } + else if ( typeof propValue === "object" ){ + return ( + {property.label} + + ) + } + + else if ( typeof propValue === "string" ){ + return () + } - - - { - DETAILS_LIST?.map((item, index) => ( - - {item?.title} - {item?.value} - - )) + else if ( typeof propValue === "number" ){ + return () } - - - { node?.graph_node?.attributes?.hasUriHuman !== undefined - ? ( - Links - - ) - : <> - } + return (<> ) + } + })} ); diff --git a/src/components/NodeDetailView/Details/GroupDetails.js b/src/components/NodeDetailView/Details/GroupDetails.js index 50e92c9..0fc6195 100644 --- a/src/components/NodeDetailView/Details/GroupDetails.js +++ b/src/components/NodeDetailView/Details/GroupDetails.js @@ -1,33 +1,51 @@ -import React from "react"; import { Box, + Typography } from "@material-ui/core"; +import Links from './Views/Links'; +import SimpleLinkedChip from './Views/SimpleLinkedChip'; import SimpleLabelValue from './Views/SimpleLabelValue'; import { detailsLabel } from '../../../constants'; +import { useSelector } from 'react-redux' +import { isValidUrl } from './utils'; const GroupDetails = (props) => { const { node } = props; - - let title = ""; - let idDetails = ""; - // both tree and graph nodes are present, extract data from both - if (node?.tree_node && node?.graph_node) { - title = node.graph_node?.name; - idDetails = node.graph_node?.id + detailsLabel; - // the below is the case where we have data only from the tree/hierarchy - } else if (node?.tree_node) { - title = node.tree_node?.basename; - idDetails = node.tree_node?.id + detailsLabel; - // the below is the case where we have data only from the graph - } else { - title = node.graph_node?.name; - idDetails = node.graph_node?.id + detailsLabel; - } + const groupPropertiesModel = useSelector(state => state.sdsState.metadata_model.group); return ( - + - + + + {groupPropertiesModel?.map( property => { + if ( property.visible ){ + const propValue = node.graph_node[property.property]; + if ( isValidUrl(propValue) ){ + return ( + {property.label} + + ) + } + + else if ( typeof propValue === "object" ){ + return ( + {property.label} + + ) + } + + else if ( typeof propValue === "string" ){ + return () + } + + else if ( typeof propValue === "number" ){ + return () + } + + return (<> ) + } + })} ); diff --git a/src/components/NodeDetailView/Details/SampleDetails.js b/src/components/NodeDetailView/Details/SampleDetails.js index 5051f4b..461a489 100644 --- a/src/components/NodeDetailView/Details/SampleDetails.js +++ b/src/components/NodeDetailView/Details/SampleDetails.js @@ -6,14 +6,14 @@ import SimpleLabelValue from './Views/SimpleLabelValue'; import SimpleLinkedChip from './Views/SimpleLinkedChip'; import Links from './Views/Links'; import { detailsLabel } from '../../../constants'; -import { rdfTypes } from "../../../utils/graphModel"; import { isValidUrl } from './utils'; +import { useSelector } from 'react-redux' const SampleDetails = (props) => { const { node } = props; - let samplePropertiesModel = [...rdfTypes["Sample"].properties]; + const samplePropertiesModel = useSelector(state => state.sdsState.metadata_model.sample); return ( diff --git a/src/components/NodeDetailView/Details/SubjectDetails.js b/src/components/NodeDetailView/Details/SubjectDetails.js index fb8315e..36923f5 100644 --- a/src/components/NodeDetailView/Details/SubjectDetails.js +++ b/src/components/NodeDetailView/Details/SubjectDetails.js @@ -8,10 +8,12 @@ import Links from './Views/Links'; import { detailsLabel } from '../../../constants'; import { rdfTypes } from "../../../utils/graphModel"; import { isValidUrl } from './utils'; +import { useSelector } from 'react-redux' const SubjectDetails = (props) => { const { node } = props; - let subjectPropertiesModel = [...rdfTypes["Subject"].properties]; + + const subjectPropertiesModel = useSelector(state => state.sdsState.metadata_model.subject); const getGroupNode = (groupName, node)=> { let n = node.graph_node.parent; @@ -24,14 +26,6 @@ const SubjectDetails = (props) => { } } - if ( n?.dataset_id ) { - n.dataset_id = node.dataset_id; - } else { - console.log("Empty node found ", node) - console.log("Empty n found ", n) - console.log("Empty groupName ", groupName) - } - return n; } diff --git a/src/components/NodeDetailView/NodeDetailView.js b/src/components/NodeDetailView/NodeDetailView.js index 819ab41..3b9250e 100644 --- a/src/components/NodeDetailView/NodeDetailView.js +++ b/src/components/NodeDetailView/NodeDetailView.js @@ -35,7 +35,6 @@ const NodeDetailView = (props) => { otherDetails = path.reverse().map( singleNode => { const graph_node = window.datasets[nodeSelected.dataset_id].splinter.nodes.get(singleNode); - console.log("dataset id ", nodeSelected.dataset_id) const new_node = { dataset_id: nodeSelected.dataset_id, graph_node: graph_node, diff --git a/src/redux/initialState.js b/src/redux/initialState.js index 306097b..344efc5 100644 --- a/src/redux/initialState.js +++ b/src/redux/initialState.js @@ -1,5 +1,6 @@ import * as Actions from './actions'; import * as LayoutActions from '@metacell/geppetto-meta-client/common/layout/actions'; +import { rdfTypes } from "../utils/graphModel"; export const sdsInitialState = { "sdsState": { @@ -20,7 +21,14 @@ export const sdsInitialState = { source: "" }, layout : {}, - settings_panel_visible : false + settings_panel_visible : false, + metadata_model : { + dataset : [...rdfTypes.Dataset.properties], + subject : [...rdfTypes.Subject.properties], + sample : [...rdfTypes.Sample.properties], + group : [...rdfTypes.Group.properties], + file : [...rdfTypes.File.properties] + } } }; diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 303a553..417bf4d 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -211,7 +211,6 @@ class Splinter { }) } }) - console.log("Force edges ", this.forced_edges) // Assign neighbors, to highlight links this.forced_edges.forEach(link => { @@ -536,7 +535,8 @@ class Splinter { childLinks : [], samples : 0, subjects : 0, - publishedURI : "" + publishedURI : "", + dataset_id : this.dataset_id }; let nodeF = this.factory.createNode(groupNode); const img = new Image(); @@ -722,21 +722,18 @@ class Splinter { if (node.attributes?.animalSubjectIsOfStrain !== undefined) { let source = this.nodes.get(node.attributes.animalSubjectIsOfStrain[0]); if ( source !== undefined ) { - console.log("source speciment", source ) node.attributes.animalSubjectIsOfStrain[0] = source.attributes.label[0]; } } if (node.attributes?.animalSubjectIsOfSpecies !== undefined) { let source = this.nodes.get(node.attributes.animalSubjectIsOfSpecies[0]); if ( source !== undefined ) { - console.log("source species ",source ) node.attributes.animalSubjectIsOfSpecies[0] = source.attributes.label[0]; } } if (node.attributes?.hasBiologicalSex !== undefined) { let source = this.nodes.get(node.attributes.hasBiologicalSex[0]); if ( source !== undefined ) { - console.log("source biological sex ", source ) node.attributes.hasBiologicalSex[0] = source.attributes.label[0]; } } diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index 66d0fde..424227b 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -68,10 +68,17 @@ export const rdfTypes = { "key": "Group", "properties": [ { - "type": "rdfs", - "key": "label", - "property": "label", - "label": "Label", + "type": "TEMP", + "key": "name", + "property": "name", + "label": "Name", + "visible" : true + }, + { + "type": "TEMP", + "key": "subjects", + "property": "subjects", + "label": "Number of Subjects", "visible" : true } ] @@ -361,16 +368,30 @@ export const rdfTypes = { }, { "type": "TEMP", - "key": "publishedURI", - "property": "publishedURI", - "label": "Human URI", + "key": "mimetype", + "property": "mimetype", + "label": "Mimetype", "visible" : true }, { "type": "TEMP", "key": "size", "property": "size", - "label": "size", + "label": "Size", + "visible" : true + }, + { + "type": "TEMP", + "key": "updated", + "property": "updated", + "label": "Updated On", + "visible" : true + }, + { + "type": "TEMP", + "key": "publishedURI", + "property": "publishedURI", + "label": "Published URI", "visible" : true } ] From d1c12d02f995182dfefdeaf350555dcb6d48af42 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Mon, 15 Jan 2024 18:28:59 +0100 Subject: [PATCH 11/76] #20 update right sidebar style --- .../Details/CollectionDetails.js | 3 +- .../NodeDetailView/Details/DatasetDetails.js | 9 +++-- .../NodeDetailView/Details/Details.js | 2 ++ .../NodeDetailView/Details/FileDetails.js | 3 +- .../NodeDetailView/Details/GroupDetails.js | 2 ++ .../NodeDetailView/Details/PersonDetails.js | 2 ++ .../NodeDetailView/Details/ProtocolDetails.js | 2 ++ .../NodeDetailView/Details/SampleDetails.js | 2 ++ .../NodeDetailView/Details/SubjectDetails.js | 4 ++- .../Details/Views/Breadcrumbs.js | 7 ++-- .../NodeDetailView/NodeDetailView.js | 15 ++++++--- src/flexlayout.css | 1 + src/images/Icons.js | 7 ++++ src/styles/constant.js | 5 ++- src/theme.js | 33 ++++++++++++++++--- 15 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 src/images/Icons.js diff --git a/src/components/NodeDetailView/Details/CollectionDetails.js b/src/components/NodeDetailView/Details/CollectionDetails.js index 4add3b3..0fcd75f 100644 --- a/src/components/NodeDetailView/Details/CollectionDetails.js +++ b/src/components/NodeDetailView/Details/CollectionDetails.js @@ -1,6 +1,6 @@ import React from "react"; import { - Box, + Box, Divider, Typography } from "@material-ui/core"; import Links from './Views/Links'; @@ -28,6 +28,7 @@ const CollectionDetails = (props) => { return ( + { node.graph_node?.attributes?.publishedURI && node.graph_node?.attributes?.publishedURI !== "" diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index ded88b8..b79afef 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -2,8 +2,7 @@ import React from "react"; import { Box, Typography, - List, - ListItemText, + Divider, } from "@material-ui/core"; import Links from './Views/Links'; import SimpleLinkedChip from './Views/SimpleLinkedChip'; @@ -12,6 +11,7 @@ import SimpleLabelValue from './Views/SimpleLabelValue'; import { detailsLabel } from '../../../constants'; import { rdfTypes } from "../../../utils/graphModel"; import { isValidUrl } from './utils'; +import {DatasetIcon} from "../../../images/Icons"; const DatasetDetails = (props) => { const { node } = props; @@ -21,7 +21,12 @@ const DatasetDetails = (props) => { return ( + + + + Dataset Details + {datasetPropertiesModel?.map( property => { if ( property.visible ){ const propValue = node.graph_node.attributes[property.property]?.[0]; diff --git a/src/components/NodeDetailView/Details/Details.js b/src/components/NodeDetailView/Details/Details.js index b5b3a90..063008f 100644 --- a/src/components/NodeDetailView/Details/Details.js +++ b/src/components/NodeDetailView/Details/Details.js @@ -1,6 +1,7 @@ import React from "react"; import { Box, + Divider, } from "@material-ui/core"; import SimpleLabelValue from './Views/SimpleLabelValue'; import { detailsLabel } from '../../../constants'; @@ -26,6 +27,7 @@ const UnknownDetails = (props) => { return ( + diff --git a/src/components/NodeDetailView/Details/FileDetails.js b/src/components/NodeDetailView/Details/FileDetails.js index af9c009..c288fdd 100644 --- a/src/components/NodeDetailView/Details/FileDetails.js +++ b/src/components/NodeDetailView/Details/FileDetails.js @@ -4,9 +4,9 @@ import { Typography, List, ListItemText, + Divider, } from "@material-ui/core"; import Links from './Views/Links'; -import SimpleLinkedChip from './Views/SimpleLinkedChip'; import SimpleLabelValue from './Views/SimpleLabelValue'; import { detailsLabel } from '../../../constants'; @@ -47,6 +47,7 @@ const FileDetails = (props) => { return ( + { node.graph_node?.attributes?.identifier ? ( diff --git a/src/components/NodeDetailView/Details/GroupDetails.js b/src/components/NodeDetailView/Details/GroupDetails.js index 50e92c9..1c54bce 100644 --- a/src/components/NodeDetailView/Details/GroupDetails.js +++ b/src/components/NodeDetailView/Details/GroupDetails.js @@ -1,6 +1,7 @@ import React from "react"; import { Box, + Divider, } from "@material-ui/core"; import SimpleLabelValue from './Views/SimpleLabelValue'; import { detailsLabel } from '../../../constants'; @@ -26,6 +27,7 @@ const GroupDetails = (props) => { return ( + diff --git a/src/components/NodeDetailView/Details/PersonDetails.js b/src/components/NodeDetailView/Details/PersonDetails.js index e4f8e5f..fb48456 100644 --- a/src/components/NodeDetailView/Details/PersonDetails.js +++ b/src/components/NodeDetailView/Details/PersonDetails.js @@ -1,6 +1,7 @@ import React from "react"; import { Box, + Divider, Typography, } from "@material-ui/core"; import Links from './Views/Links'; @@ -28,6 +29,7 @@ const PersonDetails = (props) => { return ( + Person Details diff --git a/src/components/NodeDetailView/Details/ProtocolDetails.js b/src/components/NodeDetailView/Details/ProtocolDetails.js index 9a56695..5b3fa1c 100644 --- a/src/components/NodeDetailView/Details/ProtocolDetails.js +++ b/src/components/NodeDetailView/Details/ProtocolDetails.js @@ -1,6 +1,7 @@ import React from "react"; import { Box, + Divider, Typography, } from "@material-ui/core"; import Links from './Views/Links'; @@ -29,6 +30,7 @@ const ProtocolDetails = (props) => { return ( + { node.graph_node.attributes?.hasUriHuman && node.graph_node.attributes?.hasUriHuman[0] !== "" ? ( diff --git a/src/components/NodeDetailView/Details/SampleDetails.js b/src/components/NodeDetailView/Details/SampleDetails.js index 68801df..5adb334 100644 --- a/src/components/NodeDetailView/Details/SampleDetails.js +++ b/src/components/NodeDetailView/Details/SampleDetails.js @@ -1,6 +1,7 @@ import React from "react"; import { Box, + Divider, Typography, } from "@material-ui/core"; import SimpleLabelValue from './Views/SimpleLabelValue'; @@ -29,6 +30,7 @@ const SampleDetails = (props) => { return ( + { node.graph_node.attributes?.hasUriHuman && node.graph_node.attributes?.hasUriHuman[0] !== "" ? ( diff --git a/src/components/NodeDetailView/Details/SubjectDetails.js b/src/components/NodeDetailView/Details/SubjectDetails.js index fb74576..73513e6 100644 --- a/src/components/NodeDetailView/Details/SubjectDetails.js +++ b/src/components/NodeDetailView/Details/SubjectDetails.js @@ -1,13 +1,14 @@ import React from "react"; import { Box, + Divider, Typography, } from "@material-ui/core"; import SimpleChip from './Views/SimpleChip'; import SimpleLinkedChip from './Views/SimpleLinkedChip'; import SimpleLabelValue from './Views/SimpleLabelValue'; import Links from './Views/Links'; -import { iterateSimpleValue, simpleValue } from './utils'; +import { simpleValue } from './utils'; import { detailsLabel } from '../../../constants'; const SubjectDetails = (props) => { @@ -47,6 +48,7 @@ const SubjectDetails = (props) => { return ( + diff --git a/src/components/NodeDetailView/Details/Views/Breadcrumbs.js b/src/components/NodeDetailView/Details/Views/Breadcrumbs.js index 6778ca5..f015aad 100644 --- a/src/components/NodeDetailView/Details/Views/Breadcrumbs.js +++ b/src/components/NodeDetailView/Details/Views/Breadcrumbs.js @@ -15,8 +15,8 @@ const HeaderBreadcrumbs = (props) => { return ( } aria-label="breadcrumb" + maxItems={2} > { links && links.pages ? ( @@ -27,7 +27,10 @@ const HeaderBreadcrumbs = (props) => { )) ) : null } - {goToLink(links?.current.id)}} className="breadcrumb_selected">{links?.current.text} + {goToLink(links?.current.id)}} + className="breadcrumb_selected">{links?.current.text} {/* Close */} diff --git a/src/components/NodeDetailView/NodeDetailView.js b/src/components/NodeDetailView/NodeDetailView.js index 3b9250e..8af27a4 100644 --- a/src/components/NodeDetailView/NodeDetailView.js +++ b/src/components/NodeDetailView/NodeDetailView.js @@ -1,4 +1,4 @@ -import { Box } from "@material-ui/core"; +import {Box} from "@material-ui/core"; import NodeFooter from "./Footers/Footer"; import DetailsFactory from './factory'; import Breadcrumbs from "./Details/Views/Breadcrumbs"; @@ -65,9 +65,16 @@ const NodeDetailView = (props) => { - { otherDetails } - { showSettingsContent && nodeDetails.getSettings ? nodeDetails.getSettings() : nodeDetails.getDetail() } - + { + showSettingsContent && nodeDetails.getSettings ? nodeDetails.getSettings() : null + } + { + !showSettingsContent ? + <> + { otherDetails } + {nodeDetails.getDetail()} + : null + } { !showSettingsContent && diff --git a/src/flexlayout.css b/src/flexlayout.css index 8bf7478..19700f2 100644 --- a/src/flexlayout.css +++ b/src/flexlayout.css @@ -549,4 +549,5 @@ .breadcrumb_selected { color : #3779E1 !important; + font-weight: 600 !important; } \ No newline at end of file diff --git a/src/images/Icons.js b/src/images/Icons.js new file mode 100644 index 0000000..7b76e8b --- /dev/null +++ b/src/images/Icons.js @@ -0,0 +1,7 @@ +import {SvgIcon} from "@material-ui/core"; +export const DatasetIcon = (props) => ( + + + + +); \ No newline at end of file diff --git a/src/styles/constant.js b/src/styles/constant.js index 06065d8..3998e97 100644 --- a/src/styles/constant.js +++ b/src/styles/constant.js @@ -35,9 +35,12 @@ const vars = { matlab: '#6FC386', nifti: '#7747F6', volume: '#3779E1', - sideBarLabelColor: 'rgba(46, 58, 89, 0.4)', + sideBarLabelColor: '#435070', treeBorderColor: '#4E5261', scrollbarBg: 'rgba(0, 0, 0, 0.24)', + gray800: '#0F162B', + gray400: '#586482', + gray25: '#F0F1F2' }; export default vars; diff --git a/src/theme.js b/src/theme.js index 70d507c..467fc82 100644 --- a/src/theme.js +++ b/src/theme.js @@ -43,6 +43,9 @@ const { chipBgColor, progressErrorBg, treeBorderColor, + gray800, + gray400, + gray25 } = vars; const theme = createTheme({ @@ -94,12 +97,14 @@ const theme = createTheme({ display: 'inline-flex', alignItems: 'center', height: '1.375rem', - marginTop: '.25rem', + marginTop: '.5rem', marginRight: '.375rem', '& .MuiChip-label': { - padding: '0 .375rem', + padding: '0.25rem 0.375rem', fontSize: '.75rem', - color: primaryTextColor, + color: gray400, + backgroundColor: gray25, + borderRadius: '0.3125rem' }, }, }, @@ -960,13 +965,16 @@ const theme = createTheme({ '& .MuiBreadcrumbs-li': { lineHeight: '1.5', '& a': { - color: placeHolderColor, cursor: 'pointer', lineHeight: 'normal', + color: '#475467', + fontSize: '0.75rem', + fontWeight: 500, }, }, '& .MuiBreadcrumbs-separator': { margin: '0 .5rem', + color: '#9198AB' }, }, '&_body': { @@ -1056,12 +1064,22 @@ const theme = createTheme({ '&+ .tab-content': { borderTop: `.0625rem solid ${tabsBorderColor}`, }, + '& .title-container':{ + display: 'flex', + alignItems: 'center', + marginBottom: '1.3rem', + + '& h3': { + marginBottom: 0, + marginLeft: '.25rem' + } + }, '& h3': { fontSize: '1.125rem', fontWeight: '500', lineHeight: '1.375rem', letterSpacing: '-0.03em', - color: primaryTextColor, + color: gray800, marginBottom: '1.3rem', }, '& .tab-content-row': { @@ -1074,6 +1092,7 @@ const theme = createTheme({ fontSize: '.75rem', lineHeight: '1rem', color: primaryColor, + marginTop: '.5rem', '&:not(:last-child)': { marginRight: '.75rem', @@ -1095,6 +1114,10 @@ const theme = createTheme({ color: sideBarLabelColor, '&+ p': { marginTop: '.25rem', + color: gray400, + fontSize: '.75rem', + fontWeight: '400', + lineHeight: '1rem', }, }, '&> p': { From 32829f10591859b971c72057f408c203fbb7ea86 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Tue, 16 Jan 2024 15:19:13 +0100 Subject: [PATCH 12/76] #19 Replace icons with newly updated ones on Figma. Update Sidebar Background Color --- public/images/graph/age.svg | 76 ++++---- public/images/graph/dataset.svg | 224 +++++++++++++++++++++- public/images/graph/sex.svg | 66 +++---- public/images/graph/species.svg | 68 +++---- public/images/graph/strains.svg | 72 +++---- src/components/EmptyContainer.js | 2 +- src/components/GraphViewer/GraphViewer.js | 14 +- src/components/Sidebar/Footer.js | 24 ++- src/components/Sidebar/Header.js | 7 +- src/components/Sidebar/List.js | 6 +- src/config/app.json | 2 +- src/constants.js | 4 +- src/flexlayout.css | 6 +- src/images/Icons.js | 19 ++ src/styles/constant.js | 9 +- src/theme.js | 42 +++- 16 files changed, 456 insertions(+), 185 deletions(-) create mode 100644 src/images/Icons.js diff --git a/public/images/graph/age.svg b/public/images/graph/age.svg index fdeb314..fd6f908 100644 --- a/public/images/graph/age.svg +++ b/public/images/graph/age.svg @@ -1,38 +1,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/graph/dataset.svg b/public/images/graph/dataset.svg index 0bd9038..569b878 100644 --- a/public/images/graph/dataset.svg +++ b/public/images/graph/dataset.svg @@ -1,3 +1,223 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/graph/sex.svg b/public/images/graph/sex.svg index 435d97e..81b2c4b 100644 --- a/public/images/graph/sex.svg +++ b/public/images/graph/sex.svg @@ -1,33 +1,33 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/graph/species.svg b/public/images/graph/species.svg index 2c9fc42..4f749df 100644 --- a/public/images/graph/species.svg +++ b/public/images/graph/species.svg @@ -1,36 +1,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/graph/strains.svg b/public/images/graph/strains.svg index 0c7d29d..9181a41 100644 --- a/public/images/graph/strains.svg +++ b/public/images/graph/strains.svg @@ -1,38 +1,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/EmptyContainer.js b/src/components/EmptyContainer.js index fdd04f3..7064492 100644 --- a/src/components/EmptyContainer.js +++ b/src/components/EmptyContainer.js @@ -18,7 +18,7 @@ const EmptyContainer = (props) => { color='primary' onClick={() => props.setOpenDatasetsListDialog(true)} > - + { SPARC_DATASETS } + + { IMPORT_TEXT } } diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 787f68d..cc20597 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -2,21 +2,21 @@ import * as d3 from 'd3-force-3d' import Menu from '@material-ui/core/Menu'; import { IconButton, Tooltip,Typography, Box, Link, MenuItem, CircularProgress } from '@material-ui/core'; import React, { useState, useEffect } from 'react'; -import ZoomInIcon from '@material-ui/icons/ZoomIn'; import LayersIcon from '@material-ui/icons/Layers'; import HelpIcon from '@material-ui/icons/Help'; -import ZoomOutIcon from '@material-ui/icons/ZoomOut'; import RefreshIcon from '@material-ui/icons/Refresh'; import UnfoldMoreIcon from '@material-ui/icons/UnfoldMore'; import UnfoldLessIcon from '@material-ui/icons/UnfoldLess'; import BugReportIcon from '@material-ui/icons/BugReport'; import { selectInstance } from '../../redux/actions'; import { useSelector, useDispatch } from 'react-redux'; -import FormatAlignCenterIcon from '@material-ui/icons/FormatAlignCenter'; import GeppettoGraphVisualization from '@metacell/geppetto-meta-ui/graph-visualization/Graph'; import { GRAPH_SOURCE } from '../../constants'; import { rdfTypes, typesModel } from '../../utils/graphModel'; import config from "./../../config/app.json"; +import AddRoundedIcon from '@material-ui/icons/AddRounded'; +import RemoveRoundedIcon from '@material-ui/icons/RemoveRounded'; +import {ViewTypeIcon} from "../../images/Icons"; const NODE_FONT = '500 5px Inter, sans-serif'; const ONE_SECOND = 1000; @@ -585,7 +585,7 @@ const GraphViewer = (props) => {
- + { zoomIn()}> - + zoomOut()}> - + resetCamera()}> @@ -621,7 +621,7 @@ const GraphViewer = (props) => {
- + Version 1 window.open(config.issues_url, '_blank')}> diff --git a/src/components/Sidebar/Footer.js b/src/components/Sidebar/Footer.js index c6c9409..cd97cfa 100644 --- a/src/components/Sidebar/Footer.js +++ b/src/components/Sidebar/Footer.js @@ -1,6 +1,5 @@ -import Plus from '../../images/plus.svg'; import { ADD_DATASET } from '../../constants'; -import { Box, Button, Typography } from '@material-ui/core'; +import {Box, Button, Divider, Typography} from '@material-ui/core'; import config from "./../../config/app.json"; @@ -8,30 +7,35 @@ const SidebarFooter = (props) => { return ( + { props.local ? : null } - - Powered by MetaCell - + + + { + props.expand && + Powered by MetaCell + + } + ); }; diff --git a/src/components/Sidebar/Header.js b/src/components/Sidebar/Header.js index 6d6b272..fb7ad49 100644 --- a/src/components/Sidebar/Header.js +++ b/src/components/Sidebar/Header.js @@ -7,11 +7,10 @@ import { InputAdornment, Button, } from '@material-ui/core'; -import ToggleRight from '../../images/toggle-right.svg'; import Logo from '../../images/logo.svg'; import ToggleLeft from '../../images/toggle-left.svg'; import Search from '../../images/search.svg'; - +import KeyboardTabIcon from '@material-ui/icons/KeyboardTab'; const SidebarHeader = (props) => { const { expand, setExpand, setSearchTerm, searchTerm } = props; const handleChange = ( e ) => { @@ -21,8 +20,8 @@ const SidebarHeader = (props) => { return ( Logo - setExpand(!expand)}> - Toggle + setExpand(!expand)} className='shrink-btn'> + {!expand ? : Toggle} {expand && ( diff --git a/src/components/Sidebar/List.js b/src/components/Sidebar/List.js index 60c51d2..d992d49 100644 --- a/src/components/Sidebar/List.js +++ b/src/components/Sidebar/List.js @@ -3,11 +3,10 @@ import { Box, IconButton, } from '@material-ui/core'; -import Search from '../../images/search.svg'; import Typography from '@material-ui/core/Typography'; import InstancesTreeView from './TreeView/InstancesTreeView'; import { useSelector } from 'react-redux' - +import SearchRoundedIcon from '@material-ui/icons/SearchRounded'; const SidebarContent = (props) => { const { expand, setExpand, searchTerm } = props; const datasets = useSelector(state => state.sdsState.datasets); @@ -39,8 +38,9 @@ const SidebarContent = (props) => { setExpand(!expand)} + className='shrink-btn' > - Search + ) : ( renderContent() ) } diff --git a/src/config/app.json b/src/config/app.json index 03fae6e..4ff6e31 100644 --- a/src/config/app.json +++ b/src/config/app.json @@ -14,7 +14,7 @@ } }, "text" : { - "datasetsButtonText" : "SPARC Datasets", + "datasetsButtonText" : "Import a new dataset", "datasetsDialogSearchText" : "Search datasets by label or id", "datasetsButtonSubtitleText" : "Select a dataset to load" } diff --git a/src/constants.js b/src/constants.js index 99540b2..86a10af 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,5 +1,5 @@ -export const IMPORT_TEXT = 'Load a new dataset'; -export const ADD_DATASET = 'Load Dataset'; +export const IMPORT_TEXT = 'Import a new dataset'; +export const ADD_DATASET = 'Import Dataset'; export const LIST_DATASETS = 'List Datasets'; export const SPARC_DATASETS = 'SPARC Datasets'; export const FILE_UPLOAD_PARAMS = { diff --git a/src/flexlayout.css b/src/flexlayout.css index 8bf7478..9f0496a 100644 --- a/src/flexlayout.css +++ b/src/flexlayout.css @@ -450,8 +450,8 @@ width: 24px; height: 24px; background: #ffffff; - border: 2px solid #11bffe; - color : #11bffe; + border: 1px solid #E1E3E8; + color : #435070; box-sizing: border-box; box-shadow: 0px 0px 27px rgba(0, 0, 0, 0.05); border-radius: 24px; @@ -478,7 +478,7 @@ height: 25px; } -.graph-view_controls .MuiIconButton-root:nth-child(4) { +.graph-view_controls .MuiIconButton-root:nth-child(4) , .graph-view_controls .MuiIconButton-root:nth-child(5){ margin-top: 8px; } diff --git a/src/images/Icons.js b/src/images/Icons.js new file mode 100644 index 0000000..4354e17 --- /dev/null +++ b/src/images/Icons.js @@ -0,0 +1,19 @@ +import {SvgIcon} from "@material-ui/core"; +export const DatasetIcon = (props) => ( + + + + +); + +export const ViewTypeIcon = (props) => ( + + + +); + +export const LabelIcon = (props) => ( + + + +); \ No newline at end of file diff --git a/src/styles/constant.js b/src/styles/constant.js index 06065d8..90344a9 100644 --- a/src/styles/constant.js +++ b/src/styles/constant.js @@ -11,7 +11,7 @@ const vars = { noInstanceColor: 'rgba(255, 255, 255, 0.6)', inputTextColor: 'rgba(255, 255, 255, 0.8)', iconButtonHover: 'rgba(255, 255, 255, 0.2)', - radius: 8, + radius: '.5rem', gutter: 16, whiteColor: '#FFFFFF', sidebarIconColor: 'rgba(221, 221, 221, 0.8)', @@ -38,6 +38,13 @@ const vars = { sideBarLabelColor: 'rgba(46, 58, 89, 0.4)', treeBorderColor: '#4E5261', scrollbarBg: 'rgba(0, 0, 0, 0.24)', + grey700: '#212B45', + grey500: '#435070', + grey100: '#C9CDD6', + grey400: '#586482', + grey50: '#E1E3E8', + grey25: '#F0F1F2', + grey600: '#2E3A59', }; export default vars; diff --git a/src/theme.js b/src/theme.js index 70d507c..12d8329 100644 --- a/src/theme.js +++ b/src/theme.js @@ -43,6 +43,13 @@ const { chipBgColor, progressErrorBg, treeBorderColor, + grey700, + grey500, + grey100, + grey400, + grey50, + grey25, + grey600 } = vars; const theme = createTheme({ @@ -306,12 +313,12 @@ const theme = createTheme({ MuiFilledInput: { root: { fontFamily, - backgroundColor: lightBorderColor, + backgroundColor: grey500, height: '2.375rem', - borderRadius: `${radius}px !important`, + borderRadius: `${radius} !important`, paddingRight: `0.4375rem !important`, '&:hover': { - backgroundColor: lightBorderColor, + backgroundColor: grey500, }, '& .MuiInputAdornment-positionStart': { marginTop: `0 !important`, @@ -322,9 +329,11 @@ const theme = createTheme({ paddingBottom: 0, fontSize: '0.75rem', letterSpacing: '-0.01em', - color: inputTextColor, + color: grey100, '&::placeholder': { - color: inputTextColor, + color: grey100, + fontWeight: '400', + fontSize: '.75rem' }, }, adornedEnd: { @@ -358,6 +367,7 @@ const theme = createTheme({ label: { textTransform: 'none', display: 'flex', + fontWeight: 600, '& img': { marginRight: '.25rem', }, @@ -376,6 +386,7 @@ const theme = createTheme({ outlinedPrimary: { borderColor: primaryColor, color: primaryColor, + padding: '0.75rem', '&:hover': { backgroundColor: outlinedButtonHover, }, @@ -393,6 +404,9 @@ const theme = createTheme({ display: 'flex', overflow: 'hidden', }, + '.sidebar-body': { + boxShadow: '0px -75px 49px -41px #212B45 inset', + }, '.scrollbar': { overflow: 'auto', '&::-webkit-scrollbar': { @@ -466,7 +480,7 @@ const theme = createTheme({ '.sidebar': { width: '18.75rem', overflow: 'hidden', - backgroundColor: secondaryColor, + backgroundColor: grey700, height: '100vh', flexShrink: 0, padding: '1rem 0.75rem', @@ -501,10 +515,17 @@ const theme = createTheme({ padding: 0, width: '2.25rem', minWidth: '0.0625rem', - fontSize: 0, margin: '0 auto', display: 'block', height: '2.25rem', + '&.shrink-btn': { + backgroundColor: grey25, + color: grey600, + + '& .MuiSvgIcon-root': { + fontSize: '1rem', + } + } }, }, '&:not(.shrink)': { @@ -643,7 +664,7 @@ const theme = createTheme({ }, '& .labelCaption': { height: '1rem', - backgroundColor: lightBorderColor, + backgroundColor: grey400, padding: '0 0.25rem', display: 'flex', alignItems: 'center', @@ -653,7 +674,7 @@ const theme = createTheme({ lineHeight: '0.75rem', minWidth: '2rem', justifyContent: 'center', - color: noInstanceColor, + color: grey50, letterSpacing: '-0.01em', '& img': { marginLeft: '0.125rem', @@ -804,7 +825,7 @@ const theme = createTheme({ height: '100%', fontWeight: '600', letterSpacing: '-0.01em', - color: noInstanceColor, + color: grey100, textAlign: 'center', }, }, @@ -1177,6 +1198,7 @@ const theme = createTheme({ bottom: '0', right : '0rem', zIndex: '100', + padding: '.5rem' }, }, }, From 4c34bea48ad66fbf8cb1e2f6139ce2f878892694 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 16 Jan 2024 21:11:16 +0100 Subject: [PATCH 13/76] Fix breadcrumbs after merge --- src/components/NodeDetailView/Details/FileDetails.js | 2 +- src/components/NodeDetailView/Details/GroupDetails.js | 2 +- src/components/NodeDetailView/Details/SubjectDetails.js | 2 +- src/components/NodeDetailView/Details/Views/Breadcrumbs.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/NodeDetailView/Details/FileDetails.js b/src/components/NodeDetailView/Details/FileDetails.js index a9a26fa..870c0ea 100644 --- a/src/components/NodeDetailView/Details/FileDetails.js +++ b/src/components/NodeDetailView/Details/FileDetails.js @@ -15,7 +15,7 @@ const FileDetails = (props) => { const filePropertiesModel = useSelector(state => state.sdsState.metadata_model.file); return ( - + diff --git a/src/components/NodeDetailView/Details/GroupDetails.js b/src/components/NodeDetailView/Details/GroupDetails.js index d521144..9a68798 100644 --- a/src/components/NodeDetailView/Details/GroupDetails.js +++ b/src/components/NodeDetailView/Details/GroupDetails.js @@ -15,7 +15,7 @@ const GroupDetails = (props) => { const groupPropertiesModel = useSelector(state => state.sdsState.metadata_model.group); return ( - + diff --git a/src/components/NodeDetailView/Details/SubjectDetails.js b/src/components/NodeDetailView/Details/SubjectDetails.js index 2766ae4..2f30e10 100644 --- a/src/components/NodeDetailView/Details/SubjectDetails.js +++ b/src/components/NodeDetailView/Details/SubjectDetails.js @@ -32,7 +32,7 @@ const SubjectDetails = (props) => { } return ( - + diff --git a/src/components/NodeDetailView/Details/Views/Breadcrumbs.js b/src/components/NodeDetailView/Details/Views/Breadcrumbs.js index f015aad..1eeea42 100644 --- a/src/components/NodeDetailView/Details/Views/Breadcrumbs.js +++ b/src/components/NodeDetailView/Details/Views/Breadcrumbs.js @@ -9,7 +9,7 @@ const HeaderBreadcrumbs = (props) => { const { links } = props; const goToLink = id => { const divElement = document.getElementById(id + detailsLabel); - divElement.scrollIntoView({ behavior: 'smooth' }); + divElement?.scrollIntoView({ behavior: 'smooth' }); } return ( From 26b70c54f425d38ecd0a515b10c2edd78bdda04f Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Thu, 18 Jan 2024 15:51:32 +0100 Subject: [PATCH 14/76] #22 connect settings panel with redux store --- .../NodeDetailView/settings/SettingsGroup.js | 39 +++---------------- .../NodeDetailView/settings/SettingsItem.js | 17 +++++--- .../settings/SettingsListItems.js | 15 ++++--- src/redux/actions.js | 12 +++++- src/redux/initialState.js | 16 ++++++++ 5 files changed, 51 insertions(+), 48 deletions(-) diff --git a/src/components/NodeDetailView/settings/SettingsGroup.js b/src/components/NodeDetailView/settings/SettingsGroup.js index 7a5be27..63411dc 100644 --- a/src/components/NodeDetailView/settings/SettingsGroup.js +++ b/src/components/NodeDetailView/settings/SettingsGroup.js @@ -1,16 +1,9 @@ -import React, { useState } from "react"; +import React, {useEffect, useState} from "react"; import { Box } from "@material-ui/core"; import { DragDropContext, Droppable } from "react-beautiful-dnd"; import SettingsListItems from "./SettingsListItems"; -const SettingsGroup = props => { - const [items, setItems] = useState([ - { id: "1", primary: "Created on", disabled: false, visible: true }, - { id: "2", primary: "Remote ID", disabled: false, visible: true }, - { id: "3", primary: "Mimetype", disabled: false, visible: true }, - { id: "4", primary: "Dataset", disabled: false, visible: true }, - { id: "5", primary: "Dataset Path", disabled: false, visible: true } - ]); - +const SettingsGroup = ({title, group}) => { + const [items, setItems] = useState([]); const handleDragEnd = result => { if (!result.destination) return; @@ -21,35 +14,15 @@ const SettingsGroup = props => { setItems(itemsCopy); }; - const toggleItemDisabled = itemId => { - const itemIndex = items.findIndex(item => item.id === itemId); - - if (itemIndex === -1) return; - - const updatedItems = [...items]; - const [toggledItem] = updatedItems.splice(itemIndex, 1); // Remove the item from its current position - - // If the item is currently disabled - if (toggledItem.disabled) { - // Move the item to the top of the list by unshifting it - updatedItems.unshift({ ...toggledItem, disabled: false, visible: true }); - } else { - // Toggle the disabled and visible properties - updatedItems.push({ ...toggledItem, disabled: true, visible: false }); - } - - setItems(updatedItems); - }; - return ( - + {provided => ( )} diff --git a/src/components/NodeDetailView/settings/SettingsItem.js b/src/components/NodeDetailView/settings/SettingsItem.js index 85120ed..efb9bde 100644 --- a/src/components/NodeDetailView/settings/SettingsItem.js +++ b/src/components/NodeDetailView/settings/SettingsItem.js @@ -1,4 +1,7 @@ -import React from "react"; +import React, {useEffect} from "react"; +import { useDispatch } from 'react-redux'; +import { toggleItemVisibility } from '../../../redux/actions'; + import { Typography, ListItemText, @@ -11,7 +14,9 @@ import RemoveCircleOutlineIcon from "@material-ui/icons/RemoveCircleOutline"; import AddCircleOutlineIcon from "@material-ui/icons/AddCircleOutline"; import VisibilityOffRoundedIcon from "@material-ui/icons/VisibilityOffRounded"; const SettingsItem = props => { - const { item, toggleItemDisabled } = props; + const { groupTitle, item } = props; + const dispatch = useDispatch(); + const toggleItemDisabled = () => dispatch(toggleItemVisibility(groupTitle, item.key)) return ( { fontSize: ".75rem" }} > - {item.primary} + {item.label} } /> @@ -57,10 +62,10 @@ const SettingsItem = props => { toggleItemDisabled(item.id)} + aria-label={!item.visible ? "add" : "delete"} + onClick={toggleItemDisabled} > - {item.disabled ? ( + {!item.visible ? ( diff --git a/src/components/NodeDetailView/settings/SettingsListItems.js b/src/components/NodeDetailView/settings/SettingsListItems.js index 07529a1..f3461e3 100644 --- a/src/components/NodeDetailView/settings/SettingsListItems.js +++ b/src/components/NodeDetailView/settings/SettingsListItems.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, {useEffect, useState} from "react"; import { Box, Typography, @@ -22,8 +22,7 @@ import VisibilityOffRoundedIcon from "@material-ui/icons/VisibilityOffRounded"; import { SPARC_DATASETS } from "../../../constants"; import SettingsItem from "./SettingsItem"; const SettingsListItems = props => { - const { provided, items, toggleItemDisabled } = props; - + const { provided, items, title } = props; return ( { }} > - Title + {title.charAt(0).toUpperCase() + title.slice(1)} @@ -45,10 +44,10 @@ const SettingsListItems = props => { > {items.map((item, index) => ( {provided => ( { > )} diff --git a/src/redux/actions.js b/src/redux/actions.js index 8a4de92..6a47f87 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -5,6 +5,7 @@ export const SELECT_INSTANCE = 'SELECT_INSTANCE' export const TRIGGER_ERROR = 'TRIGGER_ERROR' export const SELECT_GROUP = 'SELECT_GROUP' export const TOGGLE_METADATA_SETTINGS = 'TOGGLE_METADATA_SETTINGS' +export const TOGGLE_ITEM_VISIBILITY = 'TOGGLE_ITEM_VISIBILITY' export const addDataset = dataset => ({ type: ADD_DATASET, @@ -49,4 +50,13 @@ export const triggerError = message => ({ export const toggleSettingsPanelVisibility = visible => ({ type: TOGGLE_METADATA_SETTINGS, data: { visible: visible }, -}); \ No newline at end of file +}); + + +export const toggleItemVisibility = (groupTitle, itemId) => ({ + type: TOGGLE_ITEM_VISIBILITY, + data: { + groupTitle, + itemId, + }, +}); diff --git a/src/redux/initialState.js b/src/redux/initialState.js index 344efc5..09d9881 100644 --- a/src/redux/initialState.js +++ b/src/redux/initialState.js @@ -1,6 +1,7 @@ import * as Actions from './actions'; import * as LayoutActions from '@metacell/geppetto-meta-client/common/layout/actions'; import { rdfTypes } from "../utils/graphModel"; +import {TOGGLE_ITEM_VISIBILITY} from "./actions"; export const sdsInitialState = { "sdsState": { @@ -114,6 +115,21 @@ export default function sdsClientReducer(state = {}, action) { }; } break; + case TOGGLE_ITEM_VISIBILITY: + const { groupTitle, itemId } = action.data; + const updatedMetadataModel = { ...state.metadata_model }; + const groupIndex = updatedMetadataModel[groupTitle].findIndex(item => item.key === itemId); + if (groupIndex !== -1) { + updatedMetadataModel[groupTitle][groupIndex].visible = !updatedMetadataModel[groupTitle][groupIndex].visible; + } + + return { + ...state, + sdsState: { + ...state.sdsState, + metadata_model: updatedMetadataModel, + }, + }; case LayoutActions.layoutActions.SET_LAYOUT: return { ...state, layout : action.data.layout}; case Actions.TOGGLE_METADATA_SETTINGS: From e54330f7c3db6d6540d9c54fed8f02643ccee3a3 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Thu, 18 Jan 2024 15:51:54 +0100 Subject: [PATCH 15/76] #22 connect settings panel with redux store --- .../NodeDetailView/settings/Settings.js | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/components/NodeDetailView/settings/Settings.js b/src/components/NodeDetailView/settings/Settings.js index b3d13ae..1969477 100644 --- a/src/components/NodeDetailView/settings/Settings.js +++ b/src/components/NodeDetailView/settings/Settings.js @@ -3,36 +3,44 @@ import SettingsGroup from "./SettingsGroup"; import FolderIcon from "@material-ui/icons/Folder"; import { useSelector, useDispatch } from 'react-redux' import { toggleSettingsPanelVisibility } from '../../../redux/actions'; +import React, {useEffect, useState} from "react"; +import {DragDropContext, Droppable} from "react-beautiful-dnd"; +import SettingsListItems from "./SettingsListItems"; const Settings = props => { const dispatch = useDispatch(); const showSettingsContent = useSelector(state => state.sdsState.settings_panel_visible); - + const metaDataPropertiesModel = useSelector(state => state.sdsState.metadata_model); + const [data, setData] = useState([]) const save = () => { dispatch(toggleSettingsPanelVisibility(!showSettingsContent)); }; + useEffect(() => { + setData(metaDataPropertiesModel) + }, [metaDataPropertiesModel]) return ( - - - - First List Title - - - - + {/**/} + {/* */} + {/* */} + {/* First List Title*/} + {/* */} + {/**/} + { + Object.keys(data).map(group => ) + } Date: Thu, 18 Jan 2024 18:53:35 +0100 Subject: [PATCH 16/76] #22 add reordering feature --- .../NodeDetailView/settings/Settings.js | 7 +--- .../NodeDetailView/settings/SettingsGroup.js | 37 +++++++++++-------- src/redux/actions.js | 8 ++++ src/redux/initialState.js | 29 ++++++++++++--- 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/components/NodeDetailView/settings/Settings.js b/src/components/NodeDetailView/settings/Settings.js index 1969477..57feff1 100644 --- a/src/components/NodeDetailView/settings/Settings.js +++ b/src/components/NodeDetailView/settings/Settings.js @@ -12,14 +12,9 @@ const Settings = props => { const dispatch = useDispatch(); const showSettingsContent = useSelector(state => state.sdsState.settings_panel_visible); const metaDataPropertiesModel = useSelector(state => state.sdsState.metadata_model); - const [data, setData] = useState([]) const save = () => { dispatch(toggleSettingsPanelVisibility(!showSettingsContent)); }; - - useEffect(() => { - setData(metaDataPropertiesModel) - }, [metaDataPropertiesModel]) return ( {/* { {/* */} {/**/} { - Object.keys(data).map(group => ) + Object.keys(metaDataPropertiesModel).map(group => ) } { - const [items, setItems] = useState([]); +import { useDispatch } from "react-redux"; +import { updateItemsOrder } from "../../../redux/actions"; +const SettingsGroup = ({ title, group }) => { + const [items, setItems] = useState(group); + const dispatch = useDispatch(); + const handleDragEnd = result => { if (!result.destination) return; @@ -12,22 +16,23 @@ const SettingsGroup = ({title, group}) => { itemsCopy.splice(result.destination.index, 0, reorderedItem); setItems(itemsCopy); + dispatch(updateItemsOrder({ groupTitle: title, newItemsOrder: itemsCopy })); }; return ( - - - - {provided => ( - - )} - - - + + + + {provided => ( + + )} + + + ); }; diff --git a/src/redux/actions.js b/src/redux/actions.js index 6a47f87..0e5ffd1 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -6,6 +6,7 @@ export const TRIGGER_ERROR = 'TRIGGER_ERROR' export const SELECT_GROUP = 'SELECT_GROUP' export const TOGGLE_METADATA_SETTINGS = 'TOGGLE_METADATA_SETTINGS' export const TOGGLE_ITEM_VISIBILITY = 'TOGGLE_ITEM_VISIBILITY' +export const UPDATE_ITEMS_ORDER = 'UPDATE_ITEMS_ORDER' export const addDataset = dataset => ({ type: ADD_DATASET, @@ -60,3 +61,10 @@ export const toggleItemVisibility = (groupTitle, itemId) => ({ itemId, }, }); + +export const updateItemsOrder = ({ groupTitle, newItemsOrder }) => { + return { + type: UPDATE_ITEMS_ORDER, + payload: { title: groupTitle, newItemsOrder }, + }; +}; \ No newline at end of file diff --git a/src/redux/initialState.js b/src/redux/initialState.js index 09d9881..ffb6d14 100644 --- a/src/redux/initialState.js +++ b/src/redux/initialState.js @@ -1,7 +1,7 @@ import * as Actions from './actions'; import * as LayoutActions from '@metacell/geppetto-meta-client/common/layout/actions'; import { rdfTypes } from "../utils/graphModel"; -import {TOGGLE_ITEM_VISIBILITY} from "./actions"; +import {TOGGLE_ITEM_VISIBILITY, UPDATE_ITEMS_ORDER} from "./actions"; export const sdsInitialState = { "sdsState": { @@ -119,15 +119,34 @@ export default function sdsClientReducer(state = {}, action) { const { groupTitle, itemId } = action.data; const updatedMetadataModel = { ...state.metadata_model }; const groupIndex = updatedMetadataModel[groupTitle].findIndex(item => item.key === itemId); + if (groupIndex !== -1) { - updatedMetadataModel[groupTitle][groupIndex].visible = !updatedMetadataModel[groupTitle][groupIndex].visible; + const itemToToggle = updatedMetadataModel[groupTitle][groupIndex]; + itemToToggle.visible = !itemToToggle.visible; + + // Toggle visibility first, then reorder items + updatedMetadataModel[groupTitle].sort((a, b) => { + if (a.visible === b.visible) { + // Preserve the original order for items with the same visibility + return updatedMetadataModel[groupTitle].indexOf(a) - updatedMetadataModel[groupTitle].indexOf(b); + } else { + // Move visible items to the top + return a.visible ? -1 : 1; + } + }); } + return { + ...state, + metadata_model: { ...updatedMetadataModel } + }; + case UPDATE_ITEMS_ORDER: + const { title, newItemsOrder } = action.payload; return { ...state, - sdsState: { - ...state.sdsState, - metadata_model: updatedMetadataModel, + metadata_model: { + ...state.metadata_model, + [title]: newItemsOrder, }, }; case LayoutActions.layoutActions.SET_LAYOUT: From 79f3a56297a5bc48c1bf5b12eb15b15e1bb626a3 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Thu, 18 Jan 2024 19:00:11 +0100 Subject: [PATCH 17/76] use descriptive actions/functions names --- .../NodeDetailView/settings/SettingsGroup.js | 6 +++--- .../NodeDetailView/settings/SettingsItem.js | 5 +++-- src/redux/actions.js | 12 ++++++------ src/redux/initialState.js | 6 +++--- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/components/NodeDetailView/settings/SettingsGroup.js b/src/components/NodeDetailView/settings/SettingsGroup.js index 12b8b46..deb3230 100644 --- a/src/components/NodeDetailView/settings/SettingsGroup.js +++ b/src/components/NodeDetailView/settings/SettingsGroup.js @@ -1,9 +1,9 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { Box } from "@material-ui/core"; import { DragDropContext, Droppable } from "react-beautiful-dnd"; import SettingsListItems from "./SettingsListItems"; import { useDispatch } from "react-redux"; -import { updateItemsOrder } from "../../../redux/actions"; +import { updateMetaDataItemsOrder } from "../../../redux/actions"; const SettingsGroup = ({ title, group }) => { const [items, setItems] = useState(group); const dispatch = useDispatch(); @@ -16,7 +16,7 @@ const SettingsGroup = ({ title, group }) => { itemsCopy.splice(result.destination.index, 0, reorderedItem); setItems(itemsCopy); - dispatch(updateItemsOrder({ groupTitle: title, newItemsOrder: itemsCopy })); + dispatch(updateMetaDataItemsOrder({ groupTitle: title, newItemsOrder: itemsCopy })); }; return ( diff --git a/src/components/NodeDetailView/settings/SettingsItem.js b/src/components/NodeDetailView/settings/SettingsItem.js index efb9bde..3b61d3e 100644 --- a/src/components/NodeDetailView/settings/SettingsItem.js +++ b/src/components/NodeDetailView/settings/SettingsItem.js @@ -1,6 +1,6 @@ import React, {useEffect} from "react"; import { useDispatch } from 'react-redux'; -import { toggleItemVisibility } from '../../../redux/actions'; +import { toggleMetadataItemVisibility } from '../../../redux/actions'; import { Typography, @@ -16,7 +16,7 @@ import VisibilityOffRoundedIcon from "@material-ui/icons/VisibilityOffRounded"; const SettingsItem = props => { const { groupTitle, item } = props; const dispatch = useDispatch(); - const toggleItemDisabled = () => dispatch(toggleItemVisibility(groupTitle, item.key)) + const toggleItemDisabled = () => dispatch(toggleMetadataItemVisibility(groupTitle, item.key)) return ( { edge="end" aria-label={!item.visible ? "add" : "delete"} onClick={toggleItemDisabled} + disableRipple > {!item.visible ? ( ({ type: ADD_DATASET, @@ -54,17 +54,17 @@ export const toggleSettingsPanelVisibility = visible => ({ }); -export const toggleItemVisibility = (groupTitle, itemId) => ({ - type: TOGGLE_ITEM_VISIBILITY, +export const toggleMetadataItemVisibility = (groupTitle, itemId) => ({ + type: TOGGLE_METADATA_ITEM_VISIBILITY, data: { groupTitle, itemId, }, }); -export const updateItemsOrder = ({ groupTitle, newItemsOrder }) => { +export const updateMetaDataItemsOrder = ({ groupTitle, newItemsOrder }) => { return { - type: UPDATE_ITEMS_ORDER, + type: UPDATE_METADATA_ITEMS_ORDER, payload: { title: groupTitle, newItemsOrder }, }; }; \ No newline at end of file diff --git a/src/redux/initialState.js b/src/redux/initialState.js index ffb6d14..26ce71b 100644 --- a/src/redux/initialState.js +++ b/src/redux/initialState.js @@ -1,7 +1,7 @@ import * as Actions from './actions'; import * as LayoutActions from '@metacell/geppetto-meta-client/common/layout/actions'; import { rdfTypes } from "../utils/graphModel"; -import {TOGGLE_ITEM_VISIBILITY, UPDATE_ITEMS_ORDER} from "./actions"; +import {TOGGLE_METADATA_ITEM_VISIBILITY, UPDATE_METADATA_ITEMS_ORDER} from "./actions"; export const sdsInitialState = { "sdsState": { @@ -115,7 +115,7 @@ export default function sdsClientReducer(state = {}, action) { }; } break; - case TOGGLE_ITEM_VISIBILITY: + case TOGGLE_METADATA_ITEM_VISIBILITY: const { groupTitle, itemId } = action.data; const updatedMetadataModel = { ...state.metadata_model }; const groupIndex = updatedMetadataModel[groupTitle].findIndex(item => item.key === itemId); @@ -139,7 +139,7 @@ export default function sdsClientReducer(state = {}, action) { ...state, metadata_model: { ...updatedMetadataModel } }; - case UPDATE_ITEMS_ORDER: + case UPDATE_METADATA_ITEMS_ORDER: const { title, newItemsOrder } = action.payload; return { From a765626a64cc919bd554f31a7f80898c16e8fb4c Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Thu, 18 Jan 2024 20:27:29 +0100 Subject: [PATCH 18/76] remove unnecessary code --- .../NodeDetailView/settings/Settings.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/components/NodeDetailView/settings/Settings.js b/src/components/NodeDetailView/settings/Settings.js index 57feff1..ab1e4bc 100644 --- a/src/components/NodeDetailView/settings/Settings.js +++ b/src/components/NodeDetailView/settings/Settings.js @@ -17,22 +17,6 @@ const Settings = props => { }; return ( - {/**/} - {/* */} - {/* */} - {/* First List Title*/} - {/* */} - {/**/} { Object.keys(metaDataPropertiesModel).map(group => ) } From e94ebb5955101498cf7a5196e7391d2abd96fca3 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Mon, 29 Jan 2024 10:06:49 +0100 Subject: [PATCH 19/76] #sdsv-9 Ensure all urls on Metadata panel are displayed as link --- src/components/NodeDetailView/Details/utils.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/NodeDetailView/Details/utils.js b/src/components/NodeDetailView/Details/utils.js index d9219b8..2939404 100644 --- a/src/components/NodeDetailView/Details/utils.js +++ b/src/components/NodeDetailView/Details/utils.js @@ -20,11 +20,5 @@ export const simpleValue = (label, value) => { } export const isValidUrl = (urlString) => { - var urlPattern = new RegExp('^(https?:\\/\\/)?' + // validate protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name - '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string - '(\\#[-a-z\\d_]*)?$', 'i'); // validate fragment locator - return !!urlPattern.test(urlString); + return /(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?\/[a-zA-Z0-9]{2,}|((https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?)|(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})?/g.test(urlString); } \ No newline at end of file From af5f40b5a3389705dbf94ff937fc7503d4f74612 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Mon, 29 Jan 2024 10:26:40 +0100 Subject: [PATCH 20/76] #14 Clicking on graph node should scroll to section on metadata --- src/components/GraphViewer/GraphViewer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 372bf70..6c24635 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -11,7 +11,7 @@ import BugReportIcon from '@material-ui/icons/BugReport'; import { selectInstance } from '../../redux/actions'; import { useSelector, useDispatch } from 'react-redux'; import GeppettoGraphVisualization from '@metacell/geppetto-meta-ui/graph-visualization/Graph'; -import { GRAPH_SOURCE } from '../../constants'; +import {detailsLabel, GRAPH_SOURCE} from '../../constants'; import { rdfTypes, typesModel } from '../../utils/graphModel'; import config from "./../../config/app.json"; import AddRoundedIcon from '@material-ui/icons/AddRounded'; @@ -251,6 +251,8 @@ const GraphViewer = (props) => { source: GRAPH_SOURCE })); } + const divElement = document.getElementById(node.id + detailsLabel); + divElement?.scrollIntoView({ behavior: 'smooth' }); }; const handleLinkColor = link => { From 9bffcfdc6b4bed520784e757595fe2a2e1ea8a69 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 31 Jan 2024 14:21:33 +0100 Subject: [PATCH 21/76] #SDSV-5 Vertical Layout --- src/components/GraphViewer/GraphViewer.js | 214 +++++++++++++--------- 1 file changed, 130 insertions(+), 84 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 372bf70..e81f28b 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -39,6 +39,13 @@ const TOP_DOWN = { return graph.hierarchyVariant; } }; +const LEFT_RIGHT = { + label : "Vertical Layout", + layout : "lr", + maxNodesLevel : (graph) => { + return graph.hierarchyVariant; + } +}; const RADIAL_OUT = { label : "Radial View", layout : "null", @@ -78,17 +85,109 @@ const GraphViewer = (props) => { } } matchIndex = nodes.findIndex( n => n.id === conflict.id ); - let furthestLeft = conflict?.xPos; - if ( nodes[i].collapsed ) { - furthestLeft = conflict.xPos - ((((matchIndex - i )/2)) * nodeSpace ); - nodes[i].xPos =furthestLeft; + if ( selectedLayout.layout === TOP_DOWN.layout ) { + let furthestLeft = conflict?.xPos; + if ( nodes[i].collapsed ) { + furthestLeft = conflict.xPos - ((((matchIndex - i )/2)) * nodeSpace ); + nodes[i].xPos =furthestLeft; + } + positionsMap[level] = furthestLeft + nodeSpace; + nodes[i].fx = nodes[i].xPos; + nodes[i].fy = 50 * nodes[i].level; + } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { + let furthestLeft = conflict?.yPos; + if ( nodes[i].collapsed ) { + furthestLeft = conflict.yPos - ((((matchIndex - i )/2)) * nodeSpace ); + nodes[i].yPos =furthestLeft; + } + positionsMap[level] = furthestLeft + nodeSpace; + nodes[i].fy = nodes[i].yPos; + nodes[i].fx = 50 * nodes[i].level; } - positionsMap[level] = furthestLeft + nodeSpace; - nodes[i].fx = nodes[i].xPos; - nodes[i].fy = 50 * nodes[i].level; } } + const algorithm = (levels, furthestLeft) => { + let positionsMap = {}; + let levelsMapKeys = Object.keys(levels); + + levelsMapKeys.forEach( level => { + furthestLeft = 0 - (Math.ceil(levels[level].length)/2 * nodeSpace ); + positionsMap[level] = furthestLeft + nodeSpace; + levels[level]?.sort( (a, b) => { + if (a?.id < b?.id) return -1; + else return 1; + }); + }); + + // Start assigning the graph from the bottom up + let neighbors = 0; + levelsMapKeys.reverse().forEach( level => { + let collapsedInLevel = levels[level].filter( n => n.collapsed); + let notcollapsedInLevel = levels[level].filter( n => !n.collapsed); + levels[level].forEach ( (n, index) => { + neighbors = n?.neighbors?.filter(neighbor => { return neighbor.level > n.level }); + if ( !n.collapsed ) { + if ( neighbors?.length > 0 ) { + let max = Number.MIN_SAFE_INTEGER, min = Number.MAX_SAFE_INTEGER; + neighbors.forEach( neighbor => { + if ( selectedLayout.layout === TOP_DOWN.layout ) { + if ( neighbor.xPos > max ) { max = neighbor.xPos }; + if ( neighbor.xPos <= min ) { min = neighbor.xPos }; + } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { + if ( neighbor.yPos > max ) { max = neighbor.yPos }; + if ( neighbor.yPos <= min ) { min = neighbor.yPos }; + } + }); + if ( selectedLayout.layout === TOP_DOWN.layout ) { + n.xPos = min === max ? min : min + ((max - min) * .5); + } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { + n.yPos = min === max ? min : min + ((max - min) * .5); + } + positionsMap[n.level] = n.yPos + nodeSpace; + if ( notcollapsedInLevel?.length > 0 && collapsedInLevel.length > 0) { + updateNodes(levels[level], n, positionsMap, level, index); + } + + if ( selectedLayout.layout === TOP_DOWN.layout ) { + positionsMap[n.level] = n.xPos + nodeSpace; + n.fx = n.xPos; + n.fy = 50 * n.level; + } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { + positionsMap[n.level] = n.yPos + nodeSpace; + n.fy = n.yPos; + n.fx = 50 * n.level; + } + } else { + if ( selectedLayout.layout === TOP_DOWN.layout ) { + n.xPos = positionsMap[n.level] + nodeSpace; + positionsMap[n.level] = n.xPos; + n.fx = n.xPos; + n.fy = 50 * n.level; + } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { + n.yPos = positionsMap[n.level] + nodeSpace; + positionsMap[n.level] = n.yPos; + n.fy = n.yPos; + n.fx = 50 * n.level; + } + } + }else { + if ( selectedLayout.layout === TOP_DOWN.layout ) { + n.xPos = positionsMap[n.level] + nodeSpace; + positionsMap[n.level] = n.xPos; + n.fx = n.xPos; + n.fy = 50 * n.level; + } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { + n.yPos = positionsMap[n.level] + nodeSpace; + positionsMap[n.level] = n.yPos; + n.fy = n.yPos; + n.fx = 50 * n.level; + } + } + }) + }); + } + const getPrunedTree = () => { let nodesById = Object.fromEntries(window.datasets[props.graph_id].graph?.nodes?.map(node => [node.id, node])); window.datasets[props.graph_id].graph?.links?.forEach(link => { @@ -107,7 +206,6 @@ const GraphViewer = (props) => { let levelsMap = window.datasets[props.graph_id].graph.levelsMap; // // Calculate level with max amount of nodes - let maxLevel = Object.keys(levelsMap).reduce((a, b) => levelsMap[a].filter( l => !l.collapsed ).length > levelsMap[b].filter( l => !l.collapsed ).length ? a : b); (function traverseTree(node = nodesById[window.datasets[props.graph_id].graph?.nodes?.[0].id]) { visibleNodes.push(node); @@ -118,80 +216,24 @@ const GraphViewer = (props) => { nodes?.forEach(traverseTree); })(); // IIFE - if ( selectedLayout.layout === TOP_DOWN.layout ){ - let levels = {}; - visibleNodes.forEach( n => { - if ( levels[n.level] ){ - levels[n.level].push(n); - } else { - levels[n.level] = [n]; - } - }) + let levels = {}; + visibleNodes.forEach( n => { + if ( levels[n.level] ){ + levels[n.level].push(n); + } else { + levels[n.level] = [n]; + } + }) - // Calculate level with max amount of nodes - let highestLevel = Object.keys(levels).length; - let maxLevel = Object.keys(levels).reduce((a, b) => levels[a].length > levels[b].length ? a : b); - let maxLevelNodes = levels[maxLevel]; + // Calculate level with max amount of nodes + let maxLevel = Object.keys(levels).reduce((a, b) => levels[a].length > levels[b].length ? a : b); + let maxLevelNodes = levels[maxLevel]; - // Space between nodes - // The furthestLeft a node can be - let furthestLeft = 0 - (Math.ceil(maxLevelNodes.length)/2 * nodeSpace ); - let positionsMap = {}; + // Space between nodes + // The furthestLeft a node can be + let furthestLeft = 0 - (Math.ceil(maxLevelNodes.length)/2 * nodeSpace ); - let levelsMapKeys = Object.keys(levels); - - levelsMapKeys.forEach( level => { - furthestLeft = 0 - (Math.ceil(levels[level].length)/2 * nodeSpace ); - positionsMap[level] = furthestLeft + nodeSpace; - levels[level]?.sort( (a, b) => { - if (a?.id < b?.id) { - return -1; - } - if (a?.id > b?.id) { - return 1; - } - return 1; - }); - }); - - // Start assigning the graph from the bottom up - let neighbors = 0; - levelsMapKeys.reverse().forEach( level => { - let collapsedInLevel = levels[level].filter( n => n.collapsed); - let notcollapsedInLevel = levels[level].filter( n => !n.collapsed); - levels[level].forEach ( (n, index) => { - neighbors = n?.neighbors?.filter(neighbor => { return neighbor.level > n.level }); - if ( !n.collapsed ) { - if ( neighbors?.length > 0 ) { - let max = Number.MIN_SAFE_INTEGER, min = Number.MAX_SAFE_INTEGER; - neighbors.forEach( neighbor => { - if ( neighbor.xPos > max ) { max = neighbor.xPos }; - if ( neighbor.xPos <= min ) { min = neighbor.xPos }; - }); - n.xPos = min === max ? min : min + ((max - min) * .5); - positionsMap[n.level] = n.xPos + nodeSpace; - if ( notcollapsedInLevel?.length > 0 && collapsedInLevel.length > 0) { - updateNodes(levels[level], n, positionsMap, level, index); - } - positionsMap[n.level] = n.xPos + nodeSpace; - n.fx = n.xPos; - n.fy = 50 * n.level; - } else { - n.xPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.xPos; - n.fx = n.xPos; - n.fy = 50 * n.level; - - } - }else { - n.xPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.xPos ; - n.fx = n.xPos; - n.fy = 50 * n.level; - } - }) - }); - } + algorithm(levels, furthestLeft); const graph = { nodes : visibleNodes, links : visibleLinks, levelsMap : levelsMap, hierarchyVariant : maxLevel * 20 }; return graph; @@ -202,7 +244,7 @@ const GraphViewer = (props) => { const [selectedNode, setSelectedNode] = useState(null); const [highlightNodes, setHighlightNodes] = useState(new Set()); const [highlightLinks, setHighlightLinks] = useState(new Set()); - const [selectedLayout, setSelectedLayout] = React.useState(RADIAL_OUT); + const [selectedLayout, setSelectedLayout] = React.useState(TOP_DOWN); const [layoutAnchorEl, setLayoutAnchorEl] = React.useState(null); const [cameraPosition, setCameraPosition] = useState({ x : 0 , y : 0 }); const open = Boolean(layoutAnchorEl); @@ -318,7 +360,7 @@ const GraphViewer = (props) => { }; const setForce = () => { - if ( selectedLayout.layout !== TOP_DOWN.layout ){ + if ( selectedLayout.layout !== TOP_DOWN.layout || selectedLayout.layout !== LEFT_RIGHT.layout ){ let force = -100; graphRef?.current?.ggv?.current.d3Force('link').distance(0).strength(1); graphRef?.current?.ggv?.current.d3Force("charge").strength(force * 2); @@ -543,14 +585,14 @@ const GraphViewer = (props) => { data={data} // Create the Graph as 2 Dimensional d2={true} - cooldownTicks={selectedLayout.layout === TOP_DOWN.layout ? 0 : data?.nodes?.length} + cooldownTicks={ ( selectedLayout.layout === TOP_DOWN.layout || selectedLayout.layout === LEFT_RIGHT.layout) ? 0 : data?.nodes?.length } onEngineStop={onEngineStop} // Links properties linkColor = {handleLinkColor} linkWidth={2} - dagLevelDistance={selectedLayout.layout === TOP_DOWN.layout ? 60 : 0} + dagLevelDistance={( selectedLayout.layout !== TOP_DOWN.layout && selectedLayout.layout !== LEFT_RIGHT.layout ) ? 0 : 60} linkDirectionalParticles={1} - forceRadial={selectedLayout.layout === TOP_DOWN.layout ? 0 : 15} + forceRadial={( selectedLayout.layout !== TOP_DOWN.layout && selectedLayout.layout !== LEFT_RIGHT.layout ) ? 15 : 0} linkDirectionalParticleWidth={link => highlightLinks.has(link) ? 4 : 0} linkCanvasObjectMode={'replace'} onLinkHover={handleLinkHover} @@ -561,6 +603,9 @@ const GraphViewer = (props) => { if ( selectedLayout.layout === TOP_DOWN.layout ){ node.fx = node.xPos; node.fy = 50 * node.level; + } else if ( selectedLayout.layout === LEFT_RIGHT.layout ){ + node.fx = 50 * node.level; + node.fy = node.yPos; } return 100 / (node.level + 1); }} @@ -596,6 +641,7 @@ const GraphViewer = (props) => { > handleLayoutChange(RADIAL_OUT)}>{RADIAL_OUT.label} handleLayoutChange(TOP_DOWN)}>{TOP_DOWN.label} + handleLayoutChange(LEFT_RIGHT)}>{LEFT_RIGHT.label} zoomIn()}> From 960e823d80a789c2acaf671bca7c089acac0f667 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Wed, 14 Feb 2024 11:07:47 +0100 Subject: [PATCH 22/76] #16 apply different style to previously selected nodes --- src/components/GraphViewer/GraphViewer.js | 29 ++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 372bf70..6f8cee2 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -30,7 +30,9 @@ const GRAPH_COLORS = { textHoverRect: '#3779E1', textHover: 'white', textColor: '#2E3A59', - collapsedFolder : 'red' + collapsedFolder : 'red', + nodeSeen: '#E1E3E8', + textBGSeen: '#6E4795' }; const TOP_DOWN = { label : "Tree View", @@ -211,6 +213,7 @@ const GraphViewer = (props) => { const nodeSelected = useSelector(state => state.sdsState.instance_selected.graph_node); const groupSelected = useSelector(state => state.sdsState.group_selected.graph_node); const [collapsed, setCollapsed] = React.useState(true); + const [previouslySelectedNodes, setPreviouslySelectedNodes] = useState(new Set()); const handleLayoutClick = (event) => { setLayoutAnchorEl(event.currentTarget); @@ -504,6 +507,25 @@ const GraphViewer = (props) => { ); // reset canvas fill color ctx.fillStyle = GRAPH_COLORS.textHover; + } else if (previouslySelectedNodes.has(node.id)) { + // Apply different style previously selected nodes + roundRect( + ctx, + ...hoverRectPosition, + ...hoverRectDimensions, + hoverRectBorderRadius, + GRAPH_COLORS.nodeSeen, + 0.3 + ); + roundRect( + ctx, + ...textHoverPosition, + hoverRectDimensions[0], + hoverRectDimensions[1] / 4, + hoverRectBorderRadius, + GRAPH_COLORS.textBGSeen + ); + ctx.fillStyle = GRAPH_COLORS.textHover; } else { ctx.fillStyle = GRAPH_COLORS.textColor; } @@ -521,6 +543,11 @@ const GraphViewer = (props) => { }, [hoverNode] ); + useEffect(() => { + if (selectedNode) { + setPreviouslySelectedNodes(prev => new Set([...prev, selectedNode.id])); + } + }, [selectedNode]); return (
From ac67feaa0b950f82051360df31b34f847fb3aa94 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Mon, 19 Feb 2024 16:17:41 +0100 Subject: [PATCH 23/76] #23 Clicking on Graph Node should scroll down sidebar on the left to correct position --- src/components/Sidebar/List.js | 86 +++++++++---------- .../Sidebar/TreeView/InstancesTreeView.js | 14 +++ .../Sidebar/TreeView/TreeViewItem.js | 1 + 3 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/components/Sidebar/List.js b/src/components/Sidebar/List.js index d992d49..1ce703d 100644 --- a/src/components/Sidebar/List.js +++ b/src/components/Sidebar/List.js @@ -1,62 +1,62 @@ -import React from 'react'; -import { - Box, - IconButton, -} from '@material-ui/core'; +import React, {useEffect} from 'react'; +import {Box, IconButton} from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; import InstancesTreeView from './TreeView/InstancesTreeView'; -import { useSelector } from 'react-redux' +import {useDispatch, useSelector} from 'react-redux'; import SearchRoundedIcon from '@material-ui/icons/SearchRounded'; +import {selectInstance} from "../../redux/actions"; +import {TREE_SOURCE} from "../../constants"; + const SidebarContent = (props) => { const { expand, setExpand, searchTerm } = props; - const datasets = useSelector(state => state.sdsState.datasets); + const dispatch = useDispatch(); + + const datasets = useSelector((state) => state.sdsState.datasets); + const nodeSelected = useSelector((state) => state.sdsState.instance_selected); + useEffect(() => { + console.log('nodeSelected', nodeSelected) + if (nodeSelected?.tree_node?.id) { + const selectedNodeElement = document.getElementById(nodeSelected?.tree_node?.id); + + if (selectedNodeElement) { + selectedNodeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); + } + } + }, [nodeSelected]); + const renderContent = () => { if (datasets?.length > 0) { return ( - <> - Uploaded Instances - - { datasets.map((id, index) => ) } - - + <> + Uploaded Instances + + {datasets.map((id, index) => ( + + ))} + + ); } else { return ( - <> - - No instances to display yet. - - + <> + No instances to display yet. + ); } }; return ( - - {!expand ? ( - setExpand(!expand)} - className='shrink-btn' - > - - - ) : ( renderContent() ) - } - - ) -} + + {!expand ? ( + setExpand(!expand)} className='shrink-btn'> + + + ) : ( + renderContent() + )} + + ); +}; export default SidebarContent; - - -// ( -// <> -// Uploaded Instances -// { datasets.map( id => { -// return -// }) -// } -// -// ) diff --git a/src/components/Sidebar/TreeView/InstancesTreeView.js b/src/components/Sidebar/TreeView/InstancesTreeView.js index 48bb1bc..902d777 100644 --- a/src/components/Sidebar/TreeView/InstancesTreeView.js +++ b/src/components/Sidebar/TreeView/InstancesTreeView.js @@ -132,6 +132,7 @@ const InstancesTreeView = (props) => { { }; const treeRef = React.createRef(); + const openSpecificTreeItem = (itemId) => { + const node = window.datasets[dataset_id].splinter.tree_map.get(itemId); + if (node && node.path !== undefined) { + setNodes(node.path); + // Dispatch onNodeSelect action to select the specific tree item + dispatch(selectInstance({ + dataset_id: dataset_id, + graph_node: node?.graph_reference?.id, + tree_node: node?.id, + source: TREE_SOURCE + })); + } + }; return ( <> diff --git a/src/components/Sidebar/TreeView/TreeViewItem.js b/src/components/Sidebar/TreeView/TreeViewItem.js index ae68466..591780f 100644 --- a/src/components/Sidebar/TreeView/TreeViewItem.js +++ b/src/components/Sidebar/TreeView/TreeViewItem.js @@ -18,6 +18,7 @@ const StyledTreeItem = (props) => { return ( From 26e0f948ca487b026c46e9a282434a9df4f33d92 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 21 Feb 2024 13:46:42 -0800 Subject: [PATCH 24/76] #sdsv-23 - Fix id extraction when retrieving tree_nodes --- src/utils/Splinter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 417bf4d..f00f463 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -985,7 +985,8 @@ class Splinter { // generate the Graph this.forced_nodes = Array.from(this.nodes).map(([key, value]) => { - let tree_node = this.tree_map.get(value.id); + const id = value?.id?.match(/https?:\/\/[^\s]+/)?.[0] || ""; + let tree_node = this.tree_map.get(id); if (tree_node) { value.tree_reference = tree_node; this.nodes.set(key, value); From 7322db6c5ac3cd5e41e0fdffe279b86af6a9175b Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Wed, 28 Feb 2024 14:48:03 +0100 Subject: [PATCH 25/76] remove unnecessary code --- src/components/Sidebar/List.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Sidebar/List.js b/src/components/Sidebar/List.js index 1ce703d..5c18ccf 100644 --- a/src/components/Sidebar/List.js +++ b/src/components/Sidebar/List.js @@ -14,7 +14,6 @@ const SidebarContent = (props) => { const datasets = useSelector((state) => state.sdsState.datasets); const nodeSelected = useSelector((state) => state.sdsState.instance_selected); useEffect(() => { - console.log('nodeSelected', nodeSelected) if (nodeSelected?.tree_node?.id) { const selectedNodeElement = document.getElementById(nodeSelected?.tree_node?.id); From c83471cf8d2278532ea98dc2dea9b7cc1c414591 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 8 Mar 2024 09:31:36 -0800 Subject: [PATCH 26/76] #SDSV-24 - Add k8s files for kubernetes deployment using codefresh and metacell cluster. --- Dockerfile | 34 ++++++++++++++++++++++----------- deploy/k8s/codefresh.yaml | 35 ++++++++++++++++++++++++++++++++++ deploy/k8s/ingress_tpl.yaml | 14 +++++++------- deploy/k8s/sds_viewer_tpl.yaml | 24 +++++++++++------------ nginx/default.conf | 32 +++++++++++++++++++++++++++++++ package.json | 2 +- 6 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 deploy/k8s/codefresh.yaml create mode 100644 nginx/default.conf diff --git a/Dockerfile b/Dockerfile index 97f9e87..83c03fa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,27 @@ -# node-sass 4.14.1 requires node version <= 14 for Alpine Linux -# See: https://github.com/sass/node-sass/releases/tag/v4.14.1 -FROM node:16-alpine as build-deps -WORKDIR /usr/src/app -RUN pwd && ls -COPY yarn.lock ./ +ARG NODE_PARENT=node:16-alpine + +FROM ${NODE_PARENT} as frontend + +ENV BUILDDIR=/app + +RUN apk add git +RUN npm i -g @craco/craco + +WORKDIR ${BUILDDIR} +COPY package.json ${BUILDDIR} +COPY yarn.lock ${BUILDDIR} +COPY nginx/default.conf ${BUILDDIR} + RUN yarn install -COPY . ./ +COPY . ${BUILDDIR} RUN yarn build -COPY public/ ./public/ -COPY src/ ./src/ +FROM nginx:1.19.3-alpine + +RUN cat /etc/nginx/conf.d/default.conf + +COPY --from=frontend /app/default.conf /etc/nginx/conf.d/default.conf + +COPY --from=frontend /app/build /usr/share/nginx/html/ -EXPOSE 3000 -CMD yarn run start +EXPOSE 80 \ No newline at end of file diff --git a/deploy/k8s/codefresh.yaml b/deploy/k8s/codefresh.yaml new file mode 100644 index 0000000..444f4b6 --- /dev/null +++ b/deploy/k8s/codefresh.yaml @@ -0,0 +1,35 @@ +version: "1.0" +stages: + - "clone" + - "build" + - "deploy" +steps: + clone: + stage: "clone" + title: "Cloning SDS Viewer" + type: "git-clone" + repo: "metacell/sds-viewer" + revision: "${{CF_BRANCH}}" + build: + stage: "build" + title: "Building SDS Viewer" + type: "build" + image_name: "sds-viewer" + tag: "${{CF_BUILD_ID}}" + dockerfile: Dockerfile + working_directory: ./sds-viewer + buildkit: true + registry: "${{CODEFRESH_REGISTRY}}" + deploy: + stage: "deploy" + title: "Deploying SDS Viewer" + image: codefresh/kubectl + working_directory: ./sds-viewer/deploy/k8s + commands: + - export CLUSTER_NAME="${{CLUSTER_NAME}}" + - export NAMESPACE="${{NAMESPACE}}" + - export CF_BUILD_ID="${{CF_BUILD_ID}}" + - export REGISTRY="${{REGISTRY}}/" + - export DOMAIN="${{DOMAIN}}" + - chmod +x ./deploy.sh + - ./deploy.sh \ No newline at end of file diff --git a/deploy/k8s/ingress_tpl.yaml b/deploy/k8s/ingress_tpl.yaml index befd530..9faf5d0 100755 --- a/deploy/k8s/ingress_tpl.yaml +++ b/deploy/k8s/ingress_tpl.yaml @@ -1,29 +1,29 @@ apiVersion: cert-manager.io/v1alpha2 kind: Issuer metadata: - name: 'letsencrypt-sds_viewer' + name: 'letsencrypt-sdsviewer' spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: filippo@metacell.us privateKeySecretRef: - name: letsencrypt-sds_viewer + name: letsencrypt-sdsviewer solvers: - http01: ingress: - ingressName: sds_viewer + ingressName: sds-viewer --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - cert-manager.io/issuer: letsencrypt-sds_viewer + cert-manager.io/issuer: letsencrypt-sdsviewer kubernetes.io/ingress.class: nginx kubernetes.io/tls-acme: 'true' nginx.ingress.kubernetes.io/ssl-redirect: 'true' nginx.ingress.kubernetes.io/proxy-body-size: 512m nginx.ingress.kubernetes.io/from-to-www-redirect: 'true' - name: sds_viewer + name: sds-viewer spec: rules: - host: "{{DOMAIN}}" @@ -31,7 +31,7 @@ spec: paths: - backend: service: - name: sds_viewer + name: sds-viewer port: number: 80 path: / @@ -39,4 +39,4 @@ spec: tls: - hosts: - "{{DOMAIN}}" - secretName: sds_viewer-tls + secretName: sds-viewer-tls diff --git a/deploy/k8s/sds_viewer_tpl.yaml b/deploy/k8s/sds_viewer_tpl.yaml index 95bb514..b7be103 100755 --- a/deploy/k8s/sds_viewer_tpl.yaml +++ b/deploy/k8s/sds_viewer_tpl.yaml @@ -1,39 +1,39 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: sds_viewer + name: sds-viewer spec: selector: matchLabels: - app: sds_viewer + app: sds-viewer replicas: 1 template: metadata: labels: - app: sds_viewer + app: sds-viewer spec: containers: - - name: sds_viewer - image: "{{REGISTRY}}sds_viewer:{{TAG}}" + - name: sds-viewer + image: "{{REGISTRY}}sds-viewer:{{TAG}}" imagePullPolicy: "IfNotPresent" ports: - containerPort: 80 resources: - requests: - memory: "64Mi" - cpu: "25m" limits: - memory: "128Mi" - cpu: "100m" + cpu: 1500m + memory: 768Mi + requests: + cpu: 500m + memory: 768Mi --- apiVersion: v1 kind: Service metadata: - name: sds_viewer + name: sds-viewer spec: type: LoadBalancer ports: - port: 80 targetPort: 80 selector: - app: sds_viewer + app: sds-viewer diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..8c5795f --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,32 @@ +upstream sds-viewer { + server sds-viewer:8000; +} + +server { + listen 80; + + location / { + root /usr/share/nginx/html/; + # index index.html index.htm; + try_files $uri /index.html; + } + + location /sds-viewer/ { + root /usr/share/nginx/html/; + # index index.html index.htm; + try_files $uri /index.html; + } + + location ~* ^/(admin|api|logged-out|login|sds-viewer|complete|disconnect|__debug__)/.*$ { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_redirect off; + proxy_pass http://sds-viewer; + } + + location /static/ { + autoindex on; + alias /usr/share/nginx/html/static/; + } +} \ No newline at end of file diff --git a/package.json b/package.json index b98a9e7..c6376f4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "license": "MIT", "private": true, - "homepage": "http://metacell.github.io/sds-viewer", + "homepage": "./", "dependencies": { "@craco/craco": "^6.1.2", "@frogcat/ttl2jsonld": "^0.0.7", From 01521b9fc84e079cc57bb238c30cb06c2a4da5a0 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 8 Mar 2024 13:41:38 -0800 Subject: [PATCH 27/76] #SDSV-5 Improve vertical layout code --- src/components/GraphViewer/GraphViewer.js | 378 +----------------- .../NodeDetailView/Details/SubjectDetails.js | 2 +- src/utils/GraphViewerHelper.js | 339 ++++++++++++++++ 3 files changed, 361 insertions(+), 358 deletions(-) create mode 100644 src/utils/GraphViewerHelper.js diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 006d45e..fbc39b8 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -13,234 +13,27 @@ import { useSelector, useDispatch } from 'react-redux'; import GeppettoGraphVisualization from '@metacell/geppetto-meta-ui/graph-visualization/Graph'; import {detailsLabel, GRAPH_SOURCE} from '../../constants'; import { rdfTypes, typesModel } from '../../utils/graphModel'; +import { getPrunedTree,paintNode, collapseSubLevels, GRAPH_COLORS, TOP_DOWN, LEFT_RIGHT, RADIAL_OUT, + ZOOM_SENSITIVITY,ZOOM_DEFAULT, ONE_SECOND } from '../../utils/GraphViewerHelper'; import config from "./../../config/app.json"; import AddRoundedIcon from '@material-ui/icons/AddRounded'; import RemoveRoundedIcon from '@material-ui/icons/RemoveRounded'; import {ViewTypeIcon} from "../../images/Icons"; -const NODE_FONT = '500 5px Inter, sans-serif'; -const ONE_SECOND = 1000; -const LOADING_TIME = 1000; -const ZOOM_DEFAULT = 1; -const ZOOM_SENSITIVITY = 0.2; -const GRAPH_COLORS = { - link: '#CFD4DA', - linkHover : 'purple', - hoverRect: '#CFD4DA', - textHoverRect: '#3779E1', - textHover: 'white', - textColor: '#2E3A59', - collapsedFolder : 'red', - nodeSeen: '#E1E3E8', - textBGSeen: '#6E4795' -}; -const TOP_DOWN = { - label : "Tree View", - layout : "td", - maxNodesLevel : (graph) => { - return graph.hierarchyVariant; - } -}; -const LEFT_RIGHT = { - label : "Vertical Layout", - layout : "lr", - maxNodesLevel : (graph) => { - return graph.hierarchyVariant; - } -}; -const RADIAL_OUT = { - label : "Radial View", - layout : "null", - maxNodesLevel : (graph) => { - return graph.radialVariant - } -}; - -const nodeSpace = 50; - -const roundRect = (ctx, x, y, width, height, radius, color, alpha) => { - if (width < 2 * radius) radius = width / 2; - if (height < 2 * radius) radius = height / 2; - ctx.globalAlpha = alpha || 1; - ctx.fillStyle = color; - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.arcTo(x + width, y, x + width, y + height, radius); - ctx.arcTo(x + width, y + height, x, y + height, radius); - ctx.arcTo(x, y + height, x, y, radius); - ctx.arcTo(x, y, x + width, y, radius); - ctx.closePath(); - ctx.fill(); -}; +const styles = { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + top: 0, + margin: 'auto', + color: "#11bffe", + size: "55rem" +} const GraphViewer = (props) => { const dispatch = useDispatch(); - const updateNodes = (nodes, conflictNode, positionsMap, level, index) => { - let matchIndex = index; - for ( let i = 0; i < index ; i++ ) { - let conflict = nodes.find ( n => !n.collapsed && n?.parent?.id === nodes[i]?.parent?.id) - if ( conflict === undefined ){ - conflict = nodes.find ( n => !n.collapsed ) - if ( conflict === undefined ){ - conflict = conflictNode; - } - } - matchIndex = nodes.findIndex( n => n.id === conflict.id ); - if ( selectedLayout.layout === TOP_DOWN.layout ) { - let furthestLeft = conflict?.xPos; - if ( nodes[i].collapsed ) { - furthestLeft = conflict.xPos - ((((matchIndex - i )/2)) * nodeSpace ); - nodes[i].xPos =furthestLeft; - } - positionsMap[level] = furthestLeft + nodeSpace; - nodes[i].fx = nodes[i].xPos; - nodes[i].fy = 50 * nodes[i].level; - } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { - let furthestLeft = conflict?.yPos; - if ( nodes[i].collapsed ) { - furthestLeft = conflict.yPos - ((((matchIndex - i )/2)) * nodeSpace ); - nodes[i].yPos =furthestLeft; - } - positionsMap[level] = furthestLeft + nodeSpace; - nodes[i].fy = nodes[i].yPos; - nodes[i].fx = 50 * nodes[i].level; - } - } - } - - const algorithm = (levels, furthestLeft) => { - let positionsMap = {}; - let levelsMapKeys = Object.keys(levels); - - levelsMapKeys.forEach( level => { - furthestLeft = 0 - (Math.ceil(levels[level].length)/2 * nodeSpace ); - positionsMap[level] = furthestLeft + nodeSpace; - levels[level]?.sort( (a, b) => { - if (a?.id < b?.id) return -1; - else return 1; - }); - }); - - // Start assigning the graph from the bottom up - let neighbors = 0; - levelsMapKeys.reverse().forEach( level => { - let collapsedInLevel = levels[level].filter( n => n.collapsed); - let notcollapsedInLevel = levels[level].filter( n => !n.collapsed); - levels[level].forEach ( (n, index) => { - neighbors = n?.neighbors?.filter(neighbor => { return neighbor.level > n.level }); - if ( !n.collapsed ) { - if ( neighbors?.length > 0 ) { - let max = Number.MIN_SAFE_INTEGER, min = Number.MAX_SAFE_INTEGER; - neighbors.forEach( neighbor => { - if ( selectedLayout.layout === TOP_DOWN.layout ) { - if ( neighbor.xPos > max ) { max = neighbor.xPos }; - if ( neighbor.xPos <= min ) { min = neighbor.xPos }; - } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { - if ( neighbor.yPos > max ) { max = neighbor.yPos }; - if ( neighbor.yPos <= min ) { min = neighbor.yPos }; - } - }); - if ( selectedLayout.layout === TOP_DOWN.layout ) { - n.xPos = min === max ? min : min + ((max - min) * .5); - } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { - n.yPos = min === max ? min : min + ((max - min) * .5); - } - positionsMap[n.level] = n.yPos + nodeSpace; - if ( notcollapsedInLevel?.length > 0 && collapsedInLevel.length > 0) { - updateNodes(levels[level], n, positionsMap, level, index); - } - - if ( selectedLayout.layout === TOP_DOWN.layout ) { - positionsMap[n.level] = n.xPos + nodeSpace; - n.fx = n.xPos; - n.fy = 50 * n.level; - } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { - positionsMap[n.level] = n.yPos + nodeSpace; - n.fy = n.yPos; - n.fx = 50 * n.level; - } - } else { - if ( selectedLayout.layout === TOP_DOWN.layout ) { - n.xPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.xPos; - n.fx = n.xPos; - n.fy = 50 * n.level; - } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { - n.yPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.yPos; - n.fy = n.yPos; - n.fx = 50 * n.level; - } - } - }else { - if ( selectedLayout.layout === TOP_DOWN.layout ) { - n.xPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.xPos; - n.fx = n.xPos; - n.fy = 50 * n.level; - } else if ( selectedLayout.layout === LEFT_RIGHT.layout ) { - n.yPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.yPos; - n.fy = n.yPos; - n.fx = 50 * n.level; - } - } - }) - }); - } - - const getPrunedTree = () => { - let nodesById = Object.fromEntries(window.datasets[props.graph_id].graph?.nodes?.map(node => [node.id, node])); - window.datasets[props.graph_id].graph?.links?.forEach(link => { - const source = link.source.id; - const target = link.target.id; - const linkFound = !nodesById[source]?.childLinks?.find( l => - source === l.source.id && target === l.target.id - ); - if ( linkFound ) { - nodesById[source]?.childLinks?.push(link); - } - }); - - let visibleNodes = []; - const visibleLinks = []; - - let levelsMap = window.datasets[props.graph_id].graph.levelsMap; - // // Calculate level with max amount of nodes - - (function traverseTree(node = nodesById[window.datasets[props.graph_id].graph?.nodes?.[0].id]) { - visibleNodes.push(node); - if (node.collapsed) return; - // let childLinks = node.childLinks?.filter( link => !link.source.collapsed && !link.target.collapsed ); - visibleLinks.push(...node.childLinks); - let nodes = node.childLinks.map(link => (typeof link.target) === 'object' ? link.target : nodesById[link.target]); - nodes?.forEach(traverseTree); - })(); // IIFE - - let levels = {}; - visibleNodes.forEach( n => { - if ( levels[n.level] ){ - levels[n.level].push(n); - } else { - levels[n.level] = [n]; - } - }) - - // Calculate level with max amount of nodes - let maxLevel = Object.keys(levels).reduce((a, b) => levels[a].length > levels[b].length ? a : b); - let maxLevelNodes = levels[maxLevel]; - - // Space between nodes - // The furthestLeft a node can be - let furthestLeft = 0 - (Math.ceil(maxLevelNodes.length)/2 * nodeSpace ); - - algorithm(levels, furthestLeft); - - const graph = { nodes : visibleNodes, links : visibleLinks, levelsMap : levelsMap, hierarchyVariant : maxLevel * 20 }; - return graph; - }; - const graphRef = React.useRef(null); const [hoverNode, setHoverNode] = useState(null); const [selectedNode, setSelectedNode] = useState(null); @@ -248,14 +41,12 @@ const GraphViewer = (props) => { const [highlightLinks, setHighlightLinks] = useState(new Set()); const [selectedLayout, setSelectedLayout] = React.useState(TOP_DOWN); const [layoutAnchorEl, setLayoutAnchorEl] = React.useState(null); - const [cameraPosition, setCameraPosition] = useState({ x : 0 , y : 0 }); const open = Boolean(layoutAnchorEl); const [loading, setLoading] = React.useState(false); const [data, setData] = React.useState({ nodes : [], links : []}); const nodeSelected = useSelector(state => state.sdsState.instance_selected.graph_node); const groupSelected = useSelector(state => state.sdsState.group_selected.graph_node); const [collapsed, setCollapsed] = React.useState(true); - const [previouslySelectedNodes, setPreviouslySelectedNodes] = useState(new Set()); const handleLayoutClick = (event) => { setLayoutAnchorEl(event.currentTarget); @@ -271,19 +62,11 @@ const GraphViewer = (props) => { setForce(); }; - const collapseSubLevels = (node, collapsed, children) => { - node?.childLinks?.forEach( n => { - if ( collapsed !== undefined ) n.target.collapsed = collapsed; - collapseSubLevels(n.target, collapsed, children); - children.links = children.links + 1; - }); - } - const handleNodeLeftClick = (node, event) => { if ( node.type === rdfTypes.Subject.key || node.type === rdfTypes.Sample.key || node.type === rdfTypes.Collection.key ) { node.collapsed = !node.collapsed; collapseSubLevels(node, node.collapsed, { links : 0 }); - const updatedData = getPrunedTree(); + const updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); } handleNodeHover(node); @@ -317,14 +100,13 @@ const GraphViewer = (props) => { const handleNodeRightClick = (node, event) => { graphRef?.current?.ggv?.current.centerAt(node.x, node.y, ONE_SECOND); graphRef?.current?.ggv?.current.zoom(2, ONE_SECOND); - //setCameraPosition({ x : node.x , y : node.y }); }; const expandAll = (event) => { window.datasets[props.graph_id].graph?.nodes?.forEach( node => { collapsed ? node.collapsed = !collapsed : node.collapsed = node?.type === typesModel.NamedIndividual.subject.type; }) - let updatedData = getPrunedTree(); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); setCollapsed(!collapsed) } @@ -382,18 +164,18 @@ const GraphViewer = (props) => { } useEffect(() => { - const updatedData = getPrunedTree(); + const updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); setLoading(true); setForce(); setTimeout ( () => { setLoading(false); setForce(); - }, LOADING_TIME); + }, ONE_SECOND); }, []); useEffect(() => { - const updatedData = getPrunedTree(); + const updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); },[selectedLayout]); @@ -402,7 +184,7 @@ const GraphViewer = (props) => { let visibleNodes = e.detail; let match = visibleNodes?.find( v => v?._attributes?.id === props.graph_id ); if ( match ) { - const updatedData = getPrunedTree(); + const updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); setTimeout( timeout => { setForce() @@ -415,8 +197,6 @@ const GraphViewer = (props) => { let match = visibleNodes?.find( v => v?._attributes?.id === props.graph_id ); if ( match ) { resetCamera(); - let center = graphRef?.current?.ggv?.current.centerAt(); - setCameraPosition({ x : center?.x , y : center?.y }); } }); }); @@ -442,7 +222,7 @@ const GraphViewer = (props) => { if ( collapsed ) { node.collapsed = !node.collapsed; collapseSubLevels(node, node.collapsed, { links : 0 }); - const updatedData = getPrunedTree(); + const updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); } setSelectedNode(nodeSelected); @@ -486,126 +266,10 @@ const GraphViewer = (props) => { setHighlightNodes(highlightNodes); } - const paintNode = React.useCallback( - (node, ctx) => { - const size = 7.5; - const nodeImageSize = [size * 2.4, size * 2.4]; - const hoverRectDimensions = [size * 4.2, size * 4.2]; - const hoverRectPosition = [node.x - hoverRectDimensions[0]/2, node.y - hoverRectDimensions[1]/2]; - const textHoverPosition = [ - hoverRectPosition[0], - hoverRectPosition[1] + hoverRectDimensions[1], - ]; - const hoverRectBorderRadius = 1; - ctx.beginPath(); - - try { - ctx.drawImage( - node?.img, - node.x - size, - node.y - size, - ...nodeImageSize - ); - } catch (error) { - const img = new Image(); - img.src = rdfTypes.Unknown.image; - node.img = img; - - // Add default icon if new icon wasn't found under images - ctx.drawImage( - node?.img, - node.x - size - 1, - node.y - size, - ...nodeImageSize - ); - } - - ctx.font = NODE_FONT; - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - let nodeName = node.name; - if (nodeName.length > 10) { - nodeName = nodeName.substr(0, 9).concat('...'); - } else if ( Array.isArray(nodeName) ){ - nodeName = nodeName[0]?.substr(0, 9).concat('...'); - } - const textProps = [nodeName, node.x, textHoverPosition[1]]; - if (node === hoverNode || node?.id === selectedNode?.id || node?.id === nodeSelected?.id ) { - // image hover - roundRect( - ctx, - ...hoverRectPosition, - ...hoverRectDimensions, - hoverRectBorderRadius, - GRAPH_COLORS.hoverRec, - 0.3 - ); - // text node name hover - roundRect( - ctx, - ...textHoverPosition, - hoverRectDimensions[0], - hoverRectDimensions[1] / 4, - hoverRectBorderRadius, - GRAPH_COLORS.textHoverRect - ); - // reset canvas fill color - ctx.fillStyle = GRAPH_COLORS.textHover; - } else if (previouslySelectedNodes.has(node.id)) { - // Apply different style previously selected nodes - roundRect( - ctx, - ...hoverRectPosition, - ...hoverRectDimensions, - hoverRectBorderRadius, - GRAPH_COLORS.nodeSeen, - 0.3 - ); - roundRect( - ctx, - ...textHoverPosition, - hoverRectDimensions[0], - hoverRectDimensions[1] / 4, - hoverRectBorderRadius, - GRAPH_COLORS.textBGSeen - ); - ctx.fillStyle = GRAPH_COLORS.textHover; - } else { - ctx.fillStyle = GRAPH_COLORS.textColor; - } - ctx.fillText(...textProps); - if ( node.childLinks?.length && node.collapsed ) { - let children = { links : 0 }; - collapseSubLevels(node, undefined, children) - const collapsedNodes = [children.links, node.x, textHoverPosition[1]]; - ctx.fillStyle = GRAPH_COLORS.collapsedFolder; - ctx.textAlign = 'center'; - ctx.textBaseline = 'bottom'; - ctx.fillText(...collapsedNodes); - ctx.fillStyle = GRAPH_COLORS.textColor; - } - }, - [hoverNode] - ); - useEffect(() => { - if (selectedNode) { - setPreviouslySelectedNodes(prev => new Set([...prev, selectedNode.id])); - } - }, [selectedNode]); - return (
{ loading? - + : { linkCanvasObjectMode={'replace'} onLinkHover={handleLinkHover} // Override drawing of canvas objects, draw an image as a node - nodeCanvasObject={paintNode} + nodeCanvasObject={(node, ctx) => paintNode(node, ctx, hoverNode, selectedNode, nodeSelected)} nodeCanvasObjectMode={node => 'replace'} nodeVal = { node => { if ( selectedLayout.layout === TOP_DOWN.layout ){ diff --git a/src/components/NodeDetailView/Details/SubjectDetails.js b/src/components/NodeDetailView/Details/SubjectDetails.js index 2f30e10..24038ce 100644 --- a/src/components/NodeDetailView/Details/SubjectDetails.js +++ b/src/components/NodeDetailView/Details/SubjectDetails.js @@ -43,7 +43,7 @@ const SubjectDetails = (props) => { if ( property.isGroup ){ return ( {property.label} - + ) } diff --git a/src/utils/GraphViewerHelper.js b/src/utils/GraphViewerHelper.js new file mode 100644 index 0000000..bb0dd7d --- /dev/null +++ b/src/utils/GraphViewerHelper.js @@ -0,0 +1,339 @@ +import React, {useCallback} from 'react'; +import { rdfTypes } from './graphModel'; + +export const NODE_FONT = '500 5px Inter, sans-serif'; +export const ONE_SECOND = 1000; +export const ZOOM_DEFAULT = 1; +export const ZOOM_SENSITIVITY = 0.2; +export const GRAPH_COLORS = { + link: '#CFD4DA', + linkHover : 'purple', + hoverRect: '#CFD4DA', + textHoverRect: '#3779E1', + textHover: 'white', + textColor: '#2E3A59', + collapsedFolder : 'red' +}; +export const TOP_DOWN = { + label : "Tree View", + layout : "td", + maxNodesLevel : (graph) => { + return graph.hierarchyVariant; + } +}; +export const LEFT_RIGHT = { + label : "Vertical Layout", + layout : "lr", + maxNodesLevel : (graph) => { + return graph.hierarchyVariant; + } +}; +export const RADIAL_OUT = { + label : "Radial View", + layout : "null", + maxNodesLevel : (graph) => { + return graph.radialVariant + } +}; + +export const nodeSpace = 50; + +/** + * Create background for Nodes on Graph Viewer. + * @param {*} ctx - Canvas context rendering + * @param {*} x - x position of node, used to draw background + * @param {*} y - y position of node, used to draw background + * @param {*} width - needed width of background + * @param {*} height - needed height of background + * @param {*} radius - Radius of background + * @param {*} color - color used for the background + * @param {*} alpha - alpha color + */ +const roundRect = (ctx, x, y, width, height, radius, color, alpha) => { + if (width < 2 * radius) radius = width / 2; + if (height < 2 * radius) radius = height / 2; + ctx.globalAlpha = alpha || 1; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.arcTo(x + width, y, x + width, y + height, radius); + ctx.arcTo(x + width, y + height, x, y + height, radius); + ctx.arcTo(x, y + height, x, y, radius); + ctx.arcTo(x, y, x + width, y, radius); + ctx.closePath(); + ctx.fill(); +}; + +export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected) => { + const size = 7.5; + const nodeImageSize = [size * 2.4, size * 2.4]; + const hoverRectDimensions = [size * 4.2, size * 4.2]; + const hoverRectPosition = [node.x - hoverRectDimensions[0]/2, node.y - hoverRectDimensions[1]/2]; + const textHoverPosition = [ + hoverRectPosition[0], + hoverRectPosition[1] + hoverRectDimensions[1], + ]; + const hoverRectBorderRadius = 1; + ctx.beginPath(); + + try { + ctx.drawImage( + node?.img, + node.x - size, + node.y - size, + ...nodeImageSize + ); + } catch (error) { + const img = new Image(); + img.src = rdfTypes.Unknown.image; + node.img = img; + + // Add default icon if new icon wasn't found under images + ctx.drawImage( + node?.img, + node.x - size - 1, + node.y - size, + ...nodeImageSize + ); + } + + ctx.font = NODE_FONT; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + let nodeName = node.name; + if (nodeName.length > 10) { + nodeName = nodeName.substr(0, 9).concat('...'); + } else if ( Array.isArray(nodeName) ){ + nodeName = nodeName[0]?.substr(0, 9).concat('...'); + } + const textProps = [nodeName, node.x, textHoverPosition[1]]; + if (node === hoverNode || node?.id === selectedNode?.id || node?.id === nodeSelected?.id ) { + // image hover + roundRect( + ctx, + ...hoverRectPosition, + ...hoverRectDimensions, + hoverRectBorderRadius, + GRAPH_COLORS.hoverRec, + 0.3 + ); + // text node name hover + roundRect( + ctx, + ...textHoverPosition, + hoverRectDimensions[0], + hoverRectDimensions[1] / 4, + hoverRectBorderRadius, + GRAPH_COLORS.textHoverRect + ); + // reset canvas fill color + ctx.fillStyle = GRAPH_COLORS.textHover; + } else { + ctx.fillStyle = GRAPH_COLORS.textColor; + } + ctx.fillText(...textProps); + if ( node.childLinks?.length && node.collapsed ) { + let children = { links : 0 }; + collapseSubLevels(node, undefined, children) + const collapsedNodes = [children.links, node.x, textHoverPosition[1]]; + ctx.fillStyle = GRAPH_COLORS.collapsedFolder; + ctx.textAlign = 'center'; + ctx.textBaseline = 'bottom'; + ctx.fillText(...collapsedNodes); + ctx.fillStyle = GRAPH_COLORS.textColor; + } + } + +export const collapseSubLevels = (node, collapsed, children) => { + node?.childLinks?.forEach( n => { + if ( collapsed !== undefined ) n.target.collapsed = collapsed; + collapseSubLevels(n.target, collapsed, children); + children.links = children.links + 1; + }); +} + +/** + * Algorithm used to position nodes in a Tree. Position depends on the layout, + * either Tree or Vertical Layout views. + * @param {*} levels - How many levels we need for this tree. This depends on the dataset subjects/samples/folders/files. + * @param {*} layout + * @param {*} furthestLeft + */ +export const algorithm = (levels, layout, furthestLeft) => { + let positionsMap = {}; + let levelsMapKeys = Object.keys(levels); + + levelsMapKeys.forEach( level => { + furthestLeft = 0 - (Math.ceil(levels[level].length)/2 * nodeSpace ); + positionsMap[level] = furthestLeft + nodeSpace; + levels[level]?.sort( (a, b) => { + if (a?.id < b?.id) return -1; + else return 1; + }); + }); + + // Start assigning the graph from the bottom up + let neighbors = 0; + levelsMapKeys.reverse().forEach( level => { + let collapsedInLevel = levels[level].filter( n => n.collapsed); + let notcollapsedInLevel = levels[level].filter( n => !n.collapsed); + levels[level].forEach ( (n, index) => { + neighbors = n?.neighbors?.filter(neighbor => { return neighbor.level > n.level }); + if ( !n.collapsed ) { + if ( neighbors?.length > 0 ) { + let max = Number.MIN_SAFE_INTEGER, min = Number.MAX_SAFE_INTEGER; + neighbors.forEach( neighbor => { + if ( layout === TOP_DOWN.layout ) { + if ( neighbor.xPos > max ) { max = neighbor.xPos }; + if ( neighbor.xPos <= min ) { min = neighbor.xPos }; + } else if ( layout === LEFT_RIGHT.layout ) { + if ( neighbor.yPos > max ) { max = neighbor.yPos }; + if ( neighbor.yPos <= min ) { min = neighbor.yPos }; + } + }); + if ( layout === TOP_DOWN.layout ) { + n.xPos = min === max ? min : min + ((max - min) * .5); + } else if ( layout === LEFT_RIGHT.layout ) { + n.yPos = min === max ? min : min + ((max - min) * .5); + } + positionsMap[n.level] = n.yPos + nodeSpace; + if ( notcollapsedInLevel?.length > 0 && collapsedInLevel.length > 0) { + updateConflictedNodes(levels[level], n, positionsMap, level, index, layout); + } + + if ( layout === TOP_DOWN.layout ) { + positionsMap[n.level] = n.xPos + nodeSpace; + n.fx = n.xPos; + n.fy = 50 * n.level; + } else if ( layout === LEFT_RIGHT.layout ) { + positionsMap[n.level] = n.yPos + nodeSpace; + n.fy = n.yPos; + n.fx = 50 * n.level; + } + } else { + if ( layout === TOP_DOWN.layout ) { + n.xPos = positionsMap[n.level] + nodeSpace; + positionsMap[n.level] = n.xPos; + n.fx = n.xPos; + n.fy = 50 * n.level; + } else if ( layout === LEFT_RIGHT.layout ) { + n.yPos = positionsMap[n.level] + nodeSpace; + positionsMap[n.level] = n.yPos; + n.fy = n.yPos; + n.fx = 50 * n.level; + } + } + }else { + if ( layout === TOP_DOWN.layout ) { + n.xPos = positionsMap[n.level] + nodeSpace; + positionsMap[n.level] = n.xPos; + n.fx = n.xPos; + n.fy = 50 * n.level; + } else if ( layout === LEFT_RIGHT.layout ) { + n.yPos = positionsMap[n.level] + nodeSpace; + positionsMap[n.level] = n.yPos; + n.fy = n.yPos; + n.fx = 50 * n.level; + } + } + }) + }); + } + + /** + * Create Graph ID + * @param {*} graph_id - ID of dataset we need the tree for + * @param {*} layout - The desired layout in which we will display the data e.g. Tree, Vertical, Radial + * @returns + */ +export const getPrunedTree = (graph_id, layout) => { + let nodesById = Object.fromEntries(window.datasets[graph_id].graph?.nodes?.map(node => [node.id, node])); + window.datasets[graph_id].graph?.links?.forEach(link => { + const source = link.source.id; + const target = link.target.id; + const linkFound = !nodesById[source]?.childLinks?.find( l => + source === l.source.id && target === l.target.id + ); + if ( linkFound ) { + nodesById[source]?.childLinks?.push(link); + } + }); + + let visibleNodes = []; + const visibleLinks = []; + + let levelsMap = window.datasets[graph_id].graph.levelsMap; + // // Calculate level with max amount of nodes + + (function traverseTree(node = nodesById[window.datasets[graph_id].graph?.nodes?.[0].id]) { + visibleNodes.push(node); + if (node.collapsed) return; + // let childLinks = node.childLinks?.filter( link => !link.source.collapsed && !link.target.collapsed ); + visibleLinks.push(...node.childLinks); + let nodes = node.childLinks.map(link => (typeof link.target) === 'object' ? link.target : nodesById[link.target]); + nodes?.forEach(traverseTree); + })(); // IIFE + + let levels = {}; + visibleNodes.forEach( n => { + if ( levels[n.level] ){ + levels[n.level].push(n); + } else { + levels[n.level] = [n]; + } + }) + + // Calculate level with max amount of nodes + let maxLevel = Object.keys(levels).reduce((a, b) => levels[a].length > levels[b].length ? a : b); + let maxLevelNodes = levels[maxLevel]; + + // The furthestLeft a node can be + let furthestLeft = 0 - (Math.ceil(maxLevelNodes.length)/2 * nodeSpace ); + + algorithm(levels, layout, furthestLeft); + + const graph = { nodes : visibleNodes, links : visibleLinks, levelsMap : levelsMap, hierarchyVariant : maxLevel * 20 }; + return graph; + }; + + /** + * Update Nodes x and y position, used for vertical and tree view layouts. + * @param {*} nodes - The nodes we have for the dataset + * @param {*} conflictNode - Conflicting Node that needs re positioning + * @param {*} positionsMap - Object keeping track of positions of nodes + * @param {*} level - level of tree + * @param {*} index - Index of conflict node in this tree level + * @param {*} layout - The layout we are using to display these nodes + */ + const updateConflictedNodes = (nodes, conflictNode, positionsMap, level, index, layout) => { + let matchIndex = index; + for ( let i = 0; i < index ; i++ ) { + let conflict = nodes.find ( n => !n.collapsed && n?.parent?.id === nodes[i]?.parent?.id) + if ( conflict === undefined ){ + conflict = nodes.find ( n => !n.collapsed ) + if ( conflict === undefined ){ + conflict = conflictNode; + } + } + matchIndex = nodes.findIndex( n => n.id === conflict.id ); + if ( layout === TOP_DOWN.layout ) { + let furthestLeft = conflict?.xPos; + if ( nodes[i].collapsed ) { + furthestLeft = conflict.xPos - ((((matchIndex - i )/2)) * nodeSpace ); + nodes[i].xPos =furthestLeft; + } + positionsMap[level] = furthestLeft + nodeSpace; + nodes[i].fx = nodes[i].xPos; + nodes[i].fy = 50 * nodes[i].level; + } else if ( layout === LEFT_RIGHT.layout ) { + let furthestLeft = conflict?.yPos; + if ( nodes[i].collapsed ) { + furthestLeft = conflict.yPos - ((((matchIndex - i )/2)) * nodeSpace ); + nodes[i].yPos =furthestLeft; + } + positionsMap[level] = furthestLeft + nodeSpace; + nodes[i].fy = nodes[i].yPos; + nodes[i].fx = 50 * nodes[i].level; + } + } + } \ No newline at end of file From 53311976ccc0b55017feb5125f52d6b594b3fb24 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 8 Mar 2024 13:44:47 -0800 Subject: [PATCH 28/76] #SDSV-24 Remove cert-manager from ingress yaml --- deploy/k8s/ingress_tpl.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/deploy/k8s/ingress_tpl.yaml b/deploy/k8s/ingress_tpl.yaml index 9faf5d0..f7b8e86 100755 --- a/deploy/k8s/ingress_tpl.yaml +++ b/deploy/k8s/ingress_tpl.yaml @@ -1,23 +1,7 @@ -apiVersion: cert-manager.io/v1alpha2 -kind: Issuer -metadata: - name: 'letsencrypt-sdsviewer' -spec: - acme: - server: https://acme-v02.api.letsencrypt.org/directory - email: filippo@metacell.us - privateKeySecretRef: - name: letsencrypt-sdsviewer - solvers: - - http01: - ingress: - ingressName: sds-viewer ---- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - cert-manager.io/issuer: letsencrypt-sdsviewer kubernetes.io/ingress.class: nginx kubernetes.io/tls-acme: 'true' nginx.ingress.kubernetes.io/ssl-redirect: 'true' From a0e9e29a9192781bc3222e42fc8779522f53f3a5 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 8 Mar 2024 14:43:17 -0800 Subject: [PATCH 29/76] #SDSV-24 Revert cert-manager removal --- deploy/k8s/ingress_tpl.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/deploy/k8s/ingress_tpl.yaml b/deploy/k8s/ingress_tpl.yaml index f7b8e86..9faf5d0 100755 --- a/deploy/k8s/ingress_tpl.yaml +++ b/deploy/k8s/ingress_tpl.yaml @@ -1,7 +1,23 @@ +apiVersion: cert-manager.io/v1alpha2 +kind: Issuer +metadata: + name: 'letsencrypt-sdsviewer' +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: filippo@metacell.us + privateKeySecretRef: + name: letsencrypt-sdsviewer + solvers: + - http01: + ingress: + ingressName: sds-viewer +--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: + cert-manager.io/issuer: letsencrypt-sdsviewer kubernetes.io/ingress.class: nginx kubernetes.io/tls-acme: 'true' nginx.ingress.kubernetes.io/ssl-redirect: 'true' From a8878439fc0329b6596740c00371786e81810969 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 8 Mar 2024 14:47:51 -0800 Subject: [PATCH 30/76] #SDSV-24 Use nginx class for ingress solver --- deploy/k8s/ingress_tpl.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/k8s/ingress_tpl.yaml b/deploy/k8s/ingress_tpl.yaml index 9faf5d0..f903987 100755 --- a/deploy/k8s/ingress_tpl.yaml +++ b/deploy/k8s/ingress_tpl.yaml @@ -11,7 +11,7 @@ spec: solvers: - http01: ingress: - ingressName: sds-viewer + class: nginx --- apiVersion: networking.k8s.io/v1 kind: Ingress From 8c8ac3245667aed6974b6eadc8ec195acb7eb07a Mon Sep 17 00:00:00 2001 From: jrmartin Date: Sat, 9 Mar 2024 07:16:28 -0800 Subject: [PATCH 31/76] #SDSV-18 - Adds links to SPARC portal folders, subjects and samples from Metadata panel. Clean unused imports on details pages. --- .../Details/CollectionDetails.js | 65 ++++++++++--------- .../NodeDetailView/Details/DatasetDetails.js | 1 - .../NodeDetailView/Details/Details.js | 1 - .../NodeDetailView/Details/PersonDetails.js | 3 - .../NodeDetailView/Details/Primary.js | 1 - .../NodeDetailView/Details/ProtocolDetails.js | 1 - .../NodeDetailView/Details/Secondary.js | 1 - .../NodeDetailView/Details/SubjectDetails.js | 2 - src/components/NodeDetailView/factory.js | 9 +-- src/redux/initialState.js | 1 + src/utils/Splinter.js | 22 +++---- src/utils/graphModel.js | 55 +++++++++++++++- 12 files changed, 105 insertions(+), 57 deletions(-) diff --git a/src/components/NodeDetailView/Details/CollectionDetails.js b/src/components/NodeDetailView/Details/CollectionDetails.js index 0fcd75f..3ddd1c6 100644 --- a/src/components/NodeDetailView/Details/CollectionDetails.js +++ b/src/components/NodeDetailView/Details/CollectionDetails.js @@ -1,43 +1,50 @@ -import React from "react"; import { - Box, Divider, - Typography + Box, + Divider, + Typography, } from "@material-ui/core"; -import Links from './Views/Links'; import SimpleLabelValue from './Views/SimpleLabelValue'; +import SimpleLinkedChip from './Views/SimpleLinkedChip'; +import Links from './Views/Links'; import { detailsLabel } from '../../../constants'; +import { isValidUrl } from './utils'; +import { useSelector } from 'react-redux' + const CollectionDetails = (props) => { const { node } = props; - let title = ""; - let idDetails = ""; - // both tree and graph nodes are present, extract data from both - if (node?.tree_node && node?.graph_node) { - title = node.graph_node?.name; - idDetails = node.graph_node?.id + detailsLabel; - // the below is the case where we have data only from the tree/hierarchy - } else if (node?.tree_node) { - title = node.tree_node?.basename; - idDetails = node.tree_node?.id + detailsLabel; - // the below is the case where we have data only from the graph - } else { - title = node.graph_node?.name; - idDetails = node.graph_node?.id + detailsLabel; - } - + const collectionPropertiesModel = useSelector(state => state.sdsState.metadata_model.collection); return ( - + - - { node.graph_node?.attributes?.publishedURI && node.graph_node?.attributes?.publishedURI !== "" - ? ( - SPARC Portal Link - - ) - : <> - } + + + {collectionPropertiesModel?.map( property => { + if ( property.visible ){ + const propValue = node.graph_node.attributes[property.property]; + if ( isValidUrl(propValue) ){ + return ( + {property.label} + + ) + } + + else if ( typeof propValue === "object" ){ + return ( + {property.label} + + ) + } + + else if ( typeof propValue === "string" ){ + return () + } + + return (<> ) + } + })} ); diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index 3ebf8fb..7720da8 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -1,4 +1,3 @@ -import React from "react"; import { Box, Typography, diff --git a/src/components/NodeDetailView/Details/Details.js b/src/components/NodeDetailView/Details/Details.js index 063008f..957081e 100644 --- a/src/components/NodeDetailView/Details/Details.js +++ b/src/components/NodeDetailView/Details/Details.js @@ -1,4 +1,3 @@ -import React from "react"; import { Box, Divider, diff --git a/src/components/NodeDetailView/Details/PersonDetails.js b/src/components/NodeDetailView/Details/PersonDetails.js index fb48456..de4f1be 100644 --- a/src/components/NodeDetailView/Details/PersonDetails.js +++ b/src/components/NodeDetailView/Details/PersonDetails.js @@ -1,12 +1,9 @@ -import React from "react"; import { Box, Divider, Typography, } from "@material-ui/core"; import Links from './Views/Links'; -import SimpleLabelValue from './Views/SimpleLabelValue'; -import { detailsLabel } from '../../../constants'; const PersonDetails = (props) => { const { node } = props; diff --git a/src/components/NodeDetailView/Details/Primary.js b/src/components/NodeDetailView/Details/Primary.js index f5eb739..2e8b0a9 100644 --- a/src/components/NodeDetailView/Details/Primary.js +++ b/src/components/NodeDetailView/Details/Primary.js @@ -1,4 +1,3 @@ -import React from "react"; import { Box, Typography, diff --git a/src/components/NodeDetailView/Details/ProtocolDetails.js b/src/components/NodeDetailView/Details/ProtocolDetails.js index 5b3fa1c..d1e967c 100644 --- a/src/components/NodeDetailView/Details/ProtocolDetails.js +++ b/src/components/NodeDetailView/Details/ProtocolDetails.js @@ -1,4 +1,3 @@ -import React from "react"; import { Box, Divider, diff --git a/src/components/NodeDetailView/Details/Secondary.js b/src/components/NodeDetailView/Details/Secondary.js index 68eef3c..53c4c5f 100644 --- a/src/components/NodeDetailView/Details/Secondary.js +++ b/src/components/NodeDetailView/Details/Secondary.js @@ -1,4 +1,3 @@ -import React from "react"; import { Box, Typography, diff --git a/src/components/NodeDetailView/Details/SubjectDetails.js b/src/components/NodeDetailView/Details/SubjectDetails.js index 2f30e10..81cf895 100644 --- a/src/components/NodeDetailView/Details/SubjectDetails.js +++ b/src/components/NodeDetailView/Details/SubjectDetails.js @@ -6,9 +6,7 @@ import { import SimpleLinkedChip from './Views/SimpleLinkedChip'; import SimpleLabelValue from './Views/SimpleLabelValue'; import Links from './Views/Links'; -import { simpleValue } from './utils'; import { detailsLabel } from '../../../constants'; -import { rdfTypes } from "../../../utils/graphModel"; import { isValidUrl } from './utils'; import { useSelector } from 'react-redux' diff --git a/src/components/NodeDetailView/factory.js b/src/components/NodeDetailView/factory.js index 9fa0e22..baf846d 100644 --- a/src/components/NodeDetailView/factory.js +++ b/src/components/NodeDetailView/factory.js @@ -13,6 +13,7 @@ import DatasetDetails from './Details/DatasetDetails'; import SubjectDetails from './Details/SubjectDetails'; import ProtocolDetails from './Details/ProtocolDetails'; import GroupDetails from './Details/GroupDetails'; +import CollectionDetails from './Details/CollectionDetails' import Settings from "./settings/Settings" var DetailsFactory = function () { this.createDetails = function (node) { @@ -63,7 +64,7 @@ const Collection = function (node) { nodeDetail.getHeader = () => { return ( <> - {/* */} + ) }; @@ -71,7 +72,7 @@ const Collection = function (node) { nodeDetail.getDetail = () => { return ( <> - {/* */} + ) }; @@ -79,8 +80,8 @@ const Collection = function (node) { nodeDetail.getAll = () => { return ( <> - {/* - */} + + ) } diff --git a/src/redux/initialState.js b/src/redux/initialState.js index 26ce71b..91cf133 100644 --- a/src/redux/initialState.js +++ b/src/redux/initialState.js @@ -27,6 +27,7 @@ export const sdsInitialState = { dataset : [...rdfTypes.Dataset.properties], subject : [...rdfTypes.Subject.properties], sample : [...rdfTypes.Sample.properties], + collection : [...rdfTypes.Collection.properties], group : [...rdfTypes.Group.properties], file : [...rdfTypes.File.properties] } diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index f00f463..41dcc19 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -337,7 +337,7 @@ class Splinter { } else { this.nodes.set(node.id, { id: node.id, - attributes: {}, + attributes: {publishedURI : ""}, types: [], name: node.value, proxies: [], @@ -710,11 +710,11 @@ class Splinter { } } - if (node.attributes?.relativePath !== undefined) { + if (node.tree_reference?.dataset_relative_path !== undefined) { node.attributes.publishedURI = - Array.from(this.nodes)[0][1].attributes.hasUriPublished[0]?.replace("datasets", "file") + - "/1?path=files/" + - node.attributes?.relativePath; + [Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + + "?datasetDetailsTab=files&path=files/" + + node.tree_reference?.dataset_relative_path]; } } @@ -754,9 +754,9 @@ class Splinter { if (node.tree_reference?.dataset_relative_path !== undefined) { node.attributes.publishedURI = - Array.from(this.nodes)[0][1].attributes.hasUriPublished[0]?.replace("datasets", "file") + - "/1?path=files/" + - node.tree_reference?.dataset_relative_path; + [Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + + "?datasetDetailsTab=files&path=files/" + + node.tree_reference?.dataset_relative_path]; } } @@ -780,9 +780,9 @@ class Splinter { if (node.attributes?.relativePath !== undefined) { node.attributes.publishedURI = - Array.from(this.nodes)[0][1].attributes.hasUriPublished[0]?.replace("datasets", "file") + - "/1?path=files/" + - node.attributes?.relativePath; + Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + + "?datasetDetailsTab=files&path=files/" + + node.attributes?.relativePath; } } diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index 424227b..4a5d05d 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -51,14 +51,49 @@ export const rdfTypes = { "type": "rdfs", "key": "label", "property": "label", - "label": "To be filled", + "label": "Label", + "visible" : false + }, + { + "type": "rdfs", + "key": "identifier", + "property": "identifier", + "label": "Label", + "visible" : true + }, + { + "type": "rdfs", + "key": "mimetype", + "property": "mimetype", + "label": "Mimetype", + "visible" : false + }, + { + "type": "rdfs", + "key": "status", + "property": "status", + "label": "Status", + "visible" : true + }, + { + "type": "rdfs", + "key": "updated", + "property": "updated", + "label": "Updated On", "visible" : true }, { "type": "TEMP", "key": "hasUriHuman", "property": "hasUriHuman", - "label": "To be filled", + "label": "URI Link", + "visible" : false + }, + { + "type": "TEMP", + "key": "publishedURI", + "property": "publishedURI", + "label": "Published URI", "visible" : true } ] @@ -456,7 +491,7 @@ export const rdfTypes = { "property": "specimenHasIdentifier", "label": "Specimen has Identifier", "visible" : true, - "isGroup" : true + "isGroup" : false }, { "type": "sparc", @@ -523,6 +558,13 @@ export const rdfTypes = { "label": "Participant In Performance Of", "visible" : true }, + { + "type": "TEMP", + "key": "publishedURI", + "property": "publishedURI", + "label": "Published URI", + "visible" : true + } ], "additional_properties": [ { @@ -658,6 +700,13 @@ export const rdfTypes = { "property": "participantInPerformanceOf", "label": "Participant in Performance Of", "visible" : true + }, + { + "type": "TEMP", + "key": "publishedURI", + "property": "publishedURI", + "label": "Published URI", + "visible" : true } ] }, From 7aac4c10591b5fcd432aa7bc0d397e5f9d311fd2 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 12 Mar 2024 15:01:38 -0700 Subject: [PATCH 32/76] #SDSV-18 - Work in progress showing metadata for folders and files not on graph viewer --- .../Details/CollectionDetails.js | 4 +- .../NodeDetailView/Details/FileDetails.js | 2 +- .../NodeDetailView/Details/PersonDetails.js | 1 + .../NodeDetailView/Details/utils.js | 8 ++- .../NodeDetailView/NodeDetailView.js | 22 +++--- src/utils/Splinter.js | 8 +++ src/utils/graphModel.js | 71 ++++++++++++------- src/utils/nodesFactory.js | 6 ++ 8 files changed, 83 insertions(+), 39 deletions(-) diff --git a/src/components/NodeDetailView/Details/CollectionDetails.js b/src/components/NodeDetailView/Details/CollectionDetails.js index 3ddd1c6..45a3794 100644 --- a/src/components/NodeDetailView/Details/CollectionDetails.js +++ b/src/components/NodeDetailView/Details/CollectionDetails.js @@ -16,14 +16,14 @@ const CollectionDetails = (props) => { const collectionPropertiesModel = useSelector(state => state.sdsState.metadata_model.collection); return ( - + {collectionPropertiesModel?.map( property => { if ( property.visible ){ - const propValue = node.graph_node.attributes[property.property]; + const propValue = node?.tree_node?.[property.property] || node?.graph_node?.attributes?.[property.property]; if ( isValidUrl(propValue) ){ return ( {property.label} diff --git a/src/components/NodeDetailView/Details/FileDetails.js b/src/components/NodeDetailView/Details/FileDetails.js index 870c0ea..4bd21df 100644 --- a/src/components/NodeDetailView/Details/FileDetails.js +++ b/src/components/NodeDetailView/Details/FileDetails.js @@ -22,7 +22,7 @@ const FileDetails = (props) => { {filePropertiesModel?.map( property => { if ( property.visible ){ - const propValue = node.graph_node.attributes[property.property]; + const propValue = node?.tree_node?.[property.property] || node?.graph_node?.attributes?.[property.property]; if ( isValidUrl(propValue) ){ return ( {property.label} diff --git a/src/components/NodeDetailView/Details/PersonDetails.js b/src/components/NodeDetailView/Details/PersonDetails.js index de4f1be..0d273b2 100644 --- a/src/components/NodeDetailView/Details/PersonDetails.js +++ b/src/components/NodeDetailView/Details/PersonDetails.js @@ -4,6 +4,7 @@ import { Typography, } from "@material-ui/core"; import Links from './Views/Links'; +import { detailsLabel } from '../../../constants'; const PersonDetails = (props) => { const { node } = props; diff --git a/src/components/NodeDetailView/Details/utils.js b/src/components/NodeDetailView/Details/utils.js index 2939404..3f554c1 100644 --- a/src/components/NodeDetailView/Details/utils.js +++ b/src/components/NodeDetailView/Details/utils.js @@ -20,5 +20,11 @@ export const simpleValue = (label, value) => { } export const isValidUrl = (urlString) => { - return /(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?\/[a-zA-Z0-9]{2,}|((https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?)|(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})?/g.test(urlString); + var urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string + '(\\#[-a-z\\d_]*)?$','i'); // validate fragment locator + return ( typeof urlString === "string" && urlString?.startsWith("http")); } \ No newline at end of file diff --git a/src/components/NodeDetailView/NodeDetailView.js b/src/components/NodeDetailView/NodeDetailView.js index 8af27a4..ff1e3cd 100644 --- a/src/components/NodeDetailView/NodeDetailView.js +++ b/src/components/NodeDetailView/NodeDetailView.js @@ -25,11 +25,11 @@ const NodeDetailView = (props) => { }; var path = [] var latestNodeVisited = nodeSelected; - while ( latestNodeVisited.graph_node.parent !== undefined ) { - path.push(latestNodeVisited.graph_node.parent.id); + while ( latestNodeVisited?.graph_node?.parent !== undefined ) { + path.push(latestNodeVisited?.graph_node?.parent?.id); latestNodeVisited = { tree_node: undefined, - graph_node: latestNodeVisited.graph_node.parent + graph_node: latestNodeVisited?.graph_node?.parent }; }; @@ -40,9 +40,9 @@ const NodeDetailView = (props) => { graph_node: graph_node, tree_node: graph_node.tree_reference } - if (new_node.graph_node.id !== subject_key - && new_node.graph_node.id !== contributors_key - && new_node.graph_node.id !== protocols_key) { + if (new_node?.graph_node?.id !== subject_key + && new_node?.graph_node?.id !== contributors_key + && new_node?.graph_node?.id !== protocols_key) { links.pages.push({ id: singleNode, title: graph_node.name, @@ -52,10 +52,12 @@ const NodeDetailView = (props) => { } return <> ; }); - links.current = { - id: nodeSelected.graph_node.id, - text: nodeSelected.graph_node.name - }; + if ( nodeSelected?.graph_node ){ + links.current = { + id: nodeSelected?.graph_node?.id, + text: nodeSelected?.graph_node?.name + }; + } const toggleContent = () => { dispatch(toggleSettingsPanelVisibility(!showSettingsContent)); }; diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 41dcc19..5d983ae 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -989,6 +989,10 @@ class Splinter { let tree_node = this.tree_map.get(id); if (tree_node) { value.tree_reference = tree_node; + tree_node.publishedURI = + Array.from(this.nodes)[0][1].attributes.hasUriPublished[0]?.replace("datasets", "file") + + "/1?path=files/" + + tree_node?.dataset_relative_path; this.nodes.set(key, value); tree_node.graph_reference = value; this.tree_map.set(value.id, tree_node); @@ -996,6 +1000,10 @@ class Splinter { value.proxies.every(proxy => { tree_node = this.tree_map.get(proxy); if (tree_node) { + tree_node.publishedURI = + Array.from(this.nodes)[0][1].attributes.hasUriPublished[0]?.replace("datasets", "file") + + "/1?path=files/" + + tree_node?.dataset_relative_path; value.tree_reference = tree_node; this.nodes.set(key, value); tree_node.graph_reference = value; diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index 4a5d05d..61dfe03 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -49,16 +49,9 @@ export const rdfTypes = { "properties": [ { "type": "rdfs", - "key": "label", - "property": "label", - "label": "Label", - "visible" : false - }, - { - "type": "rdfs", - "key": "identifier", - "property": "identifier", - "label": "Label", + "key": "basename", + "property": "basename", + "label": "Basename", "visible" : true }, { @@ -66,7 +59,7 @@ export const rdfTypes = { "key": "mimetype", "property": "mimetype", "label": "Mimetype", - "visible" : false + "visible" : true }, { "type": "rdfs", @@ -77,18 +70,25 @@ export const rdfTypes = { }, { "type": "rdfs", - "key": "updated", - "property": "updated", + "key": "timestamp_updated", + "property": "timestamp_updated", "label": "Updated On", "visible" : true }, { "type": "TEMP", - "key": "hasUriHuman", - "property": "hasUriHuman", + "key": "uri_human", + "property": "uri_human", "label": "URI Link", "visible" : false }, + { + "type": "TEMP", + "key": "uri_api", + "property": "uri_api", + "label": "URI API", + "visible" : true + }, { "type": "TEMP", "key": "publishedURI", @@ -396,9 +396,16 @@ export const rdfTypes = { "properties": [ { "type": "rdfs", - "key": "identifier", - "property": "identifier", - "label": "Label", + "key": "basename", + "property": "basename", + "label": "Basename", + "visible" : true + }, + { + "type": "rdfs", + "key": "timestamp_updated", + "property": "timestamp_updated", + "label": "Updated On", "visible" : true }, { @@ -417,18 +424,32 @@ export const rdfTypes = { }, { "type": "TEMP", - "key": "updated", - "property": "updated", - "label": "Updated On", + "key": "publishedURI", + "property": "publishedURI", + "label": "Published URI", "visible" : true }, { "type": "TEMP", - "key": "publishedURI", - "property": "publishedURI", - "label": "Published URI", + "key": "uri_human", + "property": "uri_human", + "label": "URI Link", + "visible" : false + }, + { + "type": "TEMP", + "key": "uri_api", + "property": "uri_api", + "label": "URI API", "visible" : true - } + }, + { + "type": "TEMP", + "key": "status", + "property": "status", + "label": "Status", + "visible" : true + }, ] }, "Subject": { diff --git a/src/utils/nodesFactory.js b/src/utils/nodesFactory.js index 4a037ed..256a43b 100644 --- a/src/utils/nodesFactory.js +++ b/src/utils/nodesFactory.js @@ -151,6 +151,12 @@ const Protocol = function (node, ttlTypes) { const Sample = function (node, ttlTypes) { node.img = createImage(node); extractProperties(node, ttlTypes); + if (node.attributes?.identifier !== undefined) { + node.name = node.attributes?.identifier[0]; + } else { + let namesArray = node.name.split("/"); + node.name = namesArray[namesArray.length - 1]; + } return node; }; From d95816fee0870898602931ce49be7ec7ec12977f Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 12 Mar 2024 19:42:53 -0700 Subject: [PATCH 33/76] #SDSV-24 User tls-secret --- deploy/k8s/ingress_tpl.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/k8s/ingress_tpl.yaml b/deploy/k8s/ingress_tpl.yaml index f903987..b77b898 100755 --- a/deploy/k8s/ingress_tpl.yaml +++ b/deploy/k8s/ingress_tpl.yaml @@ -1,13 +1,13 @@ apiVersion: cert-manager.io/v1alpha2 kind: Issuer metadata: - name: 'letsencrypt-sdsviewer' + name: 'letsencrypt-apinatomy' spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: filippo@metacell.us privateKeySecretRef: - name: letsencrypt-sdsviewer + name: tls-secret solvers: - http01: ingress: @@ -17,7 +17,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - cert-manager.io/issuer: letsencrypt-sdsviewer + cert-manager.io/issuer: letsencrypt-apinatomy kubernetes.io/ingress.class: nginx kubernetes.io/tls-acme: 'true' nginx.ingress.kubernetes.io/ssl-redirect: 'true' From 5819ab5a5035b4faf707481c1097423a11fd4ee1 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 12 Mar 2024 20:06:45 -0700 Subject: [PATCH 34/76] #SDSV-24 Update ingres yaml --- deploy/k8s/ingress_tpl.yaml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/deploy/k8s/ingress_tpl.yaml b/deploy/k8s/ingress_tpl.yaml index b77b898..8299bf0 100755 --- a/deploy/k8s/ingress_tpl.yaml +++ b/deploy/k8s/ingress_tpl.yaml @@ -1,13 +1,13 @@ -apiVersion: cert-manager.io/v1alpha2 +apiVersion: cert-manager.io/v1 kind: Issuer metadata: - name: 'letsencrypt-apinatomy' + name: 'letsencrypt-sds-viewer' spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: filippo@metacell.us privateKeySecretRef: - name: tls-secret + name: letsencrypt-sds-viewer solvers: - http01: ingress: @@ -17,13 +17,11 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: - cert-manager.io/issuer: letsencrypt-apinatomy + cert-manager.io/issuer: letsencrypt-sds-viewer kubernetes.io/ingress.class: nginx kubernetes.io/tls-acme: 'true' - nginx.ingress.kubernetes.io/ssl-redirect: 'true' nginx.ingress.kubernetes.io/proxy-body-size: 512m - nginx.ingress.kubernetes.io/from-to-www-redirect: 'true' - name: sds-viewer + name: sds-viewer-nginx-ingress spec: rules: - host: "{{DOMAIN}}" @@ -31,7 +29,7 @@ spec: paths: - backend: service: - name: sds-viewer + name: nginx port: number: 80 path: / From 63016f6068998d0f87779b332c16b2e81172cce1 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 12 Mar 2024 20:20:28 -0700 Subject: [PATCH 35/76] #SDSV-24 Update name of ingress service --- deploy/k8s/ingress_tpl.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/k8s/ingress_tpl.yaml b/deploy/k8s/ingress_tpl.yaml index 8299bf0..b72f8cf 100755 --- a/deploy/k8s/ingress_tpl.yaml +++ b/deploy/k8s/ingress_tpl.yaml @@ -29,7 +29,7 @@ spec: paths: - backend: service: - name: nginx + name: sds-viewer port: number: 80 path: / From 2e97524884d15f4b993e94c40a85cd34649e5474 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 13 Mar 2024 07:17:01 -0700 Subject: [PATCH 36/76] #SDSV-24 Add environment properties to deploy state on codefresh yaml --- deploy/k8s/codefresh.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deploy/k8s/codefresh.yaml b/deploy/k8s/codefresh.yaml index 444f4b6..af79cb3 100644 --- a/deploy/k8s/codefresh.yaml +++ b/deploy/k8s/codefresh.yaml @@ -32,4 +32,7 @@ steps: - export REGISTRY="${{REGISTRY}}/" - export DOMAIN="${{DOMAIN}}" - chmod +x ./deploy.sh - - ./deploy.sh \ No newline at end of file + - ./deploy.sh + environment: + - KUBECONTEXT=${{CLUSTER_NAME}} + - KUBERNETES_NAMESPACE=${{NAMESPACE}} \ No newline at end of file From c1ee10dbe73d800cc134cd26fb2c5d32666bb3e2 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 13 Mar 2024 07:31:36 -0700 Subject: [PATCH 37/76] #SDSV-24 Update codefresh deployment --- deploy/k8s/codefresh.yaml | 12 ++++-------- deploy/k8s/ingress.yaml | 40 ++++++++++++++++++++++++++++++++++++++ deploy/k8s/sds_viewer.yaml | 39 +++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100755 deploy/k8s/ingress.yaml create mode 100755 deploy/k8s/sds_viewer.yaml diff --git a/deploy/k8s/codefresh.yaml b/deploy/k8s/codefresh.yaml index af79cb3..15c9a10 100644 --- a/deploy/k8s/codefresh.yaml +++ b/deploy/k8s/codefresh.yaml @@ -23,16 +23,12 @@ steps: deploy: stage: "deploy" title: "Deploying SDS Viewer" - image: codefresh/kubectl + image: codefresh/cf-deploy-kubernetes + tag: latest working_directory: ./sds-viewer/deploy/k8s commands: - - export CLUSTER_NAME="${{CLUSTER_NAME}}" - - export NAMESPACE="${{NAMESPACE}}" - - export CF_BUILD_ID="${{CF_BUILD_ID}}" - - export REGISTRY="${{REGISTRY}}/" - - export DOMAIN="${{DOMAIN}}" - - chmod +x ./deploy.sh - - ./deploy.sh + - /cf-deploy-kubernetes cde-mapper.yaml + - /cf-deploy-kubernetes ingress.yaml environment: - KUBECONTEXT=${{CLUSTER_NAME}} - KUBERNETES_NAMESPACE=${{NAMESPACE}} \ No newline at end of file diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/ingress.yaml new file mode 100755 index 0000000..b72f8cf --- /dev/null +++ b/deploy/k8s/ingress.yaml @@ -0,0 +1,40 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: 'letsencrypt-sds-viewer' +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: filippo@metacell.us + privateKeySecretRef: + name: letsencrypt-sds-viewer + solvers: + - http01: + ingress: + class: nginx +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + cert-manager.io/issuer: letsencrypt-sds-viewer + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: 'true' + nginx.ingress.kubernetes.io/proxy-body-size: 512m + name: sds-viewer-nginx-ingress +spec: + rules: + - host: "{{DOMAIN}}" + http: + paths: + - backend: + service: + name: sds-viewer + port: + number: 80 + path: / + pathType: ImplementationSpecific + tls: + - hosts: + - "{{DOMAIN}}" + secretName: sds-viewer-tls diff --git a/deploy/k8s/sds_viewer.yaml b/deploy/k8s/sds_viewer.yaml new file mode 100755 index 0000000..3f0a039 --- /dev/null +++ b/deploy/k8s/sds_viewer.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sds-viewer +spec: + selector: + matchLabels: + app: sds-viewer + replicas: 1 + template: + metadata: + labels: + app: sds-viewer + spec: + containers: + - name: sds-viewer + image: "{{REGISTRY}}sds-viewer:{{CF_BUILD_ID}}" + imagePullPolicy: "IfNotPresent" + ports: + - containerPort: 80 + resources: + limits: + cpu: 1500m + memory: 768Mi + requests: + cpu: 500m + memory: 768Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: sds-viewer +spec: + type: LoadBalancer + ports: + - port: 80 + targetPort: 80 + selector: + app: sds-viewer From 5820a15b517615cd2b1d32023abf7fbd88e3ada1 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 13 Mar 2024 07:39:51 -0700 Subject: [PATCH 38/76] #SDSV-24 Fix typo --- deploy/k8s/codefresh.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/k8s/codefresh.yaml b/deploy/k8s/codefresh.yaml index 15c9a10..356b28f 100644 --- a/deploy/k8s/codefresh.yaml +++ b/deploy/k8s/codefresh.yaml @@ -27,7 +27,7 @@ steps: tag: latest working_directory: ./sds-viewer/deploy/k8s commands: - - /cf-deploy-kubernetes cde-mapper.yaml + - /cf-deploy-kubernetes sds_viewer.yaml - /cf-deploy-kubernetes ingress.yaml environment: - KUBECONTEXT=${{CLUSTER_NAME}} From 22855eb8dc946acdc5068b77a9deea1c1c16f4e9 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 13 Mar 2024 08:26:30 -0700 Subject: [PATCH 39/76] #SDSV-24 Revert previous change --- deploy/k8s/codefresh.yaml | 9 +++++++-- deploy/k8s/ingress.yaml | 40 -------------------------------------- deploy/k8s/sds_viewer.yaml | 39 ------------------------------------- 3 files changed, 7 insertions(+), 81 deletions(-) delete mode 100755 deploy/k8s/ingress.yaml delete mode 100755 deploy/k8s/sds_viewer.yaml diff --git a/deploy/k8s/codefresh.yaml b/deploy/k8s/codefresh.yaml index 356b28f..27aa8eb 100644 --- a/deploy/k8s/codefresh.yaml +++ b/deploy/k8s/codefresh.yaml @@ -27,8 +27,13 @@ steps: tag: latest working_directory: ./sds-viewer/deploy/k8s commands: - - /cf-deploy-kubernetes sds_viewer.yaml - - /cf-deploy-kubernetes ingress.yaml + - export CLUSTER_NAME="${{CLUSTER_NAME}}" + - export NAMESPACE="${{NAMESPACE}}" + - export CF_BUILD_ID="${{CF_BUILD_ID}}" + - export REGISTRY="${{REGISTRY}}/" + - export DOMAIN="${{DOMAIN}}" + - chmod +x ./deploy.sh + - ./deploy.sh environment: - KUBECONTEXT=${{CLUSTER_NAME}} - KUBERNETES_NAMESPACE=${{NAMESPACE}} \ No newline at end of file diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/ingress.yaml deleted file mode 100755 index b72f8cf..0000000 --- a/deploy/k8s/ingress.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: 'letsencrypt-sds-viewer' -spec: - acme: - server: https://acme-v02.api.letsencrypt.org/directory - email: filippo@metacell.us - privateKeySecretRef: - name: letsencrypt-sds-viewer - solvers: - - http01: - ingress: - class: nginx ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: - cert-manager.io/issuer: letsencrypt-sds-viewer - kubernetes.io/ingress.class: nginx - kubernetes.io/tls-acme: 'true' - nginx.ingress.kubernetes.io/proxy-body-size: 512m - name: sds-viewer-nginx-ingress -spec: - rules: - - host: "{{DOMAIN}}" - http: - paths: - - backend: - service: - name: sds-viewer - port: - number: 80 - path: / - pathType: ImplementationSpecific - tls: - - hosts: - - "{{DOMAIN}}" - secretName: sds-viewer-tls diff --git a/deploy/k8s/sds_viewer.yaml b/deploy/k8s/sds_viewer.yaml deleted file mode 100755 index 3f0a039..0000000 --- a/deploy/k8s/sds_viewer.yaml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sds-viewer -spec: - selector: - matchLabels: - app: sds-viewer - replicas: 1 - template: - metadata: - labels: - app: sds-viewer - spec: - containers: - - name: sds-viewer - image: "{{REGISTRY}}sds-viewer:{{CF_BUILD_ID}}" - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 80 - resources: - limits: - cpu: 1500m - memory: 768Mi - requests: - cpu: 500m - memory: 768Mi ---- -apiVersion: v1 -kind: Service -metadata: - name: sds-viewer -spec: - type: LoadBalancer - ports: - - port: 80 - targetPort: 80 - selector: - app: sds-viewer From b711f0670d37812c9db6dbe625b869ef27cf5d56 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 13 Mar 2024 08:54:21 -0700 Subject: [PATCH 40/76] #SDSV-24 Try again cf-deploy-kubernetes deployment --- deploy/k8s/codefresh.yaml | 11 ++------ deploy/k8s/ingress.yaml | 40 ++++++++++++++++++++++++++ deploy/k8s/sds_viewer.yaml | 57 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 deploy/k8s/ingress.yaml create mode 100644 deploy/k8s/sds_viewer.yaml diff --git a/deploy/k8s/codefresh.yaml b/deploy/k8s/codefresh.yaml index 27aa8eb..1b2f2dd 100644 --- a/deploy/k8s/codefresh.yaml +++ b/deploy/k8s/codefresh.yaml @@ -15,7 +15,7 @@ steps: title: "Building SDS Viewer" type: "build" image_name: "sds-viewer" - tag: "${{CF_BUILD_ID}}" + tag: "${{CF_SHORT_REVISION}}" dockerfile: Dockerfile working_directory: ./sds-viewer buildkit: true @@ -27,13 +27,8 @@ steps: tag: latest working_directory: ./sds-viewer/deploy/k8s commands: - - export CLUSTER_NAME="${{CLUSTER_NAME}}" - - export NAMESPACE="${{NAMESPACE}}" - - export CF_BUILD_ID="${{CF_BUILD_ID}}" - - export REGISTRY="${{REGISTRY}}/" - - export DOMAIN="${{DOMAIN}}" - - chmod +x ./deploy.sh - - ./deploy.sh + - /cf-deploy-kubernetes sds_viewer.yaml + - /cf-deploy-kubernetes ingress.yaml environment: - KUBECONTEXT=${{CLUSTER_NAME}} - KUBERNETES_NAMESPACE=${{NAMESPACE}} \ No newline at end of file diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/ingress.yaml new file mode 100644 index 0000000..b72f8cf --- /dev/null +++ b/deploy/k8s/ingress.yaml @@ -0,0 +1,40 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: 'letsencrypt-sds-viewer' +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: filippo@metacell.us + privateKeySecretRef: + name: letsencrypt-sds-viewer + solvers: + - http01: + ingress: + class: nginx +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + cert-manager.io/issuer: letsencrypt-sds-viewer + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: 'true' + nginx.ingress.kubernetes.io/proxy-body-size: 512m + name: sds-viewer-nginx-ingress +spec: + rules: + - host: "{{DOMAIN}}" + http: + paths: + - backend: + service: + name: sds-viewer + port: + number: 80 + path: / + pathType: ImplementationSpecific + tls: + - hosts: + - "{{DOMAIN}}" + secretName: sds-viewer-tls diff --git a/deploy/k8s/sds_viewer.yaml b/deploy/k8s/sds_viewer.yaml new file mode 100644 index 0000000..837cc4d --- /dev/null +++ b/deploy/k8s/sds_viewer.yaml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sds-viewer +spec: + selector: + matchLabels: + app: sds-viewer + replicas: 1 + template: + metadata: + labels: + app: sds-viewer + spec: + containers: + - name: sds-viewer + image: "us.gcr.io/metacellllc/sds-viewer:{{CF_SHORT_REVISION}}" + imagePullPolicy: "IfNotPresent" + ports: + - containerPort: 80 + livenessProbe: + failureThreshold: 3 + httpGet: + path: /index.html + port: 80 + scheme: HTTP + initialDelaySeconds: 45 + periodSeconds: 30 + timeoutSeconds: 2 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /index.html + port: 80 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 30 + timeoutSeconds: 2 + resources: + limits: + cpu: 1500m + memory: 768Mi + requests: + cpu: 500m + memory: 768Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: sds-viewer +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + selector: + app: sds-viewer From e66e3cc339963631aeebbd5a031321b19664754e Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 13 Mar 2024 09:12:53 -0700 Subject: [PATCH 41/76] #sdsv-24 wrong image domain used --- deploy/k8s/sds_viewer.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/k8s/sds_viewer.yaml b/deploy/k8s/sds_viewer.yaml index 837cc4d..7b67d58 100644 --- a/deploy/k8s/sds_viewer.yaml +++ b/deploy/k8s/sds_viewer.yaml @@ -14,7 +14,7 @@ spec: spec: containers: - name: sds-viewer - image: "us.gcr.io/metacellllc/sds-viewer:{{CF_SHORT_REVISION}}" + image: "gcr.io/metacellllc/sds-viewer:{{CF_SHORT_REVISION}}" imagePullPolicy: "IfNotPresent" ports: - containerPort: 80 From 95526222fcbfcf05c9f956de199fe85afa69ff57 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 13 Mar 2024 09:34:40 -0700 Subject: [PATCH 42/76] #SDSV-24 Cleanup --- deploy/k8s/codefresh.yaml | 2 +- deploy/k8s/ingress.yaml | 1 - deploy/k8s/ingress_tpl.yaml | 40 ---------------------------------- deploy/k8s/sds_viewer_tpl.yaml | 39 --------------------------------- 4 files changed, 1 insertion(+), 81 deletions(-) delete mode 100755 deploy/k8s/ingress_tpl.yaml delete mode 100755 deploy/k8s/sds_viewer_tpl.yaml diff --git a/deploy/k8s/codefresh.yaml b/deploy/k8s/codefresh.yaml index 1b2f2dd..50cc838 100644 --- a/deploy/k8s/codefresh.yaml +++ b/deploy/k8s/codefresh.yaml @@ -19,7 +19,7 @@ steps: dockerfile: Dockerfile working_directory: ./sds-viewer buildkit: true - registry: "${{CODEFRESH_REGISTRY}}" + registry: "${{REGISTRY}}" deploy: stage: "deploy" title: "Deploying SDS Viewer" diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/ingress.yaml index b72f8cf..5edc109 100644 --- a/deploy/k8s/ingress.yaml +++ b/deploy/k8s/ingress.yaml @@ -20,7 +20,6 @@ metadata: cert-manager.io/issuer: letsencrypt-sds-viewer kubernetes.io/ingress.class: nginx kubernetes.io/tls-acme: 'true' - nginx.ingress.kubernetes.io/proxy-body-size: 512m name: sds-viewer-nginx-ingress spec: rules: diff --git a/deploy/k8s/ingress_tpl.yaml b/deploy/k8s/ingress_tpl.yaml deleted file mode 100755 index b72f8cf..0000000 --- a/deploy/k8s/ingress_tpl.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: 'letsencrypt-sds-viewer' -spec: - acme: - server: https://acme-v02.api.letsencrypt.org/directory - email: filippo@metacell.us - privateKeySecretRef: - name: letsencrypt-sds-viewer - solvers: - - http01: - ingress: - class: nginx ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: - cert-manager.io/issuer: letsencrypt-sds-viewer - kubernetes.io/ingress.class: nginx - kubernetes.io/tls-acme: 'true' - nginx.ingress.kubernetes.io/proxy-body-size: 512m - name: sds-viewer-nginx-ingress -spec: - rules: - - host: "{{DOMAIN}}" - http: - paths: - - backend: - service: - name: sds-viewer - port: - number: 80 - path: / - pathType: ImplementationSpecific - tls: - - hosts: - - "{{DOMAIN}}" - secretName: sds-viewer-tls diff --git a/deploy/k8s/sds_viewer_tpl.yaml b/deploy/k8s/sds_viewer_tpl.yaml deleted file mode 100755 index b7be103..0000000 --- a/deploy/k8s/sds_viewer_tpl.yaml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sds-viewer -spec: - selector: - matchLabels: - app: sds-viewer - replicas: 1 - template: - metadata: - labels: - app: sds-viewer - spec: - containers: - - name: sds-viewer - image: "{{REGISTRY}}sds-viewer:{{TAG}}" - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 80 - resources: - limits: - cpu: 1500m - memory: 768Mi - requests: - cpu: 500m - memory: 768Mi ---- -apiVersion: v1 -kind: Service -metadata: - name: sds-viewer -spec: - type: LoadBalancer - ports: - - port: 80 - targetPort: 80 - selector: - app: sds-viewer From ef03b2fe0cdda27dd088914dd62891c7d2c2a53d Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 13 Mar 2024 09:45:58 -0700 Subject: [PATCH 43/76] #SDSV-24 Use codefresh_registry variable --- deploy/k8s/codefresh.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/k8s/codefresh.yaml b/deploy/k8s/codefresh.yaml index 50cc838..1b2f2dd 100644 --- a/deploy/k8s/codefresh.yaml +++ b/deploy/k8s/codefresh.yaml @@ -19,7 +19,7 @@ steps: dockerfile: Dockerfile working_directory: ./sds-viewer buildkit: true - registry: "${{REGISTRY}}" + registry: "${{CODEFRESH_REGISTRY}}" deploy: stage: "deploy" title: "Deploying SDS Viewer" From 4a9c9cca76ceb735ba3bbb3b424c3c1db10b7bde Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Wed, 13 Mar 2024 23:32:52 +0100 Subject: [PATCH 44/76] #26 Store Metadata Properties in Local Storage --- .../NodeDetailView/settings/Settings.js | 11 ++----- .../NodeDetailView/settings/SettingsItem.js | 2 +- .../settings/SettingsListItems.js | 18 ++---------- src/components/Sidebar/List.js | 5 +--- src/redux/initialState.js | 29 +++++++++++-------- 5 files changed, 24 insertions(+), 41 deletions(-) diff --git a/src/components/NodeDetailView/settings/Settings.js b/src/components/NodeDetailView/settings/Settings.js index ab1e4bc..dd7ab7e 100644 --- a/src/components/NodeDetailView/settings/Settings.js +++ b/src/components/NodeDetailView/settings/Settings.js @@ -1,14 +1,9 @@ -import { Box, Button, Typography } from "@material-ui/core"; +import React from "react"; +import { Box, Button } from "@material-ui/core"; import SettingsGroup from "./SettingsGroup"; -import FolderIcon from "@material-ui/icons/Folder"; import { useSelector, useDispatch } from 'react-redux' import { toggleSettingsPanelVisibility } from '../../../redux/actions'; -import React, {useEffect, useState} from "react"; -import {DragDropContext, Droppable} from "react-beautiful-dnd"; -import SettingsListItems from "./SettingsListItems"; - - -const Settings = props => { +const Settings = () => { const dispatch = useDispatch(); const showSettingsContent = useSelector(state => state.sdsState.settings_panel_visible); const metaDataPropertiesModel = useSelector(state => state.sdsState.metadata_model); diff --git a/src/components/NodeDetailView/settings/SettingsItem.js b/src/components/NodeDetailView/settings/SettingsItem.js index 3b61d3e..7c35b92 100644 --- a/src/components/NodeDetailView/settings/SettingsItem.js +++ b/src/components/NodeDetailView/settings/SettingsItem.js @@ -1,4 +1,4 @@ -import React, {useEffect} from "react"; +import React from "react"; import { useDispatch } from 'react-redux'; import { toggleMetadataItemVisibility } from '../../../redux/actions'; diff --git a/src/components/NodeDetailView/settings/SettingsListItems.js b/src/components/NodeDetailView/settings/SettingsListItems.js index f3461e3..955c9a6 100644 --- a/src/components/NodeDetailView/settings/SettingsListItems.js +++ b/src/components/NodeDetailView/settings/SettingsListItems.js @@ -1,25 +1,11 @@ -import React, {useEffect, useState} from "react"; +import React from "react"; import { Box, Typography, List, - ListItemText, - ListItem, - ListItemIcon, - ListItemSecondaryAction, - IconButton, ListSubheader, - Button } from "@material-ui/core"; -import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; -import ReorderIcon from "@material-ui/icons/Reorder"; -import RemoveCircleOutlineIcon from "@material-ui/icons/RemoveCircleOutline"; -import AddCircleOutlineIcon from "@material-ui/icons/AddCircleOutline"; -import VisibilityIcon from "@material-ui/icons/Visibility"; -import { TuneRounded } from "@material-ui/icons"; -import FolderIcon from "@material-ui/icons/Folder"; -import VisibilityOffRoundedIcon from "@material-ui/icons/VisibilityOffRounded"; -import { SPARC_DATASETS } from "../../../constants"; +import { Draggable } from "react-beautiful-dnd"; import SettingsItem from "./SettingsItem"; const SettingsListItems = props => { const { provided, items, title } = props; diff --git a/src/components/Sidebar/List.js b/src/components/Sidebar/List.js index 5c18ccf..001ab8e 100644 --- a/src/components/Sidebar/List.js +++ b/src/components/Sidebar/List.js @@ -2,14 +2,11 @@ import React, {useEffect} from 'react'; import {Box, IconButton} from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; import InstancesTreeView from './TreeView/InstancesTreeView'; -import {useDispatch, useSelector} from 'react-redux'; +import {useSelector} from 'react-redux'; import SearchRoundedIcon from '@material-ui/icons/SearchRounded'; -import {selectInstance} from "../../redux/actions"; -import {TREE_SOURCE} from "../../constants"; const SidebarContent = (props) => { const { expand, setExpand, searchTerm } = props; - const dispatch = useDispatch(); const datasets = useSelector((state) => state.sdsState.datasets); const nodeSelected = useSelector((state) => state.sdsState.instance_selected); diff --git a/src/redux/initialState.js b/src/redux/initialState.js index 26ce71b..08bc94e 100644 --- a/src/redux/initialState.js +++ b/src/redux/initialState.js @@ -3,6 +3,14 @@ import * as LayoutActions from '@metacell/geppetto-meta-client/common/layout/act import { rdfTypes } from "../utils/graphModel"; import {TOGGLE_METADATA_ITEM_VISIBILITY, UPDATE_METADATA_ITEMS_ORDER} from "./actions"; +const savedMetadataModel = localStorage.getItem("metadata_model"); +const initialMetadataModel = savedMetadataModel ? JSON.parse(savedMetadataModel) : { + dataset: [...rdfTypes.Dataset.properties], + subject: [...rdfTypes.Subject.properties], + sample: [...rdfTypes.Sample.properties], + group: [...rdfTypes.Group.properties], + file: [...rdfTypes.File.properties] +}; export const sdsInitialState = { "sdsState": { datasets: [], @@ -23,13 +31,7 @@ export const sdsInitialState = { }, layout : {}, settings_panel_visible : false, - metadata_model : { - dataset : [...rdfTypes.Dataset.properties], - subject : [...rdfTypes.Subject.properties], - sample : [...rdfTypes.Sample.properties], - group : [...rdfTypes.Group.properties], - file : [...rdfTypes.File.properties] - } + metadata_model : initialMetadataModel } }; @@ -135,19 +137,22 @@ export default function sdsClientReducer(state = {}, action) { } }); } + localStorage.setItem("metadata_model", JSON.stringify(updatedMetadataModel)); + return { ...state, metadata_model: { ...updatedMetadataModel } }; case UPDATE_METADATA_ITEMS_ORDER: const { title, newItemsOrder } = action.payload; - + const updatedMetadataModelOrder = { + ...state.metadata_model, + [title]: newItemsOrder, + }; + localStorage.setItem("metadata_model", JSON.stringify(updatedMetadataModelOrder)); return { ...state, - metadata_model: { - ...state.metadata_model, - [title]: newItemsOrder, - }, + metadata_model: updatedMetadataModelOrder, }; case LayoutActions.layoutActions.SET_LAYOUT: return { ...state, layout : action.data.layout}; From 922b8d31c6aeca86682f3b7cf3496b6d44ab6498 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Mon, 18 Mar 2024 11:33:18 -0700 Subject: [PATCH 45/76] #SDSV-16 Bring branch up with development --- src/components/GraphViewer/GraphViewer.js | 11 ++++++++-- src/utils/GraphViewerHelper.js | 25 +++++++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index fbc39b8..7c0d0f1 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -47,6 +47,7 @@ const GraphViewer = (props) => { const nodeSelected = useSelector(state => state.sdsState.instance_selected.graph_node); const groupSelected = useSelector(state => state.sdsState.group_selected.graph_node); const [collapsed, setCollapsed] = React.useState(true); + const [previouslySelectedNodes, setPreviouslySelectedNodes] = useState(new Set()); const handleLayoutClick = (event) => { setLayoutAnchorEl(event.currentTarget); @@ -208,7 +209,13 @@ const GraphViewer = (props) => { graphRef?.current?.ggv?.current.centerAt(groupSelected.x, groupSelected.y, ONE_SECOND); graphRef?.current?.ggv?.current.zoom(2, ONE_SECOND); } - },[groupSelected]) + },[groupSelected]) + + useEffect(() => { + if (selectedNode) { + setPreviouslySelectedNodes(prev => new Set([...prev, selectedNode.id])); + } + }, [selectedNode]); useEffect(() => { if ( nodeSelected ) { @@ -290,7 +297,7 @@ const GraphViewer = (props) => { linkCanvasObjectMode={'replace'} onLinkHover={handleLinkHover} // Override drawing of canvas objects, draw an image as a node - nodeCanvasObject={(node, ctx) => paintNode(node, ctx, hoverNode, selectedNode, nodeSelected)} + nodeCanvasObject={(node, ctx) => paintNode(node, ctx, hoverNode, selectedNode, nodeSelected, previouslySelectedNodes)} nodeCanvasObjectMode={node => 'replace'} nodeVal = { node => { if ( selectedLayout.layout === TOP_DOWN.layout ){ diff --git a/src/utils/GraphViewerHelper.js b/src/utils/GraphViewerHelper.js index bb0dd7d..3bb0c87 100644 --- a/src/utils/GraphViewerHelper.js +++ b/src/utils/GraphViewerHelper.js @@ -12,7 +12,9 @@ export const GRAPH_COLORS = { textHoverRect: '#3779E1', textHover: 'white', textColor: '#2E3A59', - collapsedFolder : 'red' + collapsedFolder : 'red', + nodeSeen: '#E1E3E8', + textBGSeen: '#6E4795' }; export const TOP_DOWN = { label : "Tree View", @@ -64,7 +66,7 @@ const roundRect = (ctx, x, y, width, height, radius, color, alpha) => { ctx.fill(); }; -export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected) => { +export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected, previouslySelectedNodes) => { const size = 7.5; const nodeImageSize = [size * 2.4, size * 2.4]; const hoverRectDimensions = [size * 4.2, size * 4.2]; @@ -128,6 +130,25 @@ export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected) => ); // reset canvas fill color ctx.fillStyle = GRAPH_COLORS.textHover; + } else if (previouslySelectedNodes.has(node.id)) { + // Apply different style previously selected nodes + roundRect( + ctx, + ...hoverRectPosition, + ...hoverRectDimensions, + hoverRectBorderRadius, + GRAPH_COLORS.nodeSeen, + 0.3 + ); + roundRect( + ctx, + ...textHoverPosition, + hoverRectDimensions[0], + hoverRectDimensions[1] / 4, + hoverRectBorderRadius, + GRAPH_COLORS.textBGSeen + ); + ctx.fillStyle = GRAPH_COLORS.textHover; } else { ctx.fillStyle = GRAPH_COLORS.textColor; } From 0a81847e35bbedf7687ca131ee6b01f4f7db1ed8 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 19 Mar 2024 13:03:57 -0700 Subject: [PATCH 46/76] #hot_fix - Make Title of dataset clickable --- .../NodeDetailView/Details/DatasetDetails.js | 11 ++++++++++- src/utils/graphModel.js | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index 7720da8..6091c3e 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -28,10 +28,19 @@ const DatasetDetails = (props) => { {datasetPropertiesModel?.map( property => { if ( property.visible ){ const propValue = node.graph_node.attributes[property.property]?.[0]; + + if ( property.link ){ + const value = node.graph_node.attributes[property.link.property]?.[0]; + return ( + {property.label} + + ) + } + if ( isValidUrl(propValue) ){ return ( {property.label} - + ) } diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index 61dfe03..d974155 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -127,7 +127,10 @@ export const rdfTypes = { "key": "title", "property": "title", "label": "Title", - "visible" : true + "visible" : true, + "link" : { + "property" : "hasUriPublished" + } }, { "type": "rdfs", From 0eca013989f4c02086cc857293463238ca0c7f97 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Thu, 21 Mar 2024 01:16:32 +0100 Subject: [PATCH 47/76] #17 Allow Files to be Link to Sparc Portal from Sidebar --- .../Sidebar/TreeView/InstancesTreeView.js | 51 +++++++++---------- .../Sidebar/TreeView/TreeViewItem.js | 7 ++- src/theme.js | 12 ++++- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/src/components/Sidebar/TreeView/InstancesTreeView.js b/src/components/Sidebar/TreeView/InstancesTreeView.js index 902d777..da4f783 100644 --- a/src/components/Sidebar/TreeView/InstancesTreeView.js +++ b/src/components/Sidebar/TreeView/InstancesTreeView.js @@ -20,22 +20,31 @@ const InstancesTreeView = (props) => { const [items, setItems] = useState(datasets); const widgets = useSelector(state => state.widgets); - const onNodeSelect = (e, nodeId) => { + const onNodeSelect = (e, nodeId, isOpenFile = false) => { const node = window.datasets[dataset_id].splinter.tree_map.get(nodeId); - dispatch(selectInstance({ - dataset_id: dataset_id, - graph_node: node?.graph_reference?.id, - tree_node: node?.id, - source: TREE_SOURCE - })); - if (widgets[dataset_id] !== undefined) { - widgets[dataset_id].status = WidgetStatus.ACTIVE; - dispatch(layoutActions.updateWidget(widgets[dataset_id])); - } - if (widgets[dataset_id] !== undefined) { - widgets[dataset_id].status = WidgetStatus.ACTIVE; - dispatch(layoutActions.updateWidget(widgets[dataset_id])); + + if (isOpenFile) { + const publishedURI = node.graph_reference?.attributes?.publishedURI; + if (publishedURI) { + window.open(publishedURI, '_blank'); + } + } else { + dispatch(selectInstance({ + dataset_id: dataset_id, + graph_node: node?.graph_reference?.id, + tree_node: node?.id, + source: TREE_SOURCE + })); + if (widgets[dataset_id] !== undefined) { + widgets[dataset_id].status = WidgetStatus.ACTIVE; + dispatch(layoutActions.updateWidget(widgets[dataset_id])); + } + if (widgets[dataset_id] !== undefined) { + widgets[dataset_id].status = WidgetStatus.ACTIVE; + dispatch(layoutActions.updateWidget(widgets[dataset_id])); + } } + }; const onNodeToggle = (e, nodeIds) => { @@ -127,7 +136,6 @@ const InstancesTreeView = (props) => { { labelIcon: DATASET, iconClass: 'dataset' } : itemLength > 0 ? { labelIcon: FOLDER, iconClass: 'folder' } : { labelIcon: FILE, iconClass: 'file' }; - return ( { }; const treeRef = React.createRef(); - const openSpecificTreeItem = (itemId) => { - const node = window.datasets[dataset_id].splinter.tree_map.get(itemId); - if (node && node.path !== undefined) { - setNodes(node.path); - // Dispatch onNodeSelect action to select the specific tree item - dispatch(selectInstance({ - dataset_id: dataset_id, - graph_node: node?.graph_reference?.id, - tree_node: node?.id, - source: TREE_SOURCE - })); - } - }; return ( <> diff --git a/src/components/Sidebar/TreeView/TreeViewItem.js b/src/components/Sidebar/TreeView/TreeViewItem.js index 591780f..3776f76 100644 --- a/src/components/Sidebar/TreeView/TreeViewItem.js +++ b/src/components/Sidebar/TreeView/TreeViewItem.js @@ -1,8 +1,9 @@ import React from "react"; import PropTypes from "prop-types"; import { TreeItem } from "@material-ui/lab"; -import { Typography, Box } from "@material-ui/core"; +import {Typography, Box, IconButton} from "@material-ui/core"; import DOWN from "../../../images/tree/down.svg"; +import {OpenInNewRounded} from "@material-ui/icons"; const StyledTreeItem = (props) => { const { @@ -30,6 +31,10 @@ const StyledTreeItem = (props) => { variant="body2" className="labelText"> {labelText} + {props.iconClass === 'file' ? { + onNodeSelect(event, props.nodeId, true); + event.preventDefault(); + }}> : null} {labelInfo > 0 ? ( Date: Thu, 21 Mar 2024 15:54:57 -0700 Subject: [PATCH 48/76] #SDSV-17 Fixing errors with links --- src/components/Sidebar/TreeView/InstancesTreeView.js | 2 +- src/components/Sidebar/TreeView/TreeViewItem.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Sidebar/TreeView/InstancesTreeView.js b/src/components/Sidebar/TreeView/InstancesTreeView.js index da4f783..5738f0f 100644 --- a/src/components/Sidebar/TreeView/InstancesTreeView.js +++ b/src/components/Sidebar/TreeView/InstancesTreeView.js @@ -138,7 +138,7 @@ const InstancesTreeView = (props) => { : { labelIcon: FILE, iconClass: 'file' }; return ( { variant="body2" className="labelText"> {labelText} - {props.iconClass === 'file' ? { + {window.datasets[dataset].splinter.tree_map.get(props.nodeId)?.graph_reference?.attributes?.publishedURI != undefined ? { onNodeSelect(event, props.nodeId, true); event.preventDefault(); }}> : null} From 5853eda89620486de66d0c232b4cf902a9d6e228 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Thu, 21 Mar 2024 16:12:32 -0700 Subject: [PATCH 49/76] #SDSV-17 - Show icon for all files/folders with published URI links --- src/components/Sidebar/TreeView/TreeViewItem.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Sidebar/TreeView/TreeViewItem.js b/src/components/Sidebar/TreeView/TreeViewItem.js index 007e665..89c535a 100644 --- a/src/components/Sidebar/TreeView/TreeViewItem.js +++ b/src/components/Sidebar/TreeView/TreeViewItem.js @@ -31,7 +31,8 @@ const StyledTreeItem = (props) => { variant="body2" className="labelText"> {labelText} - {window.datasets[dataset].splinter.tree_map.get(props.nodeId)?.graph_reference?.attributes?.publishedURI != undefined ? { + {window.datasets[dataset].splinter.tree_map.get(props.nodeId)?.graph_reference?.attributes?.publishedURI != undefined ? + { onNodeSelect(event, props.nodeId, true); event.preventDefault(); }}> : null} From 8012b19b896eb3bef1803c64cf3720579d227099 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Thu, 4 Apr 2024 07:18:32 -0700 Subject: [PATCH 50/76] #SDSV-17 - Revert back change to show links to sparc portal from sidebar for folders, only files. --- src/components/GraphViewer/GraphViewer.js | 2 +- src/components/Sidebar/TreeView/TreeViewItem.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 7c0d0f1..373bb7c 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -39,7 +39,7 @@ const GraphViewer = (props) => { const [selectedNode, setSelectedNode] = useState(null); const [highlightNodes, setHighlightNodes] = useState(new Set()); const [highlightLinks, setHighlightLinks] = useState(new Set()); - const [selectedLayout, setSelectedLayout] = React.useState(TOP_DOWN); + const [selectedLayout, setSelectedLayout] = React.useState(LEFT_RIGHT); const [layoutAnchorEl, setLayoutAnchorEl] = React.useState(null); const open = Boolean(layoutAnchorEl); const [loading, setLoading] = React.useState(false); diff --git a/src/components/Sidebar/TreeView/TreeViewItem.js b/src/components/Sidebar/TreeView/TreeViewItem.js index 89c535a..de37ac5 100644 --- a/src/components/Sidebar/TreeView/TreeViewItem.js +++ b/src/components/Sidebar/TreeView/TreeViewItem.js @@ -31,7 +31,7 @@ const StyledTreeItem = (props) => { variant="body2" className="labelText"> {labelText} - {window.datasets[dataset].splinter.tree_map.get(props.nodeId)?.graph_reference?.attributes?.publishedURI != undefined ? + {props.iconClass === 'file' && window.datasets[dataset].splinter.tree_map.get(props.nodeId)?.graph_reference?.attributes?.publishedURI != undefined ? { onNodeSelect(event, props.nodeId, true); event.preventDefault(); From dde6fcffdbfd3be32ba9a57a0f5fd63d3824d268 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Thu, 4 Apr 2024 12:56:38 -0700 Subject: [PATCH 51/76] #hot-fix Switch labels for title and label --- src/utils/graphModel.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index d974155..abbd2fd 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -123,9 +123,9 @@ export const rdfTypes = { "key": "Dataset", "properties": [ { - "type": "dc", - "key": "title", - "property": "title", + "type": "rdfs", + "key": "label", + "property": "label", "label": "Title", "visible" : true, "link" : { @@ -133,11 +133,11 @@ export const rdfTypes = { } }, { - "type": "rdfs", - "key": "label", - "property": "label", + "type": "dc", + "key": "title", + "property": "title", "label": "Label", - "visible" : true + "visible" : false }, { "type": "dc", From 0fb90ff81803f215357d7c751a79c74bddd81e5b Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 12 Apr 2024 06:00:03 -0700 Subject: [PATCH 52/76] #SDSV-28 - Fix links on metadata panel --- .../NodeDetailView/Details/DatasetDetails.js | 36 ++++- .../NodeDetailView/settings/SettingsItem.js | 23 +-- src/utils/Splinter.js | 19 ++- src/utils/graphModel.js | 148 ++++++++++++------ 4 files changed, 161 insertions(+), 65 deletions(-) diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index 6091c3e..c2bfe4a 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -2,7 +2,9 @@ import { Box, Typography, Divider, + IconButton } from "@material-ui/core"; +import { useState, useEffect } from "react"; import Links from './Views/Links'; import SimpleLinkedChip from './Views/SimpleLinkedChip'; import SimpleLabelValue from './Views/SimpleLabelValue'; @@ -10,11 +12,23 @@ import { detailsLabel } from '../../../constants'; import { isValidUrl } from './utils'; import { useSelector } from 'react-redux' import {DatasetIcon} from "../../../images/Icons"; +import FileCopyIcon from '@material-ui/icons/FileCopy'; +import Tooltip from '@material-ui/core/Tooltip'; const DatasetDetails = (props) => { const { node } = props; - const datasetPropertiesModel = useSelector(state => state.sdsState.metadata_model.dataset); + const [copiedDOI, setCopiedDOI] = useState({}); + + useEffect( () => { + let properties = {}; + datasetPropertiesModel?.map( property => { + if ( property.link ){ + properties[property.label] =false; + } + }); + setCopiedDOI(properties) + }, [] ); return ( @@ -33,7 +47,25 @@ const DatasetDetails = (props) => { const value = node.graph_node.attributes[property.link.property]?.[0]; return ( {property.label} - + + + { + navigator.clipboard.writeText(value); + const newClipboardState = { ...copiedDOI, [property.label] : true}; + setCopiedDOI(newClipboardState) + }}> + + + + + ) } diff --git a/src/components/NodeDetailView/settings/SettingsItem.js b/src/components/NodeDetailView/settings/SettingsItem.js index 7c35b92..a693d29 100644 --- a/src/components/NodeDetailView/settings/SettingsItem.js +++ b/src/components/NodeDetailView/settings/SettingsItem.js @@ -7,7 +7,8 @@ import { ListItemText, ListItem, ListItemSecondaryAction, - IconButton + IconButton, + Tooltip } from "@material-ui/core"; import ReorderIcon from "@material-ui/icons/Reorder"; import RemoveCircleOutlineIcon from "@material-ui/icons/RemoveCircleOutline"; @@ -66,15 +67,17 @@ const SettingsItem = props => { onClick={toggleItemDisabled} disableRipple > - {!item.visible ? ( - - ) : ( - - )} + + {!item.visible ? ( + + ) : ( + + )} + diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 5d983ae..0b0b1a0 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -185,7 +185,6 @@ class Splinter { if (this.nodes === undefined || this.edges === undefined) { await this.processDataset(); } - let filteredNodes = this.forced_nodes?.filter( n => n.type !== rdfTypes.UBERON.key && n.type !== rdfTypes.Award.key && !(n.type === rdfTypes.Collection.key && n.children_counter === 0)); let cleanLinks = []; let that = this; @@ -440,7 +439,6 @@ class Splinter { // we might need to display some of its properties, so we merge them. let dataset_node = undefined; let ontology_node = undefined; - // cast each node to the right type, also keep trace of the dataset and ontology nodes. this.nodes.forEach((value, key) => { value.type = this.get_type(value); @@ -696,6 +694,15 @@ class Splinter { let nodesToRemove = []; this.forced_nodes.forEach((node, index, array) => { + if (node.type === rdfTypes.Dataset.key) { + if (node.attributes?.hasProtocol !== undefined) { + let source = this.nodes.get(node.attributes.hasProtocol[0]); + if ( source !== undefined ) { + node.attributes.hasProtocol[0] = source.attributes.hasDoi?.[0]; + } + } + } + if (node.type === rdfTypes.Sample.key) { if (node.attributes.derivedFrom !== undefined) { let source = this.nodes.get(node.attributes.derivedFrom[0]); @@ -710,8 +717,8 @@ class Splinter { } } - if (node.tree_reference?.dataset_relative_path !== undefined) { - node.attributes.publishedURI = + if (node.attributes?.hasFolderAboutIt !== undefined) { + node.attributes.hasFolderAboutIt = [Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + "?datasetDetailsTab=files&path=files/" + node.tree_reference?.dataset_relative_path]; @@ -752,8 +759,8 @@ class Splinter { } } - if (node.tree_reference?.dataset_relative_path !== undefined) { - node.attributes.publishedURI = + if (node.attributes?.hasFolderAboutIt !== undefined) { + node.attributes.hasFolderAboutIt = [Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + "?datasetDetailsTab=files&path=files/" + node.tree_reference?.dataset_relative_path]; diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index abbd2fd..fd3e506 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -74,27 +74,6 @@ export const rdfTypes = { "property": "timestamp_updated", "label": "Updated On", "visible" : true - }, - { - "type": "TEMP", - "key": "uri_human", - "property": "uri_human", - "label": "URI Link", - "visible" : false - }, - { - "type": "TEMP", - "key": "uri_api", - "property": "uri_api", - "label": "URI API", - "visible" : true - }, - { - "type": "TEMP", - "key": "publishedURI", - "property": "publishedURI", - "label": "Published URI", - "visible" : true } ] }, @@ -207,7 +186,10 @@ export const rdfTypes = { "key": "hasDoi", "property": "hasDoi", "label": "DOI", - "visible" : true + "visible" : true, + "link" : { + "property" : "hasUriPublished" + } }, { "type": "TEMP", @@ -234,7 +216,7 @@ export const rdfTypes = { "type": "TEMP", "key": "hasUriHuman", "property": "hasUriHuman", - "label": "URI Human", + "label": "Pensieve Link", "visible" : true }, { @@ -277,7 +259,7 @@ export const rdfTypes = { "key": "hasUriApi", "property": "hasUriApi", "label": "URI API", - "visible" : true + "visible" : false }, { "type": "TEMP", @@ -290,7 +272,7 @@ export const rdfTypes = { "type": "TEMP", "key": "hasUriHuman", "property": "hasUriHuman", - "label": "URI Human", + "label": "Pennsieve Dataset Link", "visible" : true }, { @@ -340,7 +322,7 @@ export const rdfTypes = { "key": "hasPathErrorReport", "property": "hasPathErrorReport", "label": "Path Error Report", - "visible" : true + "visible" : false }, { "type": "TEMP", @@ -425,13 +407,6 @@ export const rdfTypes = { "label": "Size", "visible" : true }, - { - "type": "TEMP", - "key": "publishedURI", - "property": "publishedURI", - "label": "Published URI", - "visible" : true - }, { "type": "TEMP", "key": "uri_human", @@ -444,7 +419,7 @@ export const rdfTypes = { "key": "uri_api", "property": "uri_api", "label": "URI API", - "visible" : true + "visible" : false }, { "type": "TEMP", @@ -581,12 +556,71 @@ export const rdfTypes = { "property": "participantInPerformanceOf", "label": "Participant In Performance Of", "visible" : true + } + ], + "additional_properties": [ + { + "label": "Age unit", + "property": "ageUnit", + "path": [ "TEMP:hasAge", "TEMP:hasUnit", "@id" ], + "trimType": "unit:", + "type": "string" + }, + { + "label": "Age value", + "property": "ageValue", + "path": [ "TEMP:hasAge", "rdf:value" ], + "innerPath": "@value", + "trimType": "", + "type": "digit" + }, + { + "label": "Age base unit", + "property": "ageBaseUnit", + "path": [ "TEMP:hasAge", "TEMP:asBaseUnits", "TEMP:hasUnit", "@id" ], + "trimType": "unit:", + "type": "string" + }, + { + "label": "Age base value", + "property": "ageBaseValue", + "path": [ "TEMP:hasAge", "TEMP:asBaseUnits", "rdf:value" ], + "innerPath": "@value", + "trimType": "", + "type": "digit" + }, + { + "label": "Weight unit", + "property": "weightUnit", + "path": [ "sparc:animalSubjectHasWeight", "TEMP:hasUnit", "@id" ], + "trimType": "unit:", + "type": "string" + }, + { + "label": "Weight value", + "property": "weightValue", + "path": [ "sparc:animalSubjectHasWeight", "rdf:value", "@value" ], + "trimType": "", + "type": "digit" + } + ] + }, + "Performance": { + "image": "./images/graph/folder.svg", + "key": "Performance", + "properties": [ + { + "type": "TEMP", + "key": "localId", + "property": "localId", + "label": "Label", + "visible" : true }, { "type": "TEMP", - "key": "publishedURI", - "property": "publishedURI", - "label": "Published URI", + "key": "participantInPerformanceOf", + "property": "participantInPerformanceOf", + "label": "Participant In Performance Of", "visible" : true } ], @@ -724,13 +758,6 @@ export const rdfTypes = { "property": "participantInPerformanceOf", "label": "Participant in Performance Of", "visible" : true - }, - { - "type": "TEMP", - "key": "publishedURI", - "property": "publishedURI", - "label": "Published URI", - "visible" : true } ] }, @@ -826,7 +853,10 @@ export const rdfTypes = { "key": "hasDoi", "property": "hasDoi", "label": "DOI", - "visible" : true + "visible" : true, + "link" : { + "property" : "hasUriPublished" + } } ] }, @@ -902,6 +932,24 @@ export const rdfTypes = { } ] }, + "NamedIndividual": { + "image": "./images/graph/files/default_file.svg", + "key": "UBERON", + "properties": [ + { + "type": "rdfs", + "key": "label", + "property": "label", + "label": "To be filled" + }, + { + "type": "TEMP", + "key": "hasUriHuman", + "property": "hasUriHuman", + "label": "To be filled" + } + ] + }, "Unknown": { "image": "./images/graph/files/default_file.svg", "key": "Unknown", @@ -938,7 +986,10 @@ export const typesModel = { }, RRID: { "type": "RRID", - } + }, + Protocol: { + "type": "Protocol" + }, }, "Class": { NCBITaxon: { @@ -949,7 +1000,10 @@ export const typesModel = { }, UBERON: { "type": "UBERON", - } + }, + Protocol: { + "type": "Protocol" + }, }, "sparc": { Protocol: { From d4de46539d45877cb0f42e186345df6c0a05fe22 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 12 Apr 2024 06:31:46 -0700 Subject: [PATCH 53/76] #SDSV-28 - Add back sparc link to files --- src/utils/graphModel.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index fd3e506..42d8843 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -428,6 +428,13 @@ export const rdfTypes = { "label": "Status", "visible" : true }, + { + "type": "TEMP", + "key": "publishedURI", + "property": "publishedURI", + "label": "Find in SPARC Portal", + "visible" : true + } ] }, "Subject": { From a160da6166d16862c249d7390a176d2f25ee2800 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Wed, 24 Apr 2024 20:54:51 -0700 Subject: [PATCH 54/76] #SDSV-29 - Fix datasets with samples. Fix links and hide links we don't need. #SDVS-28 --- src/components/GraphViewer/GraphViewer.js | 10 +- src/utils/GraphViewerHelper.js | 114 ++++++++++++++++++---- src/utils/Splinter.js | 80 ++++++++------- src/utils/graphModel.js | 9 +- 4 files changed, 158 insertions(+), 55 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 373bb7c..c9e95c9 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -61,6 +61,9 @@ const GraphViewer = (props) => { handleLayoutClose() setSelectedLayout(target); setForce(); + setTimeout( () => { + resetCamera(); + },100) }; const handleNodeLeftClick = (node, event) => { @@ -110,6 +113,9 @@ const GraphViewer = (props) => { let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); setCollapsed(!collapsed) + setTimeout( () => { + resetCamera(); + },100) } /** @@ -234,8 +240,8 @@ const GraphViewer = (props) => { } setSelectedNode(nodeSelected); handleNodeHover(nodeSelected); - graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, ONE_SECOND); - graphRef?.current?.ggv?.current.zoom(2, ONE_SECOND); + graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, 10); + //graphRef?.current?.ggv?.current.zoom(2, ONE_SECOND); } else { handleNodeHover(nodeSelected); } diff --git a/src/utils/GraphViewerHelper.js b/src/utils/GraphViewerHelper.js index 3bb0c87..3d1fff2 100644 --- a/src/utils/GraphViewerHelper.js +++ b/src/utils/GraphViewerHelper.js @@ -1,5 +1,6 @@ import React, {useCallback} from 'react'; import { rdfTypes } from './graphModel'; +import { current } from '@reduxjs/toolkit'; export const NODE_FONT = '500 5px Inter, sans-serif'; export const ONE_SECOND = 1000; @@ -38,7 +39,7 @@ export const RADIAL_OUT = { } }; -export const nodeSpace = 50; +export const nodeSpace = 60; /** * Create background for Nodes on Graph Viewer. @@ -173,6 +174,88 @@ export const collapseSubLevels = (node, collapsed, children) => { }); } +export const determineNodePosition = (positionsMap, levels, n, targetCoord) => { + let position = positionsMap[n.level]; + let found = false; + let space = 1; + let currentNodeIndex = levels[n.level]?.findIndex(node => node.id === n.id ); + levels[n.level]?.forEach( (n, index) => { + let nodeNeighbors = n?.neighbors?.filter(neighbor => { return neighbor.level > n.level }); + sortArray(nodeNeighbors) + if ( nodeNeighbors?.length > 0 && !found && currentNodeIndex < index && !n.collapsed) { + const middleNode = nodeNeighbors[Math.floor(nodeNeighbors.length/2)]; + position = middleNode?.[targetCoord]; + found = true; + space = index; + + } + }) + + let newPosition = position + ( nodeSpace * space); + if ( found) { + space = -1 * ((space - currentNodeIndex)); + newPosition = position + ( nodeSpace * space); + } + let neighbors = n.parent.neighbors; + sortArray(neighbors) + const parentIndex = neighbors?.findIndex( node => node.id === n.id ); + let nearestParentNeighbor = null; + let nearestParentNeighborIndex = 0; + let leftMatch = false; + let leftMatchIndex = 0; + levels[n.level]?.forEach( ( node, index ) => { + if ( leftMatch && nearestParentNeighbor == null && node.type == "Collection") { + nearestParentNeighbor = node; + nearestParentNeighborIndex = index; + } + + if ( !leftMatch && node.id === n.id && node.type == "Collection") { + leftMatch = true; + leftMatchIndex = index -1; + if ( leftMatchIndex < 0 ) { + leftMatchIndex = 0; + } + } + }) + let nodesInBetween = Math.floor((( neighbors?.length - 1) + ( nearestParentNeighbor?.neighbors?.length - 1)) /2 ); + if ( !isNaN(nodesInBetween) ) { + newPosition = newPosition + ( (levels[n.level].findIndex(node => node.id == n.id) - leftMatchIndex) * nodeSpace) + positionsMap[n.level] = newPosition; + } else { + positionsMap[n.level] = newPosition; + } + + return newPosition +} + +const sortArray = (arrayToSort) => { + arrayToSort?.sort( (a, b) => { + if ( a?.attributes?.relativePath && b?.attributes?.relativePath) { + let aParent = a; + let aPath= ""; + while ( aParent?.type != "Subject" ){ + aParent = aParent.parent; + if ( aParent?.attributes?.relativePath ){ + aPath = aPath + "/" + aParent?.attributes?.relativePath + } + } + let bParent = b; + let bPath = "" + while ( bParent?.type != "Subject" ){ + bParent = bParent.parent; + if ( bParent?.attributes?.relativePath ){ + bPath = bPath + "/" +bParent?.attributes?.relativePath + } + } + aPath = (aParent?.id ) + "/" + aPath + bPath = (bParent?.id ) + "/" + bPath + return aPath.localeCompare(bPath); + } else { + return a?.id.localeCompare(b.id); + } + }); +} + /** * Algorithm used to position nodes in a Tree. Position depends on the layout, * either Tree or Vertical Layout views. @@ -187,11 +270,8 @@ export const algorithm = (levels, layout, furthestLeft) => { levelsMapKeys.forEach( level => { furthestLeft = 0 - (Math.ceil(levels[level].length)/2 * nodeSpace ); positionsMap[level] = furthestLeft + nodeSpace; - levels[level]?.sort( (a, b) => { - if (a?.id < b?.id) return -1; - else return 1; - }); - }); + sortArray(levels[level]); + }); // Start assigning the graph from the bottom up let neighbors = 0; @@ -217,42 +297,42 @@ export const algorithm = (levels, layout, furthestLeft) => { } else if ( layout === LEFT_RIGHT.layout ) { n.yPos = min === max ? min : min + ((max - min) * .5); } - positionsMap[n.level] = n.yPos + nodeSpace; + // positionsMap[n.level] = n.yPos + nodeSpace; if ( notcollapsedInLevel?.length > 0 && collapsedInLevel.length > 0) { updateConflictedNodes(levels[level], n, positionsMap, level, index, layout); } if ( layout === TOP_DOWN.layout ) { - positionsMap[n.level] = n.xPos + nodeSpace; n.fx = n.xPos; n.fy = 50 * n.level; + positionsMap[n.level] = n.xPos + nodeSpace; } else if ( layout === LEFT_RIGHT.layout ) { - positionsMap[n.level] = n.yPos + nodeSpace; n.fy = n.yPos; n.fx = 50 * n.level; + positionsMap[n.level] = n.yPos + nodeSpace; } } else { if ( layout === TOP_DOWN.layout ) { - n.xPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.xPos; + let position = determineNodePosition(positionsMap, levels, n, "xPos"); + n.xPos = position; n.fx = n.xPos; n.fy = 50 * n.level; } else if ( layout === LEFT_RIGHT.layout ) { - n.yPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.yPos; + let position = determineNodePosition(positionsMap, levels, n, "yPos"); + n.yPos = position; n.fy = n.yPos; n.fx = 50 * n.level; } } }else { if ( layout === TOP_DOWN.layout ) { - n.xPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.xPos; + let position = determineNodePosition(positionsMap, levels, n, "xPos"); + n.xPos = position; n.fx = n.xPos; n.fy = 50 * n.level; } else if ( layout === LEFT_RIGHT.layout ) { - n.yPos = positionsMap[n.level] + nodeSpace; - positionsMap[n.level] = n.yPos; + let position = determineNodePosition(positionsMap, levels, n, "yPos"); + n.yPos = position; n.fy = n.yPos; n.fx = 50 * n.level; } diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 0b0b1a0..5558888 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -185,30 +185,32 @@ class Splinter { if (this.nodes === undefined || this.edges === undefined) { await this.processDataset(); } - let filteredNodes = this.forced_nodes?.filter( n => n.type !== rdfTypes.UBERON.key && n.type !== rdfTypes.Award.key && !(n.type === rdfTypes.Collection.key && n.children_counter === 0)); + let filteredNodes = [...new Set(this.forced_nodes?.filter( n => n.type !== rdfTypes.UBERON.key && n.type !== rdfTypes.Sample.key && n.type !== rdfTypes.Award.key && !(n.type === rdfTypes.Collection.key && n.children_counter === 0)))] let cleanLinks = []; let that = this; + + // Count how many subjects each Group has filteredNodes?.forEach( n => { if ( n.type === rdfTypes.Subject.key ) { let keys = Object.keys(that.groups); keys.forEach( key => { if ( n.attributes ) { if ( n?.attributes[key] ) { - that.groups[key][n.attributes[key][0]].subjects += 1; + let groupKeys = Object.keys(that.groups[key]); + groupKeys.forEach( groupKey => { + if ( n?.attributes[key][0] === groupKey ) { + const groupNodes = filteredNodes?.filter(n => n.name == groupKey); + groupNodes?.forEach( groupNode => { + if ( n?.id?.includes(groupNode.id) ) { + groupNode.subjects += 1; + } + }) + } + }) } } }) } - if ( n.type === rdfTypes.Sample.key ) { - let keys = Object.keys(that.groups); - keys.forEach( key => { - if ( n.attributes ){ - if ( n?.attributes[key] ) { - that.groups[key][n.attributes[key][0]].samples += 1; - } - } - }) - } }) // Assign neighbors, to highlight links @@ -218,7 +220,7 @@ class Splinter { if ( !existingLing ) { const a = this.nodes.get( link.source ); const b = this.nodes.get( link.target ); - if ( a && b && ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ) && !((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1))) { + if ( a && b && ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ) && ( a?.type !== rdfTypes.Sample.key && b?.type !== rdfTypes.Sample.key ) && !((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1))) { !a.neighbors && (a.neighbors = []); !b.neighbors && (b.neighbors = []); if ( !a.neighbors.find( n => n.id === b.id )){ @@ -243,6 +245,7 @@ class Splinter { } } }); + console.log("This levels map ", this.levelsMap) return { nodes: filteredNodes, links: cleanLinks, @@ -666,10 +669,10 @@ class Splinter { target_node.parent = protocols; this.nodes.set(target_node.id, target_node); } else if (link.source === id && target_node.type === rdfTypes.Sample.key) { - link.source = target_node.attributes.derivedFrom[0]; - target_node.level = subjects.level + 2; - target_node.parent = this.nodes.get(target_node.attributes.derivedFrom[0]); - this.nodes.set(target_node.id, target_node); + // link.source = target_node.attributes.derivedFrom[0]; + // target_node.level = subjects.level + 2; + // target_node.parent = this.nodes.get(target_node.attributes.derivedFrom[0]); + // this.nodes.set(target_node.id, target_node); } let source_node = this.nodes.get(link.source); if ( source_node?.childLinks ) { @@ -774,9 +777,9 @@ class Splinter { if (node.attributes?.relativePath !== undefined) { node.attributes.publishedURI = - Array.from(this.nodes)[0][1].attributes.hasUriPublished[0]?.replace("datasets", "file") + - "/1?path=files/" + - node.attributes?.relativePath; + Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + + "?datasetDetailsTab=files&path=files/" + + node.attributes?.relativePath.substr(0, node.attributes?.relativePath.lastIndexOf("/")); } } @@ -793,7 +796,7 @@ class Splinter { } } - if (node.type === rdfTypes.RRID.key || node.type === rdfTypes.NCBITaxon?.key || node.type === rdfTypes.PATO?.key) { + if (node.type === rdfTypes.RRID.key || node.type === rdfTypes.Sample.key || node.type === rdfTypes.NCBITaxon?.key || node.type === rdfTypes.PATO?.key) { nodesToRemove.unshift(index); } @@ -913,9 +916,9 @@ class Splinter { buildFolder(item) { let copiedItem = {...item}; - let newName = copiedItem.dataset_relative_path.split('/')[0]; + const splitName = copiedItem.dataset_relative_path.split('/'); + let newName = splitName[0]; copiedItem.parent_id = copiedItem.remote_id; - copiedItem.remote_id = copiedItem.basename + '_' + newName; copiedItem.uri_api = copiedItem.remote_id; copiedItem.basename = newName; // copiedItem.basename = copiedItem.remote_id; @@ -924,20 +927,26 @@ class Splinter { linkToNode(node, parent) { - let level = parent.level; - if (parent.type === rdfTypes.Sample.key) { + let level = parent?.level; + if (parent?.type === rdfTypes.Sample.key) { if (parent.attributes.derivedFrom !== undefined) { level = this.nodes.get(parent.attributes.derivedFrom[0])?.level + 1; } } + if ( parent ) { parent.children_counter++; const new_node = this.buildNodeFromJson(node, level); new_node.parent = parent; - new_node.id = parent.id + new_node.id; - node.remote_id = new_node.id; + + // if ( new_node?.type === "File" || new_node?.type === "Collection" ) { + // new_node.id = parent?.id + new_node?.attributes.identifier; + // console.log("new_node.id " , new_node) + // node.remote_id = new_node?.id; + + // } this.forced_edges.push({ - source: parent.id, - target: new_node.id + source: parent?.id, + target: new_node?.id }); new_node.childLinks = []; new_node.collapsed = new_node.type === typesModel.NamedIndividual.subject.type @@ -948,6 +957,7 @@ class Splinter { !this.filterNode(child) && this.linkToNode(child, new_node); }); } + } } @@ -997,9 +1007,9 @@ class Splinter { if (tree_node) { value.tree_reference = tree_node; tree_node.publishedURI = - Array.from(this.nodes)[0][1].attributes.hasUriPublished[0]?.replace("datasets", "file") + - "/1?path=files/" + - tree_node?.dataset_relative_path; + Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + + "?datasetDetailsTab=files&path=files/" + + tree_node?.dataset_relative_path.substr(0, tree_node?.dataset_relative_path.lastIndexOf("/")); this.nodes.set(key, value); tree_node.graph_reference = value; this.tree_map.set(value.id, tree_node); @@ -1008,9 +1018,9 @@ class Splinter { tree_node = this.tree_map.get(proxy); if (tree_node) { tree_node.publishedURI = - Array.from(this.nodes)[0][1].attributes.hasUriPublished[0]?.replace("datasets", "file") + - "/1?path=files/" + - tree_node?.dataset_relative_path; + Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + + "?datasetDetailsTab=files&path=files/" + + tree_node?.dataset_relative_path.substr(0, tree_node?.dataset_relative_path.lastIndexOf("/")); value.tree_reference = tree_node; this.nodes.set(key, value); tree_node.graph_reference = value; diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index 42d8843..8149702 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -74,6 +74,13 @@ export const rdfTypes = { "property": "timestamp_updated", "label": "Updated On", "visible" : true + }, + { + "type": "TEMP", + "key": "publishedURI", + "property": "publishedURI", + "label": "Find in SPARC Portal", + "visible" : true } ] }, @@ -555,7 +562,7 @@ export const rdfTypes = { "key": "hasDerivedInformationAsParticipant", "property": "hasDerivedInformationAsParticipant", "label": "Derived Information as Participant", - "visible" : true + "visible" : false }, { "type": "TEMP", From 1692b5e897e837776356560fd3f276e183d0dbfb Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 26 Apr 2024 11:35:10 -0700 Subject: [PATCH 55/76] #SDSV - 29 : Fix issues with collapsing nodes --- src/utils/GraphViewerHelper.js | 130 ++++++++++++++++++++------------- 1 file changed, 80 insertions(+), 50 deletions(-) diff --git a/src/utils/GraphViewerHelper.js b/src/utils/GraphViewerHelper.js index 3d1fff2..86a09f0 100644 --- a/src/utils/GraphViewerHelper.js +++ b/src/utils/GraphViewerHelper.js @@ -175,57 +175,85 @@ export const collapseSubLevels = (node, collapsed, children) => { } export const determineNodePosition = (positionsMap, levels, n, targetCoord) => { - let position = positionsMap[n.level]; - let found = false; - let space = 1; - let currentNodeIndex = levels[n.level]?.findIndex(node => node.id === n.id ); - levels[n.level]?.forEach( (n, index) => { - let nodeNeighbors = n?.neighbors?.filter(neighbor => { return neighbor.level > n.level }); - sortArray(nodeNeighbors) - if ( nodeNeighbors?.length > 0 && !found && currentNodeIndex < index && !n.collapsed) { - const middleNode = nodeNeighbors[Math.floor(nodeNeighbors.length/2)]; - position = middleNode?.[targetCoord]; - found = true; - space = index; - + let nearestParentNeighbor = null; + let nearestParentNeighborIndex = 0; + let leftParentMatch = false; + let firstParentCollection = null; + let firstParentCollectionIndex = 0; + sortArray(levels[n.level - 1]) + levels[n.level-1]?.forEach( ( node, index ) => { + if ( !leftParentMatch && node.type == "Collection" && node.id !== n.parent.id && !node.collapsed ) { + firstParentCollection = node; + firstParentCollectionIndex = index; + } + + if ( !leftParentMatch && node.id === n.parent.id) { + leftParentMatch = true; + nearestParentNeighbor = node; + nearestParentNeighborIndex = index; } }) - let newPosition = position + ( nodeSpace * space); - if ( found) { - space = -1 * ((space - currentNodeIndex)); - newPosition = position + ( nodeSpace * space); + let nodesInBetween = Math.floor((( firstParentCollection?.neighbors?.length - 1) + ( nearestParentNeighbor?.neighbors?.length - 1)) /2 ) - 1; + if ( firstParentCollection === nearestParentNeighbor ) { + nodesInBetween = Math.floor((( nearestParentNeighbor?.neighbors?.length - 1)) /2 ); + } else if ( firstParentCollection == null ) { + nodesInBetween = Math.floor((( nearestParentNeighbor?.neighbors?.length - 1)) /2 ); } - let neighbors = n.parent.neighbors; - sortArray(neighbors) - const parentIndex = neighbors?.findIndex( node => node.id === n.id ); - let nearestParentNeighbor = null; - let nearestParentNeighborIndex = 0; - let leftMatch = false; - let leftMatchIndex = 0; - levels[n.level]?.forEach( ( node, index ) => { - if ( leftMatch && nearestParentNeighbor == null && node.type == "Collection") { - nearestParentNeighbor = node; - nearestParentNeighborIndex = index; - } + + let spacesNeeded = nearestParentNeighborIndex - firstParentCollectionIndex; - if ( !leftMatch && node.id === n.id && node.type == "Collection") { - leftMatch = true; - leftMatchIndex = index -1; - if ( leftMatchIndex < 0 ) { - leftMatchIndex = 0; - } - } - }) - let nodesInBetween = Math.floor((( neighbors?.length - 1) + ( nearestParentNeighbor?.neighbors?.length - 1)) /2 ); - if ( !isNaN(nodesInBetween) ) { - newPosition = newPosition + ( (levels[n.level].findIndex(node => node.id == n.id) - leftMatchIndex) * nodeSpace) - positionsMap[n.level] = newPosition; - } else { - positionsMap[n.level] = newPosition; - } + let nearestNeighbor = null; + let nearestNeighborIndex = 0; + let leftMatch = false; + let leftMatchIndex = 0; + let firstCollection = null; + levels[n.level]?.forEach( ( node, index ) => { + if ( leftMatch && nearestNeighbor == null && node.type == "Collection" && !node.collapsed) { + nearestNeighbor = node; + nearestNeighborIndex = index; + } - return newPosition + if ( !leftMatch ) { + firstCollection = n; + } + + if ( !leftMatch && node.id === n.id ) { + leftMatch = true; + leftMatchIndex = index - 1; + } + }) + + let position = positionsMap[n.level]; + if ( !isNaN(nodesInBetween) && spacesNeeded - 1 > nodesInBetween) { + let neighbors = n?.parent?.neighbors; + let matchNeighbor = neighbors.findIndex( (node) => node.id === n.id ) ;; + if ( matchNeighbor === 1 && n.level === Object.keys(levels)?.length ) { + position = position + (Math.abs( spacesNeeded - nodesInBetween) * nodeSpace) + } else { + position = position + nodeSpace + } + } else if ( nearestNeighborIndex > 0 ) { + if ( nearestNeighbor != null && !n.collapsed ){ + let middleNode = nearestNeighbor.neighbors?.[Math.floor(nearestNeighbor.neighbors?.length / 2)]?.[targetCoord]; + if ( middleNode ) { + position = middleNode + } + position = position - ( (nearestNeighborIndex - leftMatchIndex - 1) * nodeSpace) + } else if ( n.collapsed ) { + position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] + nodeSpace : position + nodeSpace + } else if ( nearestNeighborIndex - leftMatchIndex > 0 ) { + position = position - ( nodeSpace) + } else { + position = (position + nodeSpace) + } + } else if ( n.collapsed) { + position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] + nodeSpace : position + nodeSpace + } else { + position = position + nodeSpace + } + positionsMap[n.level] = position; + return position } const sortArray = (arrayToSort) => { @@ -297,9 +325,10 @@ export const algorithm = (levels, layout, furthestLeft) => { } else if ( layout === LEFT_RIGHT.layout ) { n.yPos = min === max ? min : min + ((max - min) * .5); } - // positionsMap[n.level] = n.yPos + nodeSpace; if ( notcollapsedInLevel?.length > 0 && collapsedInLevel.length > 0) { - updateConflictedNodes(levels[level], n, positionsMap, level, index, layout); + if ( n.type === "Subject" || n.parent?.type === "Subject" ) { + updateConflictedNodes(levels[level], n, positionsMap, level, index, layout); + } } if ( layout === TOP_DOWN.layout ) { @@ -419,11 +448,11 @@ export const getPrunedTree = (graph_id, layout) => { matchIndex = nodes.findIndex( n => n.id === conflict.id ); if ( layout === TOP_DOWN.layout ) { let furthestLeft = conflict?.xPos; - if ( nodes[i].collapsed ) { - furthestLeft = conflict.xPos - ((((matchIndex - i )/2)) * nodeSpace ); + if ( nodes?.[i]?.collapsed ) { + furthestLeft = conflict.xPos - ((((matchIndex - i )/2)) * nodeSpace ); nodes[i].xPos =furthestLeft; + positionsMap[level] = furthestLeft + nodeSpace; } - positionsMap[level] = furthestLeft + nodeSpace; nodes[i].fx = nodes[i].xPos; nodes[i].fy = 50 * nodes[i].level; } else if ( layout === LEFT_RIGHT.layout ) { @@ -434,6 +463,7 @@ export const getPrunedTree = (graph_id, layout) => { } positionsMap[level] = furthestLeft + nodeSpace; nodes[i].fy = nodes[i].yPos; + positionsMap[level] = nodes[i].fy + nodeSpace; nodes[i].fx = 50 * nodes[i].level; } } From 7fce44e2b08c6911c529bc04a773aaec93fc40ec Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 26 Apr 2024 11:45:24 -0700 Subject: [PATCH 56/76] #SDSV-29 - cleanup --- src/components/GraphViewer/GraphViewer.js | 1 - src/utils/Splinter.js | 6 ------ 2 files changed, 7 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index c9e95c9..a347bf9 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -241,7 +241,6 @@ const GraphViewer = (props) => { setSelectedNode(nodeSelected); handleNodeHover(nodeSelected); graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, 10); - //graphRef?.current?.ggv?.current.zoom(2, ONE_SECOND); } else { handleNodeHover(nodeSelected); } diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 5558888..7204e2e 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -938,12 +938,6 @@ class Splinter { const new_node = this.buildNodeFromJson(node, level); new_node.parent = parent; - // if ( new_node?.type === "File" || new_node?.type === "Collection" ) { - // new_node.id = parent?.id + new_node?.attributes.identifier; - // console.log("new_node.id " , new_node) - // node.remote_id = new_node?.id; - - // } this.forced_edges.push({ source: parent?.id, target: new_node?.id From 856ffa0c5e4cb9dc5bac0207eb9aa1136bce2143 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Mon, 29 Apr 2024 13:52:02 -0700 Subject: [PATCH 57/76] #SDSV-29 Fix dataset parsing for Samples --- src/utils/GraphViewerHelper.js | 18 ++--- src/utils/Splinter.js | 130 +++++++++++++++++---------------- 2 files changed, 77 insertions(+), 71 deletions(-) diff --git a/src/utils/GraphViewerHelper.js b/src/utils/GraphViewerHelper.js index 86a09f0..f7109d6 100644 --- a/src/utils/GraphViewerHelper.js +++ b/src/utils/GraphViewerHelper.js @@ -182,12 +182,12 @@ export const determineNodePosition = (positionsMap, levels, n, targetCoord) => { let firstParentCollectionIndex = 0; sortArray(levels[n.level - 1]) levels[n.level-1]?.forEach( ( node, index ) => { - if ( !leftParentMatch && node.type == "Collection" && node.id !== n.parent.id && !node.collapsed ) { + if ( !leftParentMatch && (node.type == "Collection" || node.type == "Sample") && node.id !== n.parent.id && !node.collapsed ) { firstParentCollection = node; firstParentCollectionIndex = index; } - if ( !leftParentMatch && node.id === n.parent.id) { + if ( !leftParentMatch && node.id === n.parent?.id) { leftParentMatch = true; nearestParentNeighbor = node; nearestParentNeighborIndex = index; @@ -209,7 +209,7 @@ export const determineNodePosition = (positionsMap, levels, n, targetCoord) => { let leftMatchIndex = 0; let firstCollection = null; levels[n.level]?.forEach( ( node, index ) => { - if ( leftMatch && nearestNeighbor == null && node.type == "Collection" && !node.collapsed) { + if ( leftMatch && nearestNeighbor == null && ( node.type == "Collection" || node.type == "Sample" )&& !node.collapsed) { nearestNeighbor = node; nearestNeighborIndex = index; } @@ -241,14 +241,14 @@ export const determineNodePosition = (positionsMap, levels, n, targetCoord) => { } position = position - ( (nearestNeighborIndex - leftMatchIndex - 1) * nodeSpace) } else if ( n.collapsed ) { - position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] + nodeSpace : position + nodeSpace + position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] - ( -1 * nodeSpace) : position - ( -1 * nodeSpace) } else if ( nearestNeighborIndex - leftMatchIndex > 0 ) { position = position - ( nodeSpace) } else { position = (position + nodeSpace) } } else if ( n.collapsed) { - position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] + nodeSpace : position + nodeSpace + position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] - ( -1 * nodeSpace): position - ( -1 * nodeSpace) } else { position = position + nodeSpace } @@ -262,15 +262,15 @@ const sortArray = (arrayToSort) => { let aParent = a; let aPath= ""; while ( aParent?.type != "Subject" ){ - aParent = aParent.parent; + aParent = aParent?.parent; if ( aParent?.attributes?.relativePath ){ aPath = aPath + "/" + aParent?.attributes?.relativePath } } let bParent = b; let bPath = "" - while ( bParent?.type != "Subject" ){ - bParent = bParent.parent; + while ( bParent?.type != "Subject"){ + bParent = bParent?.parent; if ( bParent?.attributes?.relativePath ){ bPath = bPath + "/" +bParent?.attributes?.relativePath } @@ -326,7 +326,7 @@ export const algorithm = (levels, layout, furthestLeft) => { n.yPos = min === max ? min : min + ((max - min) * .5); } if ( notcollapsedInLevel?.length > 0 && collapsedInLevel.length > 0) { - if ( n.type === "Subject" || n.parent?.type === "Subject" ) { + if ( n.type === "Subject" || n.parent?.type === "Subject" || n.parent?.parent?.type === "Subject" ) { updateConflictedNodes(levels[level], n, positionsMap, level, index, layout); } } diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 7204e2e..f68ca03 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -185,7 +185,18 @@ class Splinter { if (this.nodes === undefined || this.edges === undefined) { await this.processDataset(); } - let filteredNodes = [...new Set(this.forced_nodes?.filter( n => n.type !== rdfTypes.UBERON.key && n.type !== rdfTypes.Sample.key && n.type !== rdfTypes.Award.key && !(n.type === rdfTypes.Collection.key && n.children_counter === 0)))] + let filteredNodes = [...new Set(this.forced_nodes?.filter( n => n.type !== rdfTypes.UBERON.key && n.type !== rdfTypes.Award.key && !(n.type === rdfTypes.Collection.key && n.children_counter === 0)))] + filteredNodes = filteredNodes.filter( node => { + if ( node.type === rdfTypes.Sample.key ) { + if ( node.attributes.hasFolderAboutIt !== undefined ){ + return true; + } else { + return true; + } + } else { + return true; + } + }) let cleanLinks = []; let that = this; @@ -220,7 +231,7 @@ class Splinter { if ( !existingLing ) { const a = this.nodes.get( link.source ); const b = this.nodes.get( link.target ); - if ( a && b && ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ) && ( a?.type !== rdfTypes.Sample.key && b?.type !== rdfTypes.Sample.key ) && !((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1))) { + if ( a && b && ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ) &&!((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1))) { !a.neighbors && (a.neighbors = []); !b.neighbors && (b.neighbors = []); if ( !a.neighbors.find( n => n.id === b.id )){ @@ -705,28 +716,6 @@ class Splinter { } } } - - if (node.type === rdfTypes.Sample.key) { - if (node.attributes.derivedFrom !== undefined) { - let source = this.nodes.get(node.attributes.derivedFrom[0]); - if ( source !== undefined ) { - source.children_counter++ - //this.nodes.set(node.attributes.derivedFrom[0], source); - array[index].level = source.level + 1; - this.forced_edges.push({ - source: node.attributes.derivedFrom[0], - target: node.id - }); - } - } - - if (node.attributes?.hasFolderAboutIt !== undefined) { - node.attributes.hasFolderAboutIt = - [Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + - "?datasetDetailsTab=files&path=files/" + - node.tree_reference?.dataset_relative_path]; - } - } if (node.type === rdfTypes.Subject.key) { if (node.attributes?.animalSubjectIsOfStrain !== undefined) { @@ -796,7 +785,7 @@ class Splinter { } } - if (node.type === rdfTypes.RRID.key || node.type === rdfTypes.Sample.key || node.type === rdfTypes.NCBITaxon?.key || node.type === rdfTypes.PATO?.key) { + if (node.type === rdfTypes.RRID.key || node.type === rdfTypes.NCBITaxon?.key || node.type === rdfTypes.PATO?.key) { nodesToRemove.unshift(index); } @@ -874,54 +863,69 @@ class Splinter { } - mergeData() { + mergeData() { this.nodes.forEach((value, key) => { if (value.attributes !== undefined && value.attributes.hasFolderAboutIt !== undefined) { value.attributes.hasFolderAboutIt.forEach(folder => { let jsonNode = this.tree_map.get(folder); - let newNode = this.buildFolder(jsonNode, value); - let folderChildren = this.tree_parents_map2.get(newNode.parent_id)?.map(child => { - child.parent_id = newNode.uri_api - return child; - }); + const splitName = jsonNode.dataset_relative_path.split('/'); + let newName = jsonNode.basename; + if ( value.type == "Subject" && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { + newName = splitName[0] + } + let parentNode = value; + if ( value.type == "Sample" && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { + newName = splitName[0] + "/" + newName + parentNode = this.nodes.get(value.attributes.derivedFrom[0]) + if (parentNode?.attributes !== undefined && parentNode?.attributes?.hasFolderAboutIt !== undefined) { + parentNode?.attributes?.hasFolderAboutIt.forEach(folder => { + let jNode = this.tree_map.get(folder); + this.tree_parents_map2.delete(jNode.remote_id); + }) + } - if (!this.filterNode(newNode) && (this.nodes.get(newNode.remote_id)) === undefined) { - this.linkToNode(newNode, value); } - if (this.tree_parents_map2.get(newNode.uri_api) === undefined) { - this.tree_parents_map2.set(newNode.uri_api, folderChildren); - this.tree_parents_map2.delete(newNode.parent_id); - folderChildren?.forEach(child => { - const child_node = this.nodes.get(this.proxies_map.get(child.uri_api)); - if (!this.filterNode(child) && child_node?.type !== rdfTypes.Sample.key) { - this.linkToNode(child, this.nodes.get(newNode.remote_id)); - } - }); - } else { - let tempChildren = folderChildren === undefined ? [...this.tree_parents_map2.get(newNode.uri_api)] : [...this.tree_parents_map2.get(newNode.uri_api), ...folderChildren];; - this.tree_parents_map2.set(newNode.uri_api, tempChildren); - this.tree_parents_map2.delete(newNode.parent_id); - tempChildren?.forEach(child => { - const child_node = this.nodes.get(this.proxies_map.get(child.uri_api)); - if (!this.filterNode(child) && child_node?.type !== rdfTypes.Sample.key) { - this.linkToNode(child, this.nodes.get(newNode.remote_id)); - } + let newNode = this.buildFolder(jsonNode, newName, parentNode); + + let folderChildren = this.tree_parents_map2.get(newNode.parent_id)?.map(child => { + child.parent_id = newNode.uri_api + return child; }); - } + + if (!this.filterNode(newNode) && (this.nodes.get(newNode.remote_id)) === undefined) { + this.linkToNode(newNode, parentNode); + } + + if (this.tree_parents_map2.get(newNode.uri_api) === undefined) { + this.tree_parents_map2.set(newNode.uri_api, folderChildren); + this.tree_parents_map2.delete(newNode.parent_id); + folderChildren?.forEach(child => { + if (!this.filterNode(child) ) { + this.linkToNode(child, this.nodes.get(newNode.remote_id)); + } + }); + } else { + let tempChildren = folderChildren === undefined ? [...this.tree_parents_map2.get(newNode.uri_api)] : [...this.tree_parents_map2.get(newNode.uri_api), ...folderChildren];; + this.tree_parents_map2.set(newNode.uri_api, tempChildren); + this.tree_parents_map2.delete(newNode.parent_id); + tempChildren?.forEach(child => { + if (!this.filterNode(child) ) { + this.linkToNode(child, this.nodes.get(newNode.remote_id)); + } + }); + } + //} }) } }); } - buildFolder(item) { + buildFolder(item, newName) { let copiedItem = {...item}; - const splitName = copiedItem.dataset_relative_path.split('/'); - let newName = splitName[0]; copiedItem.parent_id = copiedItem.remote_id; copiedItem.uri_api = copiedItem.remote_id; copiedItem.basename = newName; - // copiedItem.basename = copiedItem.remote_id; return copiedItem; } @@ -944,12 +948,14 @@ class Splinter { }); new_node.childLinks = []; new_node.collapsed = new_node.type === typesModel.NamedIndividual.subject.type - this.nodes.set(new_node.id, this.factory.createNode(new_node)); - var children = this.tree_parents_map2.get(node.remote_id); - if (children?.length > 0) { - children.forEach(child => { - !this.filterNode(child) && this.linkToNode(child, new_node); - }); + if ( !this.nodes.get(new_node.id) ) { + this.nodes.set(new_node.id, this.factory.createNode(new_node)); + var children = this.tree_parents_map2.get(node.remote_id); + if (children?.length > 0) { + children.forEach(child => { + !this.filterNode(child) && this.linkToNode(child, new_node); + }); + } } } } From ea37e6713d2509ba686f20331c134cd00f05b59b Mon Sep 17 00:00:00 2001 From: jrmartin Date: Mon, 29 Apr 2024 14:18:27 -0700 Subject: [PATCH 58/76] #SDSV-29 - Add names of Folders to Metadata --- src/utils/graphModel.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index 8149702..10c3249 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -54,6 +54,13 @@ export const rdfTypes = { "label": "Basename", "visible" : true }, + { + "type": "rdfs", + "key": "name", + "property": "name", + "label": "Name", + "visible" : true + }, { "type": "rdfs", "key": "mimetype", From 6cf983815d39817ceef8e774162367c226164007 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Mon, 29 Apr 2024 14:24:56 -0700 Subject: [PATCH 59/76] #SDSV-29 - Show label for Folders --- src/utils/graphModel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index 10c3249..b25d42e 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -49,9 +49,9 @@ export const rdfTypes = { "properties": [ { "type": "rdfs", - "key": "basename", - "property": "basename", - "label": "Basename", + "key": "relativePath", + "property": "relativePath", + "label": "Name", "visible" : true }, { From aa2f139a01689bcf83379c29b2c16cb2dde40e6d Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 10 May 2024 08:10:45 -0700 Subject: [PATCH 60/76] #SDSV-30 - Collapse nodes only one level down. --- src/components/GraphViewer/GraphViewer.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index a347bf9..d86336e 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -70,7 +70,11 @@ const GraphViewer = (props) => { if ( node.type === rdfTypes.Subject.key || node.type === rdfTypes.Sample.key || node.type === rdfTypes.Collection.key ) { node.collapsed = !node.collapsed; collapseSubLevels(node, node.collapsed, { links : 0 }); - const updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + setData(updatedData); + + collapseSubLevels(node, true, { links : 0 }); + updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); } handleNodeHover(node); @@ -115,7 +119,7 @@ const GraphViewer = (props) => { setCollapsed(!collapsed) setTimeout( () => { resetCamera(); - },100) + },200) } /** @@ -240,9 +244,10 @@ const GraphViewer = (props) => { } setSelectedNode(nodeSelected); handleNodeHover(nodeSelected); - graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, 10); + //handleNodeRightClick(nodeSelected) } else { handleNodeHover(nodeSelected); + //handleNodeRightClick(nodeSelected) } } },[nodeSelected]) From d2b616145730d3694e2b8e7f50e016f674165b04 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 10 May 2024 08:12:38 -0700 Subject: [PATCH 61/76] #SDSV-31 - Use local storage to store published datasets --- src/App.js | 33 +++++++++++++++++-- .../DatasetsListSplinter.js | 2 +- src/config/app.json | 3 +- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/App.js b/src/App.js index 4af5cd2..25548d7 100644 --- a/src/App.js +++ b/src/App.js @@ -132,6 +132,7 @@ const App = () => { const splinter = new DatasetsListSplinter(undefined, file.data); let graph = await splinter.getGraph(); let datasets = graph.nodes.filter((node) => node?.attributes?.hasDoi); + let version = graph.nodes.find( node => node?.attributes?.versionInfo)?.attributes?.versionInfo const match = datasets.find( node => node.attributes?.hasDoi?.[0]?.includes(doi)); if ( match ) { const datasetID = match.name; @@ -140,6 +141,20 @@ const App = () => { setLoading(false); setInitialised(false); } + + let datasetStorage = {}; + if ( version !== undefined && localStorage.getItem(config.datasetsStorage)?.version !== version[0] ) { + let parsedDatasets = [] + datasets.forEach( node => { + parsedDatasets.push({name : node.name , doi : node.attributes?.hasDoi?.[0]}); + }); + datasetStorage = { + version : version[0], + datasets : parsedDatasets + } + + localStorage.setItem(config.datasetsStorage, JSON.stringify(datasetStorage)); + } }; useEffect(() => { @@ -149,9 +164,21 @@ const App = () => { if (doi && doi !== "" ) { if ( doiMatch ){ - const fileHandler = new FileHandler(); - const summaryURL = config.repository_url + config.available_datasets; - fileHandler.get_remote_file(summaryURL, loadDatsetFromDOI); + if ( localStorage.getItem(config.datasetsStorage) ) { + let storedDatasetsInfo = JSON.parse(localStorage.getItem(config.datasetsStorage)); + const match = storedDatasetsInfo.datasets.find( node => node?.doi.includes(doi)); + if ( match ) { + const datasetID = match.name; + loadFiles(datasetID); + } else { + setLoading(false); + setInitialised(false); + } + } else { + const fileHandler = new FileHandler(); + const summaryURL = config.repository_url + config.available_datasets; + fileHandler.get_remote_file(summaryURL, loadDatsetFromDOI); + } } } }, []); diff --git a/src/components/DatasetsListViewer/DatasetsListSplinter.js b/src/components/DatasetsListViewer/DatasetsListSplinter.js index 553ae3d..c3284f0 100644 --- a/src/components/DatasetsListViewer/DatasetsListSplinter.js +++ b/src/components/DatasetsListViewer/DatasetsListSplinter.js @@ -254,7 +254,7 @@ class Splinter { dataset_node.proxies = dataset_node.proxies.concat(ontology_node.proxies); dataset_node.level = 1; this.nodes.set(dataset_node.id, dataset_node); - this.nodes.delete(ontology_node.id); + // this.nodes.delete(ontology_node.id); // fix links that were pointing to the ontology let temp_edges = this.edges.map(link => { if (link.source === ontology_node.id) { diff --git a/src/config/app.json b/src/config/app.json index 761251d..13eab97 100644 --- a/src/config/app.json +++ b/src/config/app.json @@ -17,5 +17,6 @@ "datasetsButtonText" : "Import a new dataset", "datasetsDialogSearchText" : "Search datasets by label or id", "datasetsButtonSubtitleText" : "Select a dataset to load" - } + }, + "datasetsStorage" : "publishedDatasets" } \ No newline at end of file From 098b4ce95f645f014e8fc4cfba0dab5ad55d4a03 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 10 May 2024 08:54:23 -0700 Subject: [PATCH 62/76] #SDSV-31 - Store dataset ids on local storage --- src/App.js | 2 +- .../DatasetsListViewer/DatasetsListDialog.js | 32 ++++++++++++++++--- src/utils/GraphViewerHelper.js | 2 +- src/utils/graphModel.js | 12 +++++-- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/App.js b/src/App.js index 25548d7..f5ed2a3 100644 --- a/src/App.js +++ b/src/App.js @@ -146,7 +146,7 @@ const App = () => { if ( version !== undefined && localStorage.getItem(config.datasetsStorage)?.version !== version[0] ) { let parsedDatasets = [] datasets.forEach( node => { - parsedDatasets.push({name : node.name , doi : node.attributes?.hasDoi?.[0]}); + parsedDatasets.push({ name : node.name , doi : node.attributes?.hasDoi?.[0], label : node.attributes ? node.attributes?.label?.[0]?.toLowerCase() : null}); }); datasetStorage = { version : version[0], diff --git a/src/components/DatasetsListViewer/DatasetsListDialog.js b/src/components/DatasetsListViewer/DatasetsListDialog.js index b63ef04..d27bd07 100644 --- a/src/components/DatasetsListViewer/DatasetsListDialog.js +++ b/src/components/DatasetsListViewer/DatasetsListDialog.js @@ -106,8 +106,24 @@ const DatasetsListDialog = (props) => { let datasets = graph.nodes.filter((node) => node?.attributes?.hasUriApi); datasets.forEach( node => node.attributes ? node.attributes.lowerCaseLabel = node.attributes?.label?.[0]?.toLowerCase() : null ); datasets = datasets.filter( node => node?.attributes?.statusOnPlatform?.[0]?.includes(PUBLISHED) ); - dispatch(setDatasetsList(datasets)); - setFilteredDatasets(datasets); + + + let version = graph.nodes.find( node => node?.attributes?.versionInfo)?.attributes?.versionInfo + let datasetStorage = {}; + if ( version !== undefined && localStorage.getItem(config.datasetsStorage)?.version !== version[0] ) { + let parsedDatasets = [] + datasets.forEach( node => { + parsedDatasets.push({ name : node.name , doi : node.attributes?.hasDoi?.[0], label : node.attributes ? node.attributes.lowerCaseLabel : null}); + }); + datasetStorage = { + version : version[0], + datasets : parsedDatasets + } + + localStorage.setItem(config.datasetsStorage, JSON.stringify(datasetStorage)); + dispatch(setDatasetsList(datasetStorage.datasets)); + setFilteredDatasets(datasetStorage.datasets); + } }; const summaryURL = config.repository_url + config.available_datasets; fileHandler.get_remote_file(summaryURL, callback); @@ -139,7 +155,15 @@ const DatasetsListDialog = (props) => { } useEffect(() => { - open && datasets.length === 0 && loadDatasets(); + if ( open && datasets.length === 0 ) { + if ( localStorage.getItem(config.datasetsStorage) ) { + let storedDatasetsInfo = JSON.parse(localStorage.getItem(config.datasetsStorage)); + dispatch(setDatasetsList(storedDatasetsInfo.datasets)); + setFilteredDatasets(storedDatasetsInfo.datasets); + } else { + loadDatasets(); + } + } }); return ( @@ -202,7 +226,7 @@ const DatasetsListDialog = (props) => { className="dataset_list_text" dangerouslySetInnerHTML={{ __html: - getFormattedListTex(dataset.attributes?.label[0]) + getFormattedListTex(dataset.label) }} /> } diff --git a/src/utils/GraphViewerHelper.js b/src/utils/GraphViewerHelper.js index f7109d6..4b1913b 100644 --- a/src/utils/GraphViewerHelper.js +++ b/src/utils/GraphViewerHelper.js @@ -437,7 +437,7 @@ export const getPrunedTree = (graph_id, layout) => { */ const updateConflictedNodes = (nodes, conflictNode, positionsMap, level, index, layout) => { let matchIndex = index; - for ( let i = 0; i < index ; i++ ) { + for ( let i = 0; i <= index ; i++ ) { let conflict = nodes.find ( n => !n.collapsed && n?.parent?.id === nodes[i]?.parent?.id) if ( conflict === undefined ){ conflict = nodes.find ( n => !n.collapsed ) diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index b25d42e..ad81d7b 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -22,6 +22,12 @@ export const rdfTypes = { "key": "hasUriHuman", "property": "hasUriHuman", "label": "To be filled" + }, + { + "type" : "owl", + "key" : "versionInfo", + "property" : "versionInfo", + "label" : "Version" } ] }, @@ -122,7 +128,8 @@ export const rdfTypes = { "label": "Title", "visible" : true, "link" : { - "property" : "hasUriPublished" + "property" : "hasUriPublished", + "asText" : true } }, { @@ -202,7 +209,8 @@ export const rdfTypes = { "label": "DOI", "visible" : true, "link" : { - "property" : "hasUriPublished" + "property" : "hasUriPublished", + "asText" : true } }, { From ee3937f208c27042dfe891d152302fdd56b7acdb Mon Sep 17 00:00:00 2001 From: jrmartin Date: Thu, 16 May 2024 00:05:59 -0700 Subject: [PATCH 63/76] #SDSV-30 - Dataset search fix --- src/components/DatasetsListViewer/DatasetsListDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DatasetsListViewer/DatasetsListDialog.js b/src/components/DatasetsListViewer/DatasetsListDialog.js index d27bd07..7cc464e 100644 --- a/src/components/DatasetsListViewer/DatasetsListDialog.js +++ b/src/components/DatasetsListViewer/DatasetsListDialog.js @@ -132,7 +132,7 @@ const DatasetsListDialog = (props) => { const handleChange = (event) => { const lowerCaseSearch = event.target.value.toLowerCase(); let filtered = datasets.filter((dataset) => - dataset.attributes.lowerCaseLabel.includes(lowerCaseSearch) || dataset.name.includes(lowerCaseSearch) + dataset.label?.includes(lowerCaseSearch) || dataset.name?.includes(lowerCaseSearch) ); setSearchField(lowerCaseSearch); setFilteredDatasets(filtered); From 1aa8a12d57402e3d167a653ec3563713f29c3a36 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 24 May 2024 04:35:52 -0700 Subject: [PATCH 64/76] #SDSV-29 - Fix issues with datasets showing Samples --- src/components/GraphViewer/GraphViewer.js | 7 ++ src/utils/Splinter.js | 104 ++++++++++++---------- src/utils/graphModel.js | 6 +- 3 files changed, 69 insertions(+), 48 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index d86336e..2c88198 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -48,6 +48,7 @@ const GraphViewer = (props) => { const groupSelected = useSelector(state => state.sdsState.group_selected.graph_node); const [collapsed, setCollapsed] = React.useState(true); const [previouslySelectedNodes, setPreviouslySelectedNodes] = useState(new Set()); + let triggerCenter = false; const handleLayoutClick = (event) => { setLayoutAnchorEl(event.currentTarget); @@ -172,6 +173,10 @@ const GraphViewer = (props) => { const onEngineStop = () => { setForce(); + if ( triggerCenter ) { + graphRef?.current?.ggv?.current.centerAt(selectedNode.x, selectedNode.y, ONE_SECOND); + triggerCenter = false; + } } useEffect(() => { @@ -244,9 +249,11 @@ const GraphViewer = (props) => { } setSelectedNode(nodeSelected); handleNodeHover(nodeSelected); + triggerCenter = true; //handleNodeRightClick(nodeSelected) } else { handleNodeHover(nodeSelected); + triggerCenter = true; //handleNodeRightClick(nodeSelected) } } diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index f68ca03..9c400c5 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -231,7 +231,7 @@ class Splinter { if ( !existingLing ) { const a = this.nodes.get( link.source ); const b = this.nodes.get( link.target ); - if ( a && b && ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ) &&!((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1))) { + if ( a && b && ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ) && !((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1)) && !((a?.type === rdfTypes.Sample.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Sample.key && b.children_counter < 1)) ) { !a.neighbors && (a.neighbors = []); !b.neighbors && (b.neighbors = []); if ( !a.neighbors.find( n => n.id === b.id )){ @@ -679,11 +679,11 @@ class Splinter { target_node.level = protocols.level + 1; target_node.parent = protocols; this.nodes.set(target_node.id, target_node); - } else if (link.source === id && target_node.type === rdfTypes.Sample.key) { - // link.source = target_node.attributes.derivedFrom[0]; - // target_node.level = subjects.level + 2; - // target_node.parent = this.nodes.get(target_node.attributes.derivedFrom[0]); - // this.nodes.set(target_node.id, target_node); + } else if (link.source === id && target_node.type === rdfTypes.Sample.key ) { + link.source = target_node.attributes.derivedFrom[0]; + target_node.level = subjects.level + 2; + target_node.parent = this.nodes.get(target_node.attributes.derivedFrom[0]); + this.nodes.set(target_node.id, target_node); } let source_node = this.nodes.get(link.source); if ( source_node?.childLinks ) { @@ -716,6 +716,27 @@ class Splinter { } } } + + if (node.type === rdfTypes.Sample.key) { + if (node.attributes.derivedFrom !== undefined) { + let source = this.nodes.get(node.attributes.derivedFrom[0]); + if ( source !== undefined ) { + source.children_counter++ + array[index].level = source.level + 1; + this.forced_edges.push({ + source: node.attributes.derivedFrom[0], + target: node.id + }); + } + } + + if (node.attributes?.hasFolderAboutIt !== undefined) { + node.attributes.hasFolderAboutIt = + [Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + + "?datasetDetailsTab=files&path=files/" + + node.tree_reference?.dataset_relative_path]; + } + } if (node.type === rdfTypes.Subject.key) { if (node.attributes?.animalSubjectIsOfStrain !== undefined) { @@ -863,7 +884,7 @@ class Splinter { } - mergeData() { + mergeData() { this.nodes.forEach((value, key) => { if (value.attributes !== undefined && value.attributes.hasFolderAboutIt !== undefined) { value.attributes.hasFolderAboutIt.forEach(folder => { @@ -874,48 +895,41 @@ class Splinter { newName = splitName[0] } let parentNode = value; - if ( value.type == "Sample" && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { - newName = splitName[0] + "/" + newName - parentNode = this.nodes.get(value.attributes.derivedFrom[0]) - if (parentNode?.attributes !== undefined && parentNode?.attributes?.hasFolderAboutIt !== undefined) { - parentNode?.attributes?.hasFolderAboutIt.forEach(folder => { - let jNode = this.tree_map.get(folder); - this.tree_parents_map2.delete(jNode.remote_id); - }) - } + let newNode = this.buildFolder(jsonNode, newName, parentNode); + if ( value.type === rdfTypes.Sample.key) { + newNode.remote_id = jsonNode.basename + '_' + newName; + newNode.uri_api = newNode.remote_id } - let newNode = this.buildFolder(jsonNode, newName, parentNode); - let folderChildren = this.tree_parents_map2.get(newNode.parent_id)?.map(child => { - child.parent_id = newNode.uri_api - return child; - }); + let folderChildren = this.tree_parents_map2.get(newNode.parent_id)?.map(child => { + child.parent_id = newNode.uri_api + return child; + }); - if (!this.filterNode(newNode) && (this.nodes.get(newNode.remote_id)) === undefined) { - this.linkToNode(newNode, parentNode); - } + if (!this.filterNode(newNode) && (this.nodes.get(newNode.remote_id)) === undefined) { + this.linkToNode(newNode, parentNode); + } - if (this.tree_parents_map2.get(newNode.uri_api) === undefined) { - this.tree_parents_map2.set(newNode.uri_api, folderChildren); - this.tree_parents_map2.delete(newNode.parent_id); - folderChildren?.forEach(child => { - if (!this.filterNode(child) ) { - this.linkToNode(child, this.nodes.get(newNode.remote_id)); - } - }); - } else { - let tempChildren = folderChildren === undefined ? [...this.tree_parents_map2.get(newNode.uri_api)] : [...this.tree_parents_map2.get(newNode.uri_api), ...folderChildren];; - this.tree_parents_map2.set(newNode.uri_api, tempChildren); - this.tree_parents_map2.delete(newNode.parent_id); - tempChildren?.forEach(child => { - if (!this.filterNode(child) ) { - this.linkToNode(child, this.nodes.get(newNode.remote_id)); - } - }); - } - //} + if (this.tree_parents_map2.get(newNode.uri_api) === undefined) { + this.tree_parents_map2.set(newNode.uri_api, folderChildren); + this.tree_parents_map2.delete(newNode.parent_id); + folderChildren?.forEach(child => { + if (!this.filterNode(child) ) { + this.linkToNode(child, this.nodes.get(newNode.remote_id)); + } + }); + } else { + let tempChildren = folderChildren === undefined ? [...this.tree_parents_map2.get(newNode.uri_api)] : [...this.tree_parents_map2.get(newNode.uri_api), ...folderChildren];; + this.tree_parents_map2.set(newNode.uri_api, tempChildren); + this.tree_parents_map2.delete(newNode.parent_id); + tempChildren?.forEach(child => { + if (!this.filterNode(child) ) { + this.linkToNode(child, this.nodes.get(newNode.remote_id)); + } + }); + } }) } }); @@ -941,7 +955,7 @@ class Splinter { parent.children_counter++; const new_node = this.buildNodeFromJson(node, level); new_node.parent = parent; - + new_node.id = parent.id + new_node.id; this.forced_edges.push({ source: parent?.id, target: new_node?.id @@ -1091,4 +1105,4 @@ class Splinter { } } -export default Splinter; +export default Splinter; \ No newline at end of file diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index ad81d7b..7a1d2b8 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -722,7 +722,7 @@ export const rdfTypes = { "type": "TEMP", "key": "hasFolderAboutIt", "property": "hasFolderAboutIt", - "label": "Related Folder", + "label": "Find in SPARC Portal", "visible" : true }, { @@ -730,7 +730,7 @@ export const rdfTypes = { "key": "wasDerivedFromSubject", "property": "derivedFrom", "label": "Derived from Subject", - "visible" : true + "visible" : false }, { "type": "TEMP", @@ -751,7 +751,7 @@ export const rdfTypes = { "key": "hasDerivedInformationAsParticipant", "property": "hasDerivedInformationAsParticipant", "label": "Derived Information as Participant", - "visible" : true + "visible" : false }, { "type": "TEMP", From 10b8d45558446e4a081b5325d81ddbff1240eec7 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Mon, 27 May 2024 21:00:09 -0700 Subject: [PATCH 65/76] Updgrade layout positions using d3 cluster. --- package.json | 1 + src/App.js | 1 - src/components/GraphViewer/GraphViewer.js | 28 +- .../NodeDetailView/Details/DatasetDetails.js | 4 +- .../Sidebar/TreeView/InstancesTreeView.js | 2 +- src/redux/initialState.js | 3 - src/utils/GraphViewerHelper.js | 319 +++++------------- src/utils/Splinter.js | 26 +- 8 files changed, 121 insertions(+), 263 deletions(-) diff --git a/package.json b/package.json index c6376f4..9061a36 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@types/react": "^17.0.5", "@types/react-dom": "^17.0.3", "axios": "^0.21.1", + "d3": "^7.1.1", "fs": "^0.0.1-security", "gh-pages": "^3.2.3", "jest": "26.6.0", diff --git a/src/App.js b/src/App.js index f5ed2a3..724c959 100644 --- a/src/App.js +++ b/src/App.js @@ -18,7 +18,6 @@ import { NodeViewWidget } from './app/widgets'; import { addWidget } from '@metacell/geppetto-meta-client/common/layout/actions'; import { WidgetStatus } from "@metacell/geppetto-meta-client/common/layout/model"; import DatasetsListSplinter from "./components/DatasetsListViewer/DatasetsListSplinter"; - import config from './config/app.json'; const App = () => { diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 2c88198..171b468 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -45,6 +45,7 @@ const GraphViewer = (props) => { const [loading, setLoading] = React.useState(false); const [data, setData] = React.useState({ nodes : [], links : []}); const nodeSelected = useSelector(state => state.sdsState.instance_selected.graph_node); + const nodeClickSource = useSelector(state => state.sdsState.instance_selected.source); const groupSelected = useSelector(state => state.sdsState.group_selected.graph_node); const [collapsed, setCollapsed] = React.useState(true); const [previouslySelectedNodes, setPreviouslySelectedNodes] = useState(new Set()); @@ -236,25 +237,28 @@ const GraphViewer = (props) => { if ( nodeSelected ) { if ( nodeSelected?.id !== selectedNode?.id ){ let node = nodeSelected; - let collapsed = nodeSelected.collapsed - while ( node?.parent && !collapsed ) { - node = node.parent; - collapsed = node.collapsed + let collapsed = node.collapsed + let parent = node.parent; + while ( parent && !parent?.collapsed) { + parent = parent.parent; } - if ( collapsed ) { - node.collapsed = !node.collapsed; - collapseSubLevels(node, node.collapsed, { links : 0 }); - const updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + + if ( nodeSelected.collapsed && nodeClickSource === "TREE") { + collapseSubLevels(parent, false, { links : 0 }); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + setData(updatedData); + + node.collapsed = true; + collapseSubLevels(nodeSelected, true, { links : 0 }); + updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); } setSelectedNode(nodeSelected); handleNodeHover(nodeSelected); - triggerCenter = true; - //handleNodeRightClick(nodeSelected) + graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, ONE_SECOND); } else { handleNodeHover(nodeSelected); - triggerCenter = true; - //handleNodeRightClick(nodeSelected) + graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, ONE_SECOND); } } },[nodeSelected]) diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index c2bfe4a..5e9d0a0 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -64,7 +64,9 @@ const DatasetDetails = (props) => { - + + { property.link?.asText ? {value} : } + ) } diff --git a/src/components/Sidebar/TreeView/InstancesTreeView.js b/src/components/Sidebar/TreeView/InstancesTreeView.js index 5738f0f..65da03d 100644 --- a/src/components/Sidebar/TreeView/InstancesTreeView.js +++ b/src/components/Sidebar/TreeView/InstancesTreeView.js @@ -31,7 +31,7 @@ const InstancesTreeView = (props) => { } else { dispatch(selectInstance({ dataset_id: dataset_id, - graph_node: node?.graph_reference?.id, + graph_node: node?.graph_reference?.id || node?.id, tree_node: node?.id, source: TREE_SOURCE })); diff --git a/src/redux/initialState.js b/src/redux/initialState.js index ce740f1..ccf8d90 100644 --- a/src/redux/initialState.js +++ b/src/redux/initialState.js @@ -132,9 +132,6 @@ export default function sdsClientReducer(state = {}, action) { if (a.visible === b.visible) { // Preserve the original order for items with the same visibility return updatedMetadataModel[groupTitle].indexOf(a) - updatedMetadataModel[groupTitle].indexOf(b); - } else { - // Move visible items to the top - return a.visible ? -1 : 1; } }); } diff --git a/src/utils/GraphViewerHelper.js b/src/utils/GraphViewerHelper.js index 4b1913b..5dad9cf 100644 --- a/src/utils/GraphViewerHelper.js +++ b/src/utils/GraphViewerHelper.js @@ -1,6 +1,7 @@ import React, {useCallback} from 'react'; import { rdfTypes } from './graphModel'; import { current } from '@reduxjs/toolkit'; +import * as d3 from 'd3'; export const NODE_FONT = '500 5px Inter, sans-serif'; export const ONE_SECOND = 1000; @@ -156,7 +157,7 @@ export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected, prev ctx.fillText(...textProps); if ( node.childLinks?.length && node.collapsed ) { let children = { links : 0 }; - collapseSubLevels(node, undefined, children) + collapseSubLevels(node, true, children) const collapsedNodes = [children.links, node.x, textHoverPosition[1]]; ctx.fillStyle = GRAPH_COLORS.collapsedFolder; ctx.textAlign = 'center'; @@ -168,207 +169,28 @@ export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected, prev export const collapseSubLevels = (node, collapsed, children) => { node?.childLinks?.forEach( n => { - if ( collapsed !== undefined ) n.target.collapsed = collapsed; - collapseSubLevels(n.target, collapsed, children); - children.links = children.links + 1; + if ( collapsed !== undefined ) { + n.target.collapsed = collapsed; + collapseSubLevels(n.target, collapsed, children); + children.links = children.links + 1; + } }); } -export const determineNodePosition = (positionsMap, levels, n, targetCoord) => { - let nearestParentNeighbor = null; - let nearestParentNeighborIndex = 0; - let leftParentMatch = false; - let firstParentCollection = null; - let firstParentCollectionIndex = 0; - sortArray(levels[n.level - 1]) - levels[n.level-1]?.forEach( ( node, index ) => { - if ( !leftParentMatch && (node.type == "Collection" || node.type == "Sample") && node.id !== n.parent.id && !node.collapsed ) { - firstParentCollection = node; - firstParentCollectionIndex = index; - } - - if ( !leftParentMatch && node.id === n.parent?.id) { - leftParentMatch = true; - nearestParentNeighbor = node; - nearestParentNeighborIndex = index; - } - }) - - let nodesInBetween = Math.floor((( firstParentCollection?.neighbors?.length - 1) + ( nearestParentNeighbor?.neighbors?.length - 1)) /2 ) - 1; - if ( firstParentCollection === nearestParentNeighbor ) { - nodesInBetween = Math.floor((( nearestParentNeighbor?.neighbors?.length - 1)) /2 ); - } else if ( firstParentCollection == null ) { - nodesInBetween = Math.floor((( nearestParentNeighbor?.neighbors?.length - 1)) /2 ); - } - - let spacesNeeded = nearestParentNeighborIndex - firstParentCollectionIndex; - - let nearestNeighbor = null; - let nearestNeighborIndex = 0; - let leftMatch = false; - let leftMatchIndex = 0; - let firstCollection = null; - levels[n.level]?.forEach( ( node, index ) => { - if ( leftMatch && nearestNeighbor == null && ( node.type == "Collection" || node.type == "Sample" )&& !node.collapsed) { - nearestNeighbor = node; - nearestNeighborIndex = index; - } - - if ( !leftMatch ) { - firstCollection = n; - } - - if ( !leftMatch && node.id === n.id ) { - leftMatch = true; - leftMatchIndex = index - 1; - } - }) - - let position = positionsMap[n.level]; - if ( !isNaN(nodesInBetween) && spacesNeeded - 1 > nodesInBetween) { - let neighbors = n?.parent?.neighbors; - let matchNeighbor = neighbors.findIndex( (node) => node.id === n.id ) ;; - if ( matchNeighbor === 1 && n.level === Object.keys(levels)?.length ) { - position = position + (Math.abs( spacesNeeded - nodesInBetween) * nodeSpace) - } else { - position = position + nodeSpace - } - } else if ( nearestNeighborIndex > 0 ) { - if ( nearestNeighbor != null && !n.collapsed ){ - let middleNode = nearestNeighbor.neighbors?.[Math.floor(nearestNeighbor.neighbors?.length / 2)]?.[targetCoord]; - if ( middleNode ) { - position = middleNode - } - position = position - ( (nearestNeighborIndex - leftMatchIndex - 1) * nodeSpace) - } else if ( n.collapsed ) { - position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] - ( -1 * nodeSpace) : position - ( -1 * nodeSpace) - } else if ( nearestNeighborIndex - leftMatchIndex > 0 ) { - position = position - ( nodeSpace) - } else { - position = (position + nodeSpace) - } - } else if ( n.collapsed) { - position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] - ( -1 * nodeSpace): position - ( -1 * nodeSpace) - } else { - position = position + nodeSpace - } - positionsMap[n.level] = position; - return position -} -const sortArray = (arrayToSort) => { - arrayToSort?.sort( (a, b) => { - if ( a?.attributes?.relativePath && b?.attributes?.relativePath) { - let aParent = a; - let aPath= ""; - while ( aParent?.type != "Subject" ){ - aParent = aParent?.parent; - if ( aParent?.attributes?.relativePath ){ - aPath = aPath + "/" + aParent?.attributes?.relativePath - } - } - let bParent = b; - let bPath = "" - while ( bParent?.type != "Subject"){ - bParent = bParent?.parent; - if ( bParent?.attributes?.relativePath ){ - bPath = bPath + "/" +bParent?.attributes?.relativePath - } - } - aPath = (aParent?.id ) + "/" + aPath - bPath = (bParent?.id ) + "/" + bPath - return aPath.localeCompare(bPath); - } else { - return a?.id.localeCompare(b.id); - } - }); +const hierarchy = (data) =>{ + return d3.hierarchy(data); } -/** - * Algorithm used to position nodes in a Tree. Position depends on the layout, - * either Tree or Vertical Layout views. - * @param {*} levels - How many levels we need for this tree. This depends on the dataset subjects/samples/folders/files. - * @param {*} layout - * @param {*} furthestLeft - */ -export const algorithm = (levels, layout, furthestLeft) => { - let positionsMap = {}; - let levelsMapKeys = Object.keys(levels); - - levelsMapKeys.forEach( level => { - furthestLeft = 0 - (Math.ceil(levels[level].length)/2 * nodeSpace ); - positionsMap[level] = furthestLeft + nodeSpace; - sortArray(levels[level]); - }); - - // Start assigning the graph from the bottom up - let neighbors = 0; - levelsMapKeys.reverse().forEach( level => { - let collapsedInLevel = levels[level].filter( n => n.collapsed); - let notcollapsedInLevel = levels[level].filter( n => !n.collapsed); - levels[level].forEach ( (n, index) => { - neighbors = n?.neighbors?.filter(neighbor => { return neighbor.level > n.level }); - if ( !n.collapsed ) { - if ( neighbors?.length > 0 ) { - let max = Number.MIN_SAFE_INTEGER, min = Number.MAX_SAFE_INTEGER; - neighbors.forEach( neighbor => { - if ( layout === TOP_DOWN.layout ) { - if ( neighbor.xPos > max ) { max = neighbor.xPos }; - if ( neighbor.xPos <= min ) { min = neighbor.xPos }; - } else if ( layout === LEFT_RIGHT.layout ) { - if ( neighbor.yPos > max ) { max = neighbor.yPos }; - if ( neighbor.yPos <= min ) { min = neighbor.yPos }; - } - }); - if ( layout === TOP_DOWN.layout ) { - n.xPos = min === max ? min : min + ((max - min) * .5); - } else if ( layout === LEFT_RIGHT.layout ) { - n.yPos = min === max ? min : min + ((max - min) * .5); - } - if ( notcollapsedInLevel?.length > 0 && collapsedInLevel.length > 0) { - if ( n.type === "Subject" || n.parent?.type === "Subject" || n.parent?.parent?.type === "Subject" ) { - updateConflictedNodes(levels[level], n, positionsMap, level, index, layout); - } - } - - if ( layout === TOP_DOWN.layout ) { - n.fx = n.xPos; - n.fy = 50 * n.level; - positionsMap[n.level] = n.xPos + nodeSpace; - } else if ( layout === LEFT_RIGHT.layout ) { - n.fy = n.yPos; - n.fx = 50 * n.level; - positionsMap[n.level] = n.yPos + nodeSpace; - } - } else { - if ( layout === TOP_DOWN.layout ) { - let position = determineNodePosition(positionsMap, levels, n, "xPos"); - n.xPos = position; - n.fx = n.xPos; - n.fy = 50 * n.level; - } else if ( layout === LEFT_RIGHT.layout ) { - let position = determineNodePosition(positionsMap, levels, n, "yPos"); - n.yPos = position; - n.fy = n.yPos; - n.fx = 50 * n.level; - } - } - }else { - if ( layout === TOP_DOWN.layout ) { - let position = determineNodePosition(positionsMap, levels, n, "xPos"); - n.xPos = position; - n.fx = n.xPos; - n.fy = 50 * n.level; - } else if ( layout === LEFT_RIGHT.layout ) { - let position = determineNodePosition(positionsMap, levels, n, "yPos"); - n.yPos = position; - n.fy = n.yPos; - n.fx = 50 * n.level; - } - } - }) +const dendrogram = (data) => { + const dendrogramGenerator = d3.cluster().nodeSize([1, 100]) + .separation(function(a,b){ + return 1 + d3.sum([a,b].map(function(d){ + return 15 + })) }); - } + return dendrogramGenerator(hierarchy(data)); +} /** * Create Graph ID @@ -412,59 +234,72 @@ export const getPrunedTree = (graph_id, layout) => { levels[n.level] = [n]; } }) - - // Calculate level with max amount of nodes - let maxLevel = Object.keys(levels).reduce((a, b) => levels[a].length > levels[b].length ? a : b); - let maxLevelNodes = levels[maxLevel]; - // The furthestLeft a node can be - let furthestLeft = 0 - (Math.ceil(maxLevelNodes.length)/2 * nodeSpace ); + + // Calculate level with max amount of nodes + let maxLevel = parseInt(Object.keys(levels).reduce((a, b) => levels[a].length > levels[b].length ? a : b)); - algorithm(levels, layout, furthestLeft); + + let root = levelsMap["1"]?.[0]; - const graph = { nodes : visibleNodes, links : visibleLinks, levelsMap : levelsMap, hierarchyVariant : maxLevel * 20 }; - return graph; - }; + let data = { + type : "node", + name : root?.id, + value : levelsMap["1"]?.[0]?.level - 1, + children : [] + }; - /** - * Update Nodes x and y position, used for vertical and tree view layouts. - * @param {*} nodes - The nodes we have for the dataset - * @param {*} conflictNode - Conflicting Node that needs re positioning - * @param {*} positionsMap - Object keeping track of positions of nodes - * @param {*} level - level of tree - * @param {*} index - Index of conflict node in this tree level - * @param {*} layout - The layout we are using to display these nodes - */ - const updateConflictedNodes = (nodes, conflictNode, positionsMap, level, index, layout) => { - let matchIndex = index; - for ( let i = 0; i <= index ; i++ ) { - let conflict = nodes.find ( n => !n.collapsed && n?.parent?.id === nodes[i]?.parent?.id) - if ( conflict === undefined ){ - conflict = nodes.find ( n => !n.collapsed ) - if ( conflict === undefined ){ - conflict = conflictNode; - } + function traverse(node, data) { + if (node === null) { + return; } - matchIndex = nodes.findIndex( n => n.id === conflict.id ); + node.neighbors?.forEach( n => { + if ( visibleNodes?.find( node => node.id === n.id ) ) { + if ( n.neighbors?.length > 1 ) { + if ( n?.level > node.level ) { + let node = { + type : "node", + name : n.id, + value : n?.level - 1, + children : [] + } + data.children.push(node); + traverse(n, node) + } + } else { + data.children.push({type : "leaf", + name : n.id, + value : n?.level - 1}) + } + } + }); + } + + traverse(root,data) + + // Use D3 cluster to give position to nodes + const allNodes = dendrogram(data).descendants(); + let mapNodes = {}; + allNodes.forEach( n => mapNodes[n.data?.name] = n ); + + // Assign position of nodes + visibleNodes.forEach( n => { if ( layout === TOP_DOWN.layout ) { - let furthestLeft = conflict?.xPos; - if ( nodes?.[i]?.collapsed ) { - furthestLeft = conflict.xPos - ((((matchIndex - i )/2)) * nodeSpace ); - nodes[i].xPos =furthestLeft; - positionsMap[level] = furthestLeft + nodeSpace; + if ( mapNodes[n.id] ) { + n.xPos = mapNodes[n.id].x + n.fx = n.xPos; + n.fy = 50 * n.level; } - nodes[i].fx = nodes[i].xPos; - nodes[i].fy = 50 * nodes[i].level; - } else if ( layout === LEFT_RIGHT.layout ) { - let furthestLeft = conflict?.yPos; - if ( nodes[i].collapsed ) { - furthestLeft = conflict.yPos - ((((matchIndex - i )/2)) * nodeSpace ); - nodes[i].yPos =furthestLeft; + } + if ( layout === LEFT_RIGHT.layout ) { + if ( mapNodes[n.id] ) { + n.yPos = mapNodes[n.id].x + n.fy = n.yPos; + n.fx = 50 * n.level; } - positionsMap[level] = furthestLeft + nodeSpace; - nodes[i].fy = nodes[i].yPos; - positionsMap[level] = nodes[i].fy + nodeSpace; - nodes[i].fx = 50 * nodes[i].level; } - } - } \ No newline at end of file + }) + + const graph = { nodes : visibleNodes, links : visibleLinks, levelsMap : levelsMap, hierarchyVariant : maxLevel * 20 }; + return graph; + }; diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 9c400c5..97de6d4 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -14,6 +14,7 @@ import { protocols_key, contributors_key, SUBJECTS_LEVEL, PROTOCOLS_LEVEL, CRONTRIBUTORS_LEVEL } from '../constants'; +import * as d3 from 'd3'; const N3 = require('n3'); const ttl2jsonld = require('@frogcat/ttl2jsonld').parse; @@ -231,7 +232,11 @@ class Splinter { if ( !existingLing ) { const a = this.nodes.get( link.source ); const b = this.nodes.get( link.target ); - if ( a && b && ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ) && !((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1)) && !((a?.type === rdfTypes.Sample.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Sample.key && b.children_counter < 1)) ) { + const awardEmpty = ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ); + const collectionEmpty = ((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1)); + const sampleEmpty = ((a?.type === rdfTypes.Sample.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Sample.key && b.children_counter < 1)) + const sameLevels = a?.level === b?.level; + if ( a && b && awardEmpty && !collectionEmpty && !sampleEmpty && !sameLevels) { !a.neighbors && (a.neighbors = []); !b.neighbors && (b.neighbors = []); if ( !a.neighbors.find( n => n.id === b.id )){ @@ -256,10 +261,19 @@ class Splinter { } } }); + + let newCleanLinks = cleanLinks.filter(link => { + + const collectionEmpty = ((link?.target?.type === rdfTypes.Collection.key && link?.target?.neighbors?.length <= 1 ) || ( link?.source?.type === rdfTypes.Collection.key && link?.source?.neighbors?.length <= 1)); + if ( collectionEmpty ) { + return false; + } + return true; + }); console.log("This levels map ", this.levelsMap) return { nodes: filteredNodes, - links: cleanLinks, + links: newCleanLinks, levelsMap : this.levelsMap }; } @@ -891,15 +905,21 @@ class Splinter { let jsonNode = this.tree_map.get(folder); const splitName = jsonNode.dataset_relative_path.split('/'); let newName = jsonNode.basename; - if ( value.type == "Subject" && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { + if ( value.type === rdfTypes.Subject.key && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { newName = splitName[0] } + + if ( value.type === rdfTypes.Sample.key && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { + newName = splitName[0] + "/" + newName + } + let parentNode = value; let newNode = this.buildFolder(jsonNode, newName, parentNode); if ( value.type === rdfTypes.Sample.key) { newNode.remote_id = jsonNode.basename + '_' + newName; newNode.uri_api = newNode.remote_id + // this.tree_parents_map2.delete(jsonNode.remote_id); } From fb063ccb6865e6006a5d42407eca6a9960bdea33 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 28 May 2024 05:41:25 -0700 Subject: [PATCH 66/76] #SDSV-30 - Fix dependency issues --- package.json | 1 - yarn.lock | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9061a36..c6376f4 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@types/react": "^17.0.5", "@types/react-dom": "^17.0.3", "axios": "^0.21.1", - "d3": "^7.1.1", "fs": "^0.0.1-security", "gh-pages": "^3.2.3", "jest": "26.6.0", diff --git a/yarn.lock b/yarn.lock index 1081921..c37197d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5321,6 +5321,13 @@ css-blank-pseudo@^0.1.4: dependencies: postcss "^7.0.5" +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" @@ -11733,7 +11740,7 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -memoize-one@^5.0.0: +memoize-one@^5.0.0, memoize-one@^5.1.1: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== @@ -14423,6 +14430,11 @@ quickselect@^2.0.0: resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + raf@^3.2.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -14494,6 +14506,19 @@ react-app-polyfill@^2.0.0: regenerator-runtime "^0.13.7" whatwg-fetch "^3.4.1" +react-beautiful-dnd@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-color@^2.17.3: version "2.19.3" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" @@ -14792,6 +14817,18 @@ react-redux@^5.0.6: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-redux@^7.2.0: + version "7.2.9" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" + integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" + react-redux@^7.2.4: version "7.2.8" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" @@ -17302,6 +17339,11 @@ tiny-invariant@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== +tiny-invariant@^1.0.6: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + tiny-warning@^1.0.0, tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -17929,6 +17971,11 @@ url@^0.11.0, url@~0.11.0: punycode "1.3.2" querystring "0.2.0" +use-memo-one@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" From 945b67e28a7b34b5d68edaabb92614f999e3ec81 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 28 May 2024 09:16:55 -0700 Subject: [PATCH 67/76] #SDSV-30 - Fix collapse sub levels from tree --- src/components/GraphViewer/GraphViewer.js | 33 ++++++++++++++++------- src/utils/Splinter.js | 5 ++-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 171b468..0fc0038 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -239,19 +239,30 @@ const GraphViewer = (props) => { let node = nodeSelected; let collapsed = node.collapsed let parent = node.parent; - while ( parent && !parent?.collapsed) { + let prevNode = node; + while ( parent && parent?.collapsed && node.type ) { + prevNode = parent; parent = parent.parent; } - if ( nodeSelected.collapsed && nodeClickSource === "TREE") { - collapseSubLevels(parent, false, { links : 0 }); - let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); - setData(updatedData); - - node.collapsed = true; - collapseSubLevels(nodeSelected, true, { links : 0 }); - updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); - setData(updatedData); + if ( prevNode && nodeSelected.collapsed && nodeClickSource === "TREE") { + if ( prevNode.type == rdfTypes.Subject.key || prevNode.type == rdfTypes.Sample.key || + prevNode.type == rdfTypes.Collection.key ) { + prevNode.collapsed = false; + collapseSubLevels(prevNode, false, { links : 0 }); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + setData(updatedData); + } + if ( node.parent?.type == rdfTypes.Subject.key || node.parent?.type == rdfTypes.Sample.key || + node.parent?.type == rdfTypes.Collection.key ) { + collapseSubLevels(node.parent, true, { links : 0 }); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + setData(updatedData); + } else { + collapseSubLevels(node, true, { links : 0 }); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + setData(updatedData); + } } setSelectedNode(nodeSelected); handleNodeHover(nodeSelected); @@ -260,6 +271,8 @@ const GraphViewer = (props) => { handleNodeHover(nodeSelected); graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, ONE_SECOND); } + const divElement = document.getElementById(nodeSelected.id + detailsLabel); + divElement?.scrollIntoView({ behavior: 'smooth' }); } },[nodeSelected]) diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 97de6d4..16a083b 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -925,6 +925,7 @@ class Splinter { let folderChildren = this.tree_parents_map2.get(newNode.parent_id)?.map(child => { child.parent_id = newNode.uri_api + child.collapsed = true; return child; }); @@ -971,9 +972,10 @@ class Splinter { level = this.nodes.get(parent.attributes.derivedFrom[0])?.level + 1; } } + const new_node = this.buildNodeFromJson(node, level); + new_node.collapsed = new_node.type === typesModel.NamedIndividual.subject.type if ( parent ) { parent.children_counter++; - const new_node = this.buildNodeFromJson(node, level); new_node.parent = parent; new_node.id = parent.id + new_node.id; this.forced_edges.push({ @@ -981,7 +983,6 @@ class Splinter { target: new_node?.id }); new_node.childLinks = []; - new_node.collapsed = new_node.type === typesModel.NamedIndividual.subject.type if ( !this.nodes.get(new_node.id) ) { this.nodes.set(new_node.id, this.factory.createNode(new_node)); var children = this.tree_parents_map2.get(node.remote_id); From 82867687316da20fde6c3cd145644a2cf134e564 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 28 May 2024 09:46:05 -0700 Subject: [PATCH 68/76] Fix tree selection by assigning IDs in right place --- src/utils/Splinter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 16a083b..096ebaa 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -915,6 +915,7 @@ class Splinter { let parentNode = value; let newNode = this.buildFolder(jsonNode, newName, parentNode); + newNode.id = parentNode.id + newNode.id; if ( value.type === rdfTypes.Sample.key) { newNode.remote_id = jsonNode.basename + '_' + newName; @@ -977,7 +978,6 @@ class Splinter { if ( parent ) { parent.children_counter++; new_node.parent = parent; - new_node.id = parent.id + new_node.id; this.forced_edges.push({ source: parent?.id, target: new_node?.id From e12db84ac09ad59fd967a0743d2c25ac0a391a96 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 28 May 2024 11:05:25 -0700 Subject: [PATCH 69/76] Fixing tree issues --- src/components/GraphViewer/GraphViewer.js | 2 +- src/utils/Splinter.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 0fc0038..fc9bc85 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -240,7 +240,7 @@ const GraphViewer = (props) => { let collapsed = node.collapsed let parent = node.parent; let prevNode = node; - while ( parent && parent?.collapsed && node.type ) { + while ( parent && parent?.collapsed ) { prevNode = parent; parent = parent.parent; } diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 096ebaa..bd78fac 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -915,7 +915,6 @@ class Splinter { let parentNode = value; let newNode = this.buildFolder(jsonNode, newName, parentNode); - newNode.id = parentNode.id + newNode.id; if ( value.type === rdfTypes.Sample.key) { newNode.remote_id = jsonNode.basename + '_' + newName; @@ -974,10 +973,10 @@ class Splinter { } } const new_node = this.buildNodeFromJson(node, level); - new_node.collapsed = new_node.type === typesModel.NamedIndividual.subject.type if ( parent ) { parent.children_counter++; new_node.parent = parent; + new_node.id = parent.id + new_node.id; this.forced_edges.push({ source: parent?.id, target: new_node?.id From 0975aec11ee74ac501b95ceafbf64a9891b211e5 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 28 May 2024 15:33:43 -0700 Subject: [PATCH 70/76] Check if node selected belong to same dataset as in Graph viewer --- src/components/GraphViewer/GraphViewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index fc9bc85..82fe4fd 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -234,7 +234,7 @@ const GraphViewer = (props) => { }, [selectedNode]); useEffect(() => { - if ( nodeSelected ) { + if ( nodeSelected && nodeSelected?.tree_reference?.dataset_id?.includes(props.graph_id)) { if ( nodeSelected?.id !== selectedNode?.id ){ let node = nodeSelected; let collapsed = node.collapsed From 73db68f44be00790a0577ec9f54989d092fc03f5 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Tue, 28 May 2024 16:00:14 -0700 Subject: [PATCH 71/76] Cleanup and group selection check on multiple datasets --- src/components/GraphViewer/GraphViewer.js | 5 +++-- src/utils/Splinter.js | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 82fe4fd..f07b6a2 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -219,7 +219,7 @@ const GraphViewer = (props) => { }); useEffect(() => { - if ( groupSelected ) { + if ( groupSelected && groupSelected?.dataset_id?.includes(props.graph_id)) { setSelectedNode(groupSelected); handleNodeHover(groupSelected); graphRef?.current?.ggv?.current.centerAt(groupSelected.x, groupSelected.y, ONE_SECOND); @@ -234,7 +234,8 @@ const GraphViewer = (props) => { }, [selectedNode]); useEffect(() => { - if ( nodeSelected && nodeSelected?.tree_reference?.dataset_id?.includes(props.graph_id)) { + if ( nodeSelected && ( nodeSelected?.tree_reference?.dataset_id?.includes(props.graph_id) || + nodeSelected?.dataset_id?.includes(props.graph_id) )) { if ( nodeSelected?.id !== selectedNode?.id ){ let node = nodeSelected; let collapsed = node.collapsed diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index bd78fac..625e996 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -270,7 +270,6 @@ class Splinter { } return true; }); - console.log("This levels map ", this.levelsMap) return { nodes: filteredNodes, links: newCleanLinks, From 7d112cea5da9377adb396fefe70c3e60eb08be8a Mon Sep 17 00:00:00 2001 From: jrmartin Date: Thu, 6 Jun 2024 14:45:28 -0700 Subject: [PATCH 72/76] #SDSV-30 - Fix tree interaction --- src/components/GraphViewer/GraphViewer.js | 19 +++++++------------ src/utils/Splinter.js | 9 +++++++++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index f07b6a2..4af105b 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -70,14 +70,10 @@ const GraphViewer = (props) => { const handleNodeLeftClick = (node, event) => { if ( node.type === rdfTypes.Subject.key || node.type === rdfTypes.Sample.key || node.type === rdfTypes.Collection.key ) { - node.collapsed = !node.collapsed; collapseSubLevels(node, node.collapsed, { links : 0 }); + node.collapsed = !node.collapsed; let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); - - collapseSubLevels(node, true, { links : 0 }); - updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); - setData(updatedData); } handleNodeHover(node); @@ -121,7 +117,7 @@ const GraphViewer = (props) => { setCollapsed(!collapsed) setTimeout( () => { resetCamera(); - },200) + },10) } /** @@ -174,10 +170,7 @@ const GraphViewer = (props) => { const onEngineStop = () => { setForce(); - if ( triggerCenter ) { - graphRef?.current?.ggv?.current.centerAt(selectedNode.x, selectedNode.y, ONE_SECOND); - triggerCenter = false; - } + graphRef?.current?.ggv?.current.centerAt(selectedNode.x, selectedNode.y, ONE_SECOND); } useEffect(() => { @@ -234,8 +227,10 @@ const GraphViewer = (props) => { }, [selectedNode]); useEffect(() => { - if ( nodeSelected && ( nodeSelected?.tree_reference?.dataset_id?.includes(props.graph_id) || - nodeSelected?.dataset_id?.includes(props.graph_id) )) { + let sameDataset = nodeSelected?.tree_reference?.dataset_id?.includes(props.graph_id) || + nodeSelected?.dataset_id?.includes(props.graph_id) + || nodeSelected?.attributes?.dataset_id?.includes(props.graph_id); + if ( nodeSelected && sameDataset) { if ( nodeSelected?.id !== selectedNode?.id ){ let node = nodeSelected; let collapsed = node.collapsed diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index 625e996..4736500 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -799,6 +799,7 @@ class Splinter { } if (node.attributes?.relativePath !== undefined) { + node.attributes.dataset_id = this.dataset_id; node.attributes.publishedURI = Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + "?datasetDetailsTab=files&path=files/" + @@ -1097,6 +1098,14 @@ class Splinter { if ( node.graph_reference === undefined ) { node.graph_reference = this.findReference(node.uri_api); } + if ( node.graph_reference === undefined ) { + const fn = (hashMap, str) => [...hashMap.keys()].find(k => k.includes(str)) + const graph_reference = fn(this.nodes, node.id) + + if ( graph_reference ) { + node.graph_reference = this.findReference(graph_reference); + } + } this.tree_map.set(node.id, node); const newNode = { id: node.uri_api, From 30e49146818bf9e93c8a28620a374f744092a650 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 7 Jun 2024 12:01:39 -0700 Subject: [PATCH 73/76] #SDSV-30 - Update README and fixes for graphviewer bugs --- README.md | 18 ++++++++++-------- src/components/GraphViewer/GraphViewer.js | 18 +++++++++--------- src/utils/graphModel.js | 2 +- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 30b101c..0348847 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,10 @@ Deployed version : https://metacell.github.io/sds-viewer/ The SDS Viewer can now be launched directly from datasets and models on the SPARC Portal (https://sparc.science/). From the landing page for your dataset or model of interest, simply click the SDS Viewer button, it will launch the viewer with it already loaded. In addition, users can load SPARC datasets using two other methods: -1) Loading a SPARC Dataset from list: +1) Loading a SPARC Dataset from app: - Click on 'SPARC Datasets' button, it's located on the lower left corner. - - On the window that opens up, select the dataset you want to load. + - On the window that opens up, select the dataset you want to load. You can search + by dataset title and id. ![image](https://user-images.githubusercontent.com/4562825/166984322-83b4a8c2-aa29-4e6d-96e9-bcf4d125a3a9.png) - After selection, click 'Done' - Dataset will be loaded. @@ -21,27 +22,28 @@ The SDS Viewer can now be launched directly from datasets and models on the SPAR This will open up the SDS Viewer with the dataset already loaded. ##### Loaded dataset example ##### -![Screenshot 2023-09-21 at 3 50 49 PM](https://github.com/MetaCell/sds-viewer/assets/4562825/e7247cf1-df5e-498d-a418-4cbc7f4c4de2) +![image](https://github.com/MetaCell/sds-viewer/assets/4562825/9ea43afd-28cc-4b37-8c72-96be2f821f1a) + ##### SPARC Dataset used ##### ![Screenshot 2023-09-21 at 3 53 33 PM](https://github.com/MetaCell/sds-viewer/assets/4562825/f3e287ed-f93a-436b-b3b0-b85cb1c0857c) ### Navigating the SDS Viewer - - Users can search for subjects, folders and files on the sidebar. Selecting an item on the sidebar will display the Metadata for it and zoom the Graph to its corresponding node. -![Screenshot 2023-09-21 at 4 04 23 PM](https://github.com/MetaCell/sds-viewer/assets/4562825/b64ea659-607f-42f7-b58f-edb01e31ab40) + - Users can search for subjects, folders and files on the sidebar. Selecting an item on the sidebar will display the Metadata for it and zoom the Graph to its corresponding folder or file. +![image](https://github.com/MetaCell/sds-viewer/assets/4562825/7b013f5a-eead-4996-b7d2-20b3bf35a294) - Selecting an item on the Graph will display its Metadata. ![image](https://user-images.githubusercontent.com/4562825/186723085-c6573146-82dc-4fb7-ae95-588f7b1e4842.png) - - Navigating the Graph Viewer can be done with the mouse. There's also controlers on the bottom right that allows the user to change the Layout view, zoom in/out, reset the view to its original state and expand all data in the viewer. + - Navigating the Graph Viewer can be done with the mouse. There are also controllers on the bottom right that allow user to change the Graph Layout view, zoom in/out of the graph, reset the Layout to its original state and expand/collapse all data in the viewer. ![controllers](https://github.com/MetaCell/sds-viewer/assets/99416933/30aa8bb3-ec61-46d8-9f83-55ade15b95c0) - - Multiple Datasets can be loaded at the same time, which will open a new Graph Viewer Component for each dataset. + - Multiple Datasets can be loaded at the same time, a new Graph Viewer Component will be opened for each dataset. -![multiple](https://github.com/MetaCell/sds-viewer/assets/99416933/a74fa033-ccd4-4609-b50f-852ce44d347a) +![Multiple](https://github.com/MetaCell/sds-viewer/assets/4562825/9abe621a-a406-4e6b-8d6a-165622014425) ### Datasets Used diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index 4af105b..c5101ac 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -170,7 +170,7 @@ const GraphViewer = (props) => { const onEngineStop = () => { setForce(); - graphRef?.current?.ggv?.current.centerAt(selectedNode.x, selectedNode.y, ONE_SECOND); + selectedNode && handleNodeRightClick(nodeSelected) } useEffect(() => { @@ -259,10 +259,10 @@ const GraphViewer = (props) => { let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); } + setSelectedNode(nodeSelected); + handleNodeHover(nodeSelected); + graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, ONE_SECOND); } - setSelectedNode(nodeSelected); - handleNodeHover(nodeSelected); - graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, ONE_SECOND); } else { handleNodeHover(nodeSelected); graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, ONE_SECOND); @@ -357,11 +357,6 @@ const GraphViewer = (props) => { controls={
- - - - - { handleLayoutChange(TOP_DOWN)}>{TOP_DOWN.label} handleLayoutChange(LEFT_RIGHT)}>{LEFT_RIGHT.label} + + + + + zoomIn()}> diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index 7a1d2b8..2feaece 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -137,7 +137,7 @@ export const rdfTypes = { "key": "title", "property": "title", "label": "Label", - "visible" : false + "visible" : true }, { "type": "dc", From b846680dd4cb904b153c077e2444805efdd145f4 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 7 Jun 2024 13:19:14 -0700 Subject: [PATCH 74/76] #SDSV-31 - Add version to config file to keep track of storage --- src/App.js | 11 +++++++---- .../DatasetsListViewer/DatasetsListDialog.js | 6 +++--- src/config/app.json | 3 ++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/App.js b/src/App.js index 724c959..7949ee4 100644 --- a/src/App.js +++ b/src/App.js @@ -131,7 +131,7 @@ const App = () => { const splinter = new DatasetsListSplinter(undefined, file.data); let graph = await splinter.getGraph(); let datasets = graph.nodes.filter((node) => node?.attributes?.hasDoi); - let version = graph.nodes.find( node => node?.attributes?.versionInfo)?.attributes?.versionInfo + let version = config.version const match = datasets.find( node => node.attributes?.hasDoi?.[0]?.includes(doi)); if ( match ) { const datasetID = match.name; @@ -142,13 +142,13 @@ const App = () => { } let datasetStorage = {}; - if ( version !== undefined && localStorage.getItem(config.datasetsStorage)?.version !== version[0] ) { + if ( version !== undefined && JSON.parse(localStorage.getItem(config.datasetsStorage))?.version !== version ) { let parsedDatasets = [] datasets.forEach( node => { parsedDatasets.push({ name : node.name , doi : node.attributes?.hasDoi?.[0], label : node.attributes ? node.attributes?.label?.[0]?.toLowerCase() : null}); }); datasetStorage = { - version : version[0], + version : version, datasets : parsedDatasets } @@ -163,7 +163,10 @@ const App = () => { if (doi && doi !== "" ) { if ( doiMatch ){ - if ( localStorage.getItem(config.datasetsStorage) ) { + let version = config.version; + const storage = JSON.parse(localStorage.getItem(config.datasetsStorage)); + const storageVersion = storage?.version + if ( storageVersion === version ) { let storedDatasetsInfo = JSON.parse(localStorage.getItem(config.datasetsStorage)); const match = storedDatasetsInfo.datasets.find( node => node?.doi.includes(doi)); if ( match ) { diff --git a/src/components/DatasetsListViewer/DatasetsListDialog.js b/src/components/DatasetsListViewer/DatasetsListDialog.js index 7cc464e..9edbd57 100644 --- a/src/components/DatasetsListViewer/DatasetsListDialog.js +++ b/src/components/DatasetsListViewer/DatasetsListDialog.js @@ -108,15 +108,15 @@ const DatasetsListDialog = (props) => { datasets = datasets.filter( node => node?.attributes?.statusOnPlatform?.[0]?.includes(PUBLISHED) ); - let version = graph.nodes.find( node => node?.attributes?.versionInfo)?.attributes?.versionInfo + let version = config.version; let datasetStorage = {}; - if ( version !== undefined && localStorage.getItem(config.datasetsStorage)?.version !== version[0] ) { + if ( version !== undefined && JSON.parse(localStorage.getItem(config.datasetsStorage))?.version !== version ) { let parsedDatasets = [] datasets.forEach( node => { parsedDatasets.push({ name : node.name , doi : node.attributes?.hasDoi?.[0], label : node.attributes ? node.attributes.lowerCaseLabel : null}); }); datasetStorage = { - version : version[0], + version : version, datasets : parsedDatasets } diff --git a/src/config/app.json b/src/config/app.json index 13eab97..ed8e34b 100644 --- a/src/config/app.json +++ b/src/config/app.json @@ -18,5 +18,6 @@ "datasetsDialogSearchText" : "Search datasets by label or id", "datasetsButtonSubtitleText" : "Select a dataset to load" }, - "datasetsStorage" : "publishedDatasets" + "datasetsStorage" : "publishedDatasets", + "version" : "1.1" } \ No newline at end of file From ee3dfb01528bad55df8adaf6e60cb6ea86307934 Mon Sep 17 00:00:00 2001 From: jrmartin Date: Fri, 7 Jun 2024 13:30:34 -0700 Subject: [PATCH 75/76] #SDSV-31 - Cleanup, add tooltips --- src/components/GraphViewer/GraphViewer.js | 4 ++-- .../NodeDetailView/NodeDetailView.js | 4 +++- .../NodeDetailView/settings/SettingsItem.js | 23 ++++++++++--------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index c5101ac..c070e47 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -368,11 +368,11 @@ const GraphViewer = (props) => { handleLayoutChange(TOP_DOWN)}>{TOP_DOWN.label} handleLayoutChange(LEFT_RIGHT)}>{LEFT_RIGHT.label} + - - + zoomIn()}> diff --git a/src/components/NodeDetailView/NodeDetailView.js b/src/components/NodeDetailView/NodeDetailView.js index ff1e3cd..1ca30e2 100644 --- a/src/components/NodeDetailView/NodeDetailView.js +++ b/src/components/NodeDetailView/NodeDetailView.js @@ -2,7 +2,7 @@ import {Box} from "@material-ui/core"; import NodeFooter from "./Footers/Footer"; import DetailsFactory from './factory'; import Breadcrumbs from "./Details/Views/Breadcrumbs"; -import { IconButton } from '@material-ui/core'; +import { IconButton, Tooltip } from '@material-ui/core'; import { subject_key, protocols_key, contributors_key } from '../../constants'; import {TuneRounded} from "@material-ui/icons"; import { useSelector, useDispatch } from 'react-redux' @@ -79,7 +79,9 @@ const NodeDetailView = (props) => { } { !showSettingsContent && + + } ); diff --git a/src/components/NodeDetailView/settings/SettingsItem.js b/src/components/NodeDetailView/settings/SettingsItem.js index a693d29..602e228 100644 --- a/src/components/NodeDetailView/settings/SettingsItem.js +++ b/src/components/NodeDetailView/settings/SettingsItem.js @@ -67,17 +67,18 @@ const SettingsItem = props => { onClick={toggleItemDisabled} disableRipple > - - {!item.visible ? ( - - ) : ( - - )} - + + {!item.visible ? ( + + + ) : ( + + + )} From 8dcf3d26c6c43c6c813ed91c94ce0290c4e01447 Mon Sep 17 00:00:00 2001 From: Jesus M Date: Fri, 7 Jun 2024 13:40:44 -0700 Subject: [PATCH 76/76] Update README.md --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0348847..c4dae68 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The SDS Viewer can now be launched directly from datasets and models on the SPAR ![image](https://github.com/MetaCell/sds-viewer/assets/4562825/9ea43afd-28cc-4b37-8c72-96be2f821f1a) ##### SPARC Dataset used ##### -![Screenshot 2023-09-21 at 3 53 33 PM](https://github.com/MetaCell/sds-viewer/assets/4562825/f3e287ed-f93a-436b-b3b0-b85cb1c0857c) +![image](https://github.com/MetaCell/sds-viewer/assets/4562825/16d878e2-d5bb-4dbd-9695-dfbd7ae5207f) ### Navigating the SDS Viewer @@ -33,14 +33,22 @@ The SDS Viewer can now be launched directly from datasets and models on the SPAR ![image](https://github.com/MetaCell/sds-viewer/assets/4562825/7b013f5a-eead-4996-b7d2-20b3bf35a294) - - Selecting an item on the Graph will display its Metadata. + - Selecting an item on the Graph will display its Metadata. Users can view Metadata for the Dataset's Subjects and Samples, along with its folders and files contents. Users can find links to the SPARC Portal for Subjects, Samples, Folders and Files. ![image](https://user-images.githubusercontent.com/4562825/186723085-c6573146-82dc-4fb7-ae95-588f7b1e4842.png) + - Navigating the Graph Viewer can be done with the mouse. There are also controllers on the bottom right that allow user to change the Graph Layout view, zoom in/out of the graph, reset the Layout to its original state and expand/collapse all data in the viewer. ![controllers](https://github.com/MetaCell/sds-viewer/assets/99416933/30aa8bb3-ec61-46d8-9f83-55ade15b95c0) + + - Use the Metadata Settings button to control which properties to view on the Metadata panel. Toggle on and off properties on each Object type and click Save. + +![image](https://github.com/MetaCell/sds-viewer/assets/4562825/6385b5a1-3598-4815-8aa1-f1223debe063) +![image](https://github.com/MetaCell/sds-viewer/assets/4562825/d5876581-2dfd-4f13-9213-d46907e443c8) + + - Multiple Datasets can be loaded at the same time, a new Graph Viewer Component will be opened for each dataset. ![Multiple](https://github.com/MetaCell/sds-viewer/assets/4562825/9abe621a-a406-4e6b-8d6a-165622014425)