diff --git a/webapp/grebi_api/src/main/java/uk/ac/ebi/grebi/GrebiApi.java b/webapp/grebi_api/src/main/java/uk/ac/ebi/grebi/GrebiApi.java index df52746..157cf1d 100644 --- a/webapp/grebi_api/src/main/java/uk/ac/ebi/grebi/GrebiApi.java +++ b/webapp/grebi_api/src/main/java/uk/ac/ebi/grebi/GrebiApi.java @@ -99,8 +99,10 @@ static void run( ctx.contentType("application/json"); ctx.result("{}"); + String nodeId = new String(Base64.getUrlDecoder().decode(ctx.pathParam("nodeId"))); + var q = new GrebiSolrQuery(); - q.addFilter("grebi:nodeId", List.of(ctx.pathParam("nodeId")), SearchType.WHOLE_FIELD, false); + q.addFilter("grebi:nodeId", List.of(nodeId), SearchType.WHOLE_FIELD, false); var res = solr.getFirstNode(ctx.pathParam("subgraph"), q); diff --git a/webapp/grebi_ui/src/app/util.ts b/webapp/grebi_ui/src/app/util.ts index ea1353c..6b8c02c 100644 --- a/webapp/grebi_ui/src/app/util.ts +++ b/webapp/grebi_ui/src/app/util.ts @@ -54,18 +54,27 @@ export function toCamel(str: string) { export function pickBestDisplayName(names:string[]):string|undefined { - return names.slice(0).sort((a, b) => score(b) - score(a))[0] - function score(name) { - let n = 0 - for (let c of name) { - if (c.match(/[A-Za-z ]/)) { - n++ - } else { - n-=10 - } + return sortDisplayNamesByReadability(names)[0] +} + +export function pickWorstDisplayName(names:string[]):string|undefined { + return sortDisplayNamesByReadability(names).pop() +} + +export function sortDisplayNamesByReadability(names:string[]):string[] { + return names.slice(0).sort((a, b) => readabilityScore(b) - readabilityScore(a)) +} + +export function readabilityScore(name:string) { + let n = 0 + for (let c of name) { + if (c.match(/[A-Za-z ]/)) { + n++ + } else { + n-=10 } - return n } + return n } export function difference(a:any[], b:any[]) { diff --git a/webapp/grebi_ui/src/components/CollapsingIdList.tsx b/webapp/grebi_ui/src/components/CollapsingIdList.tsx new file mode 100644 index 0000000..47689c9 --- /dev/null +++ b/webapp/grebi_ui/src/components/CollapsingIdList.tsx @@ -0,0 +1,33 @@ +import { useState } from "react"; +import { Fragment } from "react/jsx-runtime"; + +let MAX_IDS = 3 + +export default function CollapsingIdList({ids}) { + let [ expanded, setExpanded ] = useState(false); + if(ids.length > MAX_IDS && !expanded) { + return
+ {ids.slice(0, MAX_IDS).map(id => )} +   + setExpanded(true)} + > + + {ids.length - MAX_IDS} + +
+ } else { + return
+ {ids.map(id => )}
+ } + +} + +function Id({id}) { + + return {id.value} + +} + diff --git a/webapp/grebi_ui/src/components/node_edge_list/EdgesInList.tsx b/webapp/grebi_ui/src/components/node_edge_list/EdgesInList.tsx index 36fd4d8..2808647 100644 --- a/webapp/grebi_ui/src/components/node_edge_list/EdgesInList.tsx +++ b/webapp/grebi_ui/src/components/node_edge_list/EdgesInList.tsx @@ -38,7 +38,7 @@ export default function EdgesInList(params:{ async function getEdges() { console.log('refreshing ', node.getNodeId(), JSON.stringify(dsEnabled), JSON.stringify(edgesState?.datasources)) setLoading(true) - let res = (await getPaginated(`api/v1/subgraphs/${subgraph}/nodes/${node.getNodeId()}/incoming_edges?${ + let res = (await getPaginated(`api/v1/subgraphs/${subgraph}/nodes/${node.getEncodedNodeId()}/incoming_edges?${ new URLSearchParams([ ['page', page], ['size', rowsPerPage], diff --git a/webapp/grebi_ui/src/components/node_edge_list/NodeRefLink.tsx b/webapp/grebi_ui/src/components/node_edge_list/NodeRefLink.tsx index 5966e71..27767a4 100644 --- a/webapp/grebi_ui/src/components/node_edge_list/NodeRefLink.tsx +++ b/webapp/grebi_ui/src/components/node_edge_list/NodeRefLink.tsx @@ -12,7 +12,7 @@ export default function NodeRefLink({ }) { let type = nodeRef.extractType() - return + return {nodeRef.getName()} {type && } {/*
diff --git a/webapp/grebi_ui/src/components/node_graph_view/GraphViewCtx.tsx b/webapp/grebi_ui/src/components/node_graph_view/GraphViewCtx.tsx index 7be0910..b3c7847 100644 --- a/webapp/grebi_ui/src/components/node_graph_view/GraphViewCtx.tsx +++ b/webapp/grebi_ui/src/components/node_graph_view/GraphViewCtx.tsx @@ -254,7 +254,7 @@ padding: '8px', async loadShallow(node:GraphNodeRef) { let [incomingEdgeFacets,outgoingEdgeFacets] = (await Promise.all([ - getPaginated(`api/v1/subgraphs/${this.subgraph}/nodes/${node.getNodeId()}/incoming_edges?` + + getPaginated(`api/v1/subgraphs/${this.subgraph}/nodes/${node.getEncodedNodeId()}/incoming_edges?` + new URLSearchParams([ ['size', '1'], ['facet', 'grebi:type'], @@ -262,7 +262,7 @@ padding: '8px', ...Array.from(this.dsExclude).map(ds => ['-grebi:datasources', ds]) ] as any) ), - getPaginated(`api/v1/subgraphs/${this.subgraph}/nodes/${node.getNodeId()}/outgoing_edges?` + + getPaginated(`api/v1/subgraphs/${this.subgraph}/nodes/${node.getEncodedNodeId()}/outgoing_edges?` + new URLSearchParams([ ['size', '1'], ['facet', 'grebi:type'], diff --git a/webapp/grebi_ui/src/frontends/ebi/pages/EbiNodePage.tsx b/webapp/grebi_ui/src/frontends/ebi/pages/EbiNodePage.tsx index 13e136a..e517316 100644 --- a/webapp/grebi_ui/src/frontends/ebi/pages/EbiNodePage.tsx +++ b/webapp/grebi_ui/src/frontends/ebi/pages/EbiNodePage.tsx @@ -19,6 +19,7 @@ import PropTable from "../../../components/node_prop_table/PropTable"; import SearchBox from "../../../components/SearchBox"; import GraphNode from "../../../model/GraphNode"; import { get, getPaginated } from "../../../app/api"; +import encodeNodeId from "../../../encodeNodeId"; export default function EbiNodePage() { @@ -33,7 +34,7 @@ export default function EbiNodePage() { useEffect(() => { async function getNode() { - let graphNode = new GraphNode(await get(`api/v1/subgraphs/${subgraph}/nodes/${nodeId}?lang=${lang}`)) + let graphNode = new GraphNode(await get(`api/v1/subgraphs/${subgraph}/nodes/${encodeNodeId(nodeId)}?lang=${lang}`)) setNode(graphNode) } getNode() @@ -61,9 +62,9 @@ export default function EbiNodePage() {
{pageTitle} { node.extractType()?.long && {node.extractType()?.long}} - - {props['id'].map(id => + {node.getSourceIds().map(id => {id.value} )} - +
{pageDesc} diff --git a/webapp/grebi_ui/src/frontends/ebi/pages/EbiSearchPage.tsx b/webapp/grebi_ui/src/frontends/ebi/pages/EbiSearchPage.tsx index f7c1074..07d8a2e 100644 --- a/webapp/grebi_ui/src/frontends/ebi/pages/EbiSearchPage.tsx +++ b/webapp/grebi_ui/src/frontends/ebi/pages/EbiSearchPage.tsx @@ -9,6 +9,7 @@ import LoadingOverlay from "../../../components/LoadingOverlay"; import SearchBox from "../../../components/SearchBox"; import GraphNode from "../../../model/GraphNode"; import EbiHeader from "../EbiHeader"; +import CollapsingIdList from "../../../components/CollapsingIdList"; export default function EbiSearchPage() { const [searchParams] = useSearchParams(); @@ -334,11 +335,7 @@ export default function EbiSearchPage() { } -
- {graphNode.getIds().map(id => {id.value})} -
+ {graphNode.getDescription() && (
{graphNode.getDescription()} diff --git a/webapp/grebi_ui/src/frontends/exposomekg/pages/EkgSearchPage.tsx b/webapp/grebi_ui/src/frontends/exposomekg/pages/EkgSearchPage.tsx index 6a6d11f..a89b817 100644 --- a/webapp/grebi_ui/src/frontends/exposomekg/pages/EkgSearchPage.tsx +++ b/webapp/grebi_ui/src/frontends/exposomekg/pages/EkgSearchPage.tsx @@ -335,7 +335,7 @@ export default function EbiSearchPage() {
- {graphNode.getIds().map(id => {id.value})}
diff --git a/webapp/grebi_ui/src/model/GraphNode.ts b/webapp/grebi_ui/src/model/GraphNode.ts index b402d33..3b6ce04 100644 --- a/webapp/grebi_ui/src/model/GraphNode.ts +++ b/webapp/grebi_ui/src/model/GraphNode.ts @@ -36,7 +36,7 @@ export default class GraphNode extends GraphNodeRef { let allIdentifiers = [ ...this.getNames().map(p => p.value), ...this.getSynonyms().map(p => p.value), - ...this.getIds().map(p => p.value), + ...this.getSourceIds().map(p => p.value), ]; return allIdentifiers.indexOf(q) !== -1 } diff --git a/webapp/grebi_ui/src/model/GraphNodeRef.ts b/webapp/grebi_ui/src/model/GraphNodeRef.ts index b1a3267..fd8ed80 100644 --- a/webapp/grebi_ui/src/model/GraphNodeRef.ts +++ b/webapp/grebi_ui/src/model/GraphNodeRef.ts @@ -1,6 +1,7 @@ import PropVal from "./PropVal"; -import { pickBestDisplayName } from "../app/util"; +import { pickBestDisplayName, pickWorstDisplayName, readabilityScore } from "../app/util"; +import encodeNodeId from "../encodeNodeId"; export default class GraphNodeRef { @@ -14,6 +15,10 @@ export default class GraphNodeRef { return this.props['grebi:nodeId'] } + getEncodedNodeId():string { + return encodeNodeId(this.props['grebi:nodeId']) + } + getDatasources():string[] { var ds = this.props['grebi:datasources'] || [] ds.sort((a, b) => a.localeCompare(b) + (a.startsWith("OLS.") ? 10000 : 0) + (b.startsWith("OLS.") ? -10000 : 0)) @@ -39,8 +44,32 @@ export default class GraphNodeRef { return this.props['grebi:type'] } - getIds():PropVal[] { - return PropVal.arrFrom(this.props['id']) + getSourceIds():PropVal[] { + let sids:PropVal[] = PropVal.arrFrom(this.props['grebi:sourceIds']) + + // this sort order will ultimately be used in display + // ideally we will see one ID from each datasource at the beginning + // to give an idea of how many sources; and also we prefer numeric + // identifiers since this is explicitly supposed to be identifiers and + // will probably be displayed next to the readable name. + + let res:PropVal[] = [] + + for(let ds of this.getDatasources()) { + let matches = sids.filter(sid => sid.datasources.indexOf(ds) !== -1) + if(matches.length > 0) { + res.push(matches.sort((a, b) => { return numericScore(b.value) - numericScore(a.value) })[0]) + } + } + + let remainder = sids.filter(sid => res.indexOf(sid) === -1) + remainder.sort((a, b) => { return numericScore(b.value) - numericScore(a.value) }) + + return [...res, ...remainder] + + function numericScore(s:string):number { + return [...s].filter(c => c.match(/[0-9]/)).length + } }