Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend rework #73

Merged
merged 17 commits into from
Jan 30, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -45,6 +45,8 @@

/rails/node_modules

/node_modules

# -- React --
/react/dist

19 changes: 14 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -10,11 +10,15 @@ services:
POSTGRES_DB: bookworm_production
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
healthcheck:
test: [ "CMD-SHELL", "pg_isready -d bookworm_production -U postgres" ]
test: [ "CMD-SHELL", "pg_isready -d bookworm_production -U bookworm" ]
interval: 10s
timeout: 5s
retries: 10
networks:
- bookworm
hostname: db
rails:
hostname: rails
build: ./rails
container_name: bookworm-rails
command: "bundle exec rails s -p 3001 -b '0.0.0.0'"
@@ -34,20 +38,24 @@ services:
depends_on:
db:
condition: service_healthy
networks:
- bookworm

react:
build: ./react
container_name: bookworm-react
env_file: .env
environment:
NODE_ENV: production
RAILS_API_URL: $RAILS_API_URL
ports:
- 3000:80
healthcheck:
test: [ "CMD-SHELL", "wget --no-verbose --spider --tries=1 localhost:3000 || exit 1" ]
interval: 10s
timeout: 5s
retries: 10
hostname: bookworm-react
networks:
- bookworm
depends_on:
db:
condition: service_healthy
@@ -68,9 +76,10 @@ services:
condition: service_healthy
react:
condition: service_healthy
networks:
- bookworm

volumes:
postgres_data: {}
networks:
default:
name: bookworm
bookworm:
4 changes: 2 additions & 2 deletions nginx/conf.d/bookworm-ssl.conf
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
server {
listen 443;
listen 443 ssl;
server_name bookworm;

ssl_certificate /etc/ssl/certificate.pem;
ssl_certificate_key /etc/ssl/key.pem;

location /graphql {
proxy_pass http://bookworm-rails:3001/graphql;
proxy_pass http://rails:3001/graphql;
proxy_redirect default;
proxy_http_version 1.1;

2 changes: 1 addition & 1 deletion nginx/conf.d/bookworm.conf
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ server {
server_name bookworm;

location /graphql {
proxy_pass http://bookworm-rails:3001/graphql;
proxy_pass http://rails:3001/graphql;
proxy_redirect default;
proxy_http_version 1.1;

2 changes: 1 addition & 1 deletion rails/app/graphql/book_worm_schema.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class BookWormSchema < GraphQL::Schema
mutation(Types::MutationType)
# mutation(Types::MutationType)
query(Types::QueryType)

# For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
31 changes: 23 additions & 8 deletions rails/app/graphql/types/author_type.rb
Original file line number Diff line number Diff line change
@@ -17,19 +17,34 @@ def works
object.works
end

field :authors_ids, Types::AuthorsIdsType
def authors_ids
object.authors_ids
field :datasets, [Types::WorkType]
def datasets
object.works.where(work_type: "dataset")
end

field :articles, [Types::WorkType]
def articles
object.works.where(work_type: "article")
end

field :scopus, String
def scopus
object.authors_ids.scopus
end

field :wikipedia, String
def wikipedia
object.authors_ids.wikipedia
end

field :mag, String
def mag
object.authors_ids.mag
end

field :institutions, [Types::InstitutionType]
def institutions
object.institutions.uniq # move this to the Author model probably
end

field :authors_counts_by_year, [Types::AuthorsCountsByYearType]
def authors_counts_by_year
object.authors_counts_by_year
end
end
end
14 changes: 0 additions & 14 deletions rails/app/graphql/types/authors_counts_by_year_type.rb

This file was deleted.

15 changes: 0 additions & 15 deletions rails/app/graphql/types/authors_ids_type.rb

This file was deleted.

48 changes: 46 additions & 2 deletions rails/app/graphql/types/institution_type.rb
Original file line number Diff line number Diff line change
@@ -20,9 +20,53 @@ class InstitutionType < Types::BaseObject
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

field :authors, [Types::AuthorType]

def authors
object.authors
object.authors.uniq
end

field :wikidata, String
def wikidata
object.institutions_ids.wikidata
end

field :wikipedia, String
def wikipedia
object.institutions_ids.wikipedia
end

field :mag, String
def mag
object.institutions_ids.mag
end

field :grid, String
def grid
object.institutions_ids.grid
end

field :city, String
def city
object.institutions_geo.city
end

field :region, String
def region
object.institutions_geo.region
end

field :country, String
def country
object.institutions_geo.country
end

field :latitude, Float
def latitude
object.institutions_geo.latitude
end

field :longitude, Float
def longitude
object.institutions_geo.longitude
end
end
end
14 changes: 0 additions & 14 deletions rails/app/graphql/types/institutions_counts_by_year_type.rb

This file was deleted.

17 changes: 0 additions & 17 deletions rails/app/graphql/types/institutions_geo_type.rb

This file was deleted.

15 changes: 0 additions & 15 deletions rails/app/graphql/types/institutions_ids_type.rb

This file was deleted.

2 changes: 1 addition & 1 deletion rails/app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

module Types
class MutationType < Types::BaseObject
field :register_user, mutation: Mutations::RegisterUser
# field :register_user, mutation: Mutations::RegisterUser
# field :sign_in, mutation: Mutations::SignIn
# # field :sign_out, mutation: Mutations::SignOut

10 changes: 10 additions & 0 deletions rails/app/graphql/types/publisher_type.rb
Original file line number Diff line number Diff line change
@@ -14,5 +14,15 @@ class PublisherType < Types::BaseObject
field :sources_api_url, String
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

field :ror, String
def ror
object.publishers_ids.ror
end

field :wikidata, String
def wikidata
object.publishers_ids.wikidata
end
end
end
14 changes: 0 additions & 14 deletions rails/app/graphql/types/publishers_counts_by_year_type.rb

This file was deleted.

12 changes: 0 additions & 12 deletions rails/app/graphql/types/publishers_ids_type.rb

This file was deleted.

12 changes: 11 additions & 1 deletion rails/app/graphql/types/query_type.rb
Original file line number Diff line number Diff line change
@@ -40,11 +40,21 @@ def nodes(ids:)
null: true,
description: 'Fetches an institution by ROR' do
argument :ror, String, required: true
end
end
def institution_by_ror(ror:)
Institution.find_by(ror: ror)
end

field :institution_by_name,
Types::InstitutionType,
null: true,
description: 'Fetches an institution by name' do
argument :name, String, required: true
end
def institution_by_name(name:)
Institution.find_by(display_name: name)
end

# author entry points

field :author_by_openalex_id,
25 changes: 25 additions & 0 deletions rails/app/graphql/types/source_type.rb
Original file line number Diff line number Diff line change
@@ -16,5 +16,30 @@ class SourceType < Types::BaseObject
field :works_api_url, String
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

field :issn_l, String
def issn_l
object.source_ids.issn_l
end

field :issn, String
def issn
object.sources_ids.issn
end

field :mag, String
def mag
object.sources_ids.mag
end

field :wikidata, String
def wikidata
object.sources_ids.wikidata
end

field :fatcat, String
def fatcat
object.sources_ids.fatcat
end
end
end
14 changes: 0 additions & 14 deletions rails/app/graphql/types/sources_counts_by_year_type.rb

This file was deleted.

15 changes: 0 additions & 15 deletions rails/app/graphql/types/sources_ids_type.rb

This file was deleted.

87 changes: 76 additions & 11 deletions rails/app/graphql/types/work_type.rb
Original file line number Diff line number Diff line change
@@ -34,29 +34,94 @@ def referenced_works
object.referenced_works
end

field :referenced_articles, [Types::WorkType]
def referenced_articles
object.referenced_works.where(work_type: "article")
end

field :referencing_works, [Types::WorkType]
def referencing_works
object.referencing_works
end

field :works_open_access, Types::WorksOpenAccessType
def works_open_access
object.works_open_access
end

field :works_biblio, Types::WorksBiblioType
def works_biblio
object.works_biblio
field :referencing_articles, [Types::WorkType]
def referencing_articles
object.referencing_articles.where(work_type: "article")
end

field :topics, [Types::TopicType]
def topics
object.topics
end

field :works_ids, Types::WorksIdsType
def works_ids
object.works_ids
field :is_oa, Boolean
def is_oa
object.works_open_access.is_oa
end

field :oa_status, String
def oa_status
object.works_open_access.oa_status
end

field :oa_url, String
def oa_url
object.works_open_access.oa_url
end

field :volume, String
def volume
object.works_biblio.volume
end

field :issue, String
def issue
object.works_biblio.issue
end

field :first_page, String
def first_page
object.works_biblio.first_page
end

field :last_page, String
def last_page
object.works_biblio.last_page
end

field :pmid, String
def pmid
object.works_ids.pmid
end

field :pmcid, String
def pmcid
object.works_ids.pmcid
end

field :landing_page_url, String
def landing_page_url
object.works_best_oa_location.landing_page_url
end

field :pdf_url, String
def pdf_url
object.works_best_oa_location.pdf_url
end

field :license, String
def license
object.works_best_oa_location.license
end

field :version, String
def version
object.works_best_oa_location.version
end

field :any_repository_has_fulltext, Boolean
def any_repository_has_fulltext
object.works_open_access.any_repository_has_fulltext
end
end
end
16 changes: 0 additions & 16 deletions rails/app/graphql/types/works_best_oa_location_type.rb

This file was deleted.

14 changes: 0 additions & 14 deletions rails/app/graphql/types/works_biblio_type.rb

This file was deleted.

14 changes: 0 additions & 14 deletions rails/app/graphql/types/works_ids_type.rb

This file was deleted.

16 changes: 0 additions & 16 deletions rails/app/graphql/types/works_location_type.rb

This file was deleted.

14 changes: 0 additions & 14 deletions rails/app/graphql/types/works_open_access_type.rb

This file was deleted.

16 changes: 0 additions & 16 deletions rails/app/graphql/types/works_primary_location_type.rb

This file was deleted.

14 changes: 11 additions & 3 deletions rails/app/models/institution.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# frozen_string_literal: true

class Institution < ApplicationRecord
has_many :institutions_counts_by_year
has_one :institutions_geo
has_one :institutions_ids
has_many :institutions_counts_by_year,
primary_key: :institution_openalex_id,
foreign_key: :institution_openalex_id

has_one :institutions_geo,
primary_key: :institution_openalex_id,
foreign_key: :institution_openalex_id

has_one :institutions_ids,
primary_key: :institution_openalex_id,
foreign_key: :institution_openalex_id

has_many :institutions_associated_institutions,
foreign_key: :associated_institution_openalex_id,
2 changes: 1 addition & 1 deletion rails/config/database.yml
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ default: &default
adapter: postgresql
encoding: unicode
username: bookworm
# password: <%= ENV["POSTGRES_PASSWORD"] %>
password: <%= ENV["POSTGRES_PASSWORD"] %>
pool: 5

development:
25 changes: 14 additions & 11 deletions react/index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>📖 🐛 BookWorm </title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>📖 🐛 BookWorm </title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
21,758 changes: 0 additions & 21,758 deletions react/package-lock.json

This file was deleted.

10 changes: 6 additions & 4 deletions react/package.json
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"test": "jest"
@@ -21,7 +21,10 @@
"react-dom": "^18.2.0",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.0",
"reactflow": "^11.11.4"
"reactflow": "^11.11.4",
"d3-force": "^3.0.0",
"graphiql": "^3.8.3",
"react-sliding-side-panel": "^2.0.5"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -46,7 +49,6 @@
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"ts-node": "^10.9.2",
"typescript": "^5.2.2",
"vite": "^5.2.0"
"typescript": "^5.2.2"
}
}
11 changes: 0 additions & 11 deletions react/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import { useState } from "react";
import SignOutButton from "../signOutButton";

function Header() {
const [userEmail, setUserEmail] = useState<any | null>(null);

// setUserEmail(localStorage.getItem("email"));

// if (localStorage.getItem("email")) {
// setUserEmail(localStorage.getItem("email"));
// }

return (
<>
<header className="header flex flex-row items-center justify-between sm:justify-around p-2 border-b-2 bg-green-200">
<div>📖 🐛 BookWorm</div>
<div> Your email is: {localStorage.getItem("email")}</div>
<SignOutButton />
</header>
</>
);
14 changes: 12 additions & 2 deletions react/src/components/graph/AuthorshipGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { useMemo, } from "react";
import ReactFlow, { useNodesState, useEdgesState, MarkerType, useReactFlow } from "reactflow";

import {
ReactFlow,
ReactFlowProvider,
Panel,
useNodesState,
useEdgesState,
useReactFlow,
useNodesInitialized,
} from "reactflow"

import "reactflow/dist/style.css";
import WorkNode from "./nodes/WorkNode.tsx";
import AuthorNode from "./nodes/AuthorNode.tsx";
@@ -29,7 +39,7 @@ const getLayoutedElements = (nodes, edges) => {
const root = g(hierarchy(nodes));
const layout = g.nodeSize([width * 2, height * 2])(root);

return {
return { // should just return the nodes/edges for reactflow instead of taking them as arguments
nodes: layout
.descendants()
.map((node) => ({ ...node.data, position: { x: node.x, y: node.y } })),
125 changes: 0 additions & 125 deletions react/src/components/graph/InvestigationGraph.tsx

This file was deleted.

49 changes: 49 additions & 0 deletions react/src/components/graph/collide.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { quadtree } from 'd3-quadtree';

export function collide() {
let nodes = [];
let force = (alpha) => {
const tree = quadtree(
nodes,
(d) => d.x,
(d) => d.y,
);

for (const node of nodes) {
const r = node.measured.width / 2;
const nx1 = node.x - r;
const nx2 = node.x + r;
const ny1 = node.y - r;
const ny2 = node.y + r;

tree.visit((quad, x1, y1, x2, y2) => {
if (!quad.length) {
do {
if (quad.data !== node) {
const r = node.measured.width / 2 + quad.data.width / 2;
let x = node.x - quad.data.x;
let y = node.y - quad.data.y;
let l = Math.hypot(x, y);

if (l < r) {
l = ((l - r) / l) * alpha;
node.x -= x *= l;
node.y -= y *= l;
quad.data.x += x;
quad.data.y += y;
}
}
} while ((quad = quad.next));
}

return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
}
};

force.initialize = (newNodes) => (nodes = newNodes);

return force;
}

export default collide;
91 changes: 66 additions & 25 deletions react/src/components/graph/nodes/AuthorDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,88 @@
interface Props {
displayName: string;
orcid: string;
authorOpenalexId: string;
orcid: string;
displayName: string;
worksCount: number;
citedByCount: number;
lastKnownInstitution: string;
scopus: string;
wikipedia: string;
mag: string;
}

function AuthorDetails({
displayName,
orcid,
authorOpenalexId,
orcid,
displayName,
worksCount,
citedByCount,
lastKnownInstitution,
scopus,
wikipedia,
mag
}: Props) {
return (
<>
<table
className="border-collapse border border-slate-500"
style={{ backgroundColor: "rgb(229, 190, 190)" }}
>
<tr className="border border-slate-700">
<td>ORCID</td>
<td>{orcid}</td>
</tr>
<tr className="border border-slate-700">
<td>OpenAlexId</td>
<td>{authorOpenalexId}</td>
</tr>
<tr className="border border-slate-700">
<td>Cited By Count</td>
<td>{citedByCount}</td>
</tr>
<tr className="border border-slate-700">
<td>Works Count</td>
<td>{worksCount}</td>
</tr>
<tr className="border border-slate-700">
<td>Last Known Institution</td>
<td>{lastKnownInstitution}</td>
</tr>

<tbody>
{authorOpenalexId &&
<tr className="border border-slate-700">
<td>OpenAlexId</td>
<td>{authorOpenalexId}</td>
</tr>
}
{orcid &&
<tr className="border border-slate-700">
<td>ORCID</td>
<td>{orcid}</td>
</tr>
}
{displayName &&
<tr className="border border-slate-700">
<td>Cited By Count</td>
<td>{citedByCount}</td>
</tr>
}
{worksCount &&
<tr className="border border-slate-700">
<td>Works Count</td>
<td>{worksCount}</td>
</tr>
}
{citedByCount &&
<tr className="border border-slate-700">
<td>Cited By Count</td>
<td>{citedByCount}</td>
</tr>
}
{lastKnownInstitution &&
<tr className="border border-slate-700">
<td>Last Known Institution</td>
<td>{lastKnownInstitution}</td>
</tr>
}
{scopus &&
<tr className="border border-slate-700">
<td>SCOPUS</td>
<td>{scopus}</td>
</tr>
}
{wikipedia &&
<tr className="border border-slate-700">
<td>Wikipedia</td>
<td>{wikipedia}</td>
</tr>
}
{mag &&
<tr className="border border-slate-700">
<td>MAG</td>
<td>{mag}</td>
</tr>
}
</tbody>
</table>
</>
);
9 changes: 5 additions & 4 deletions react/src/components/graph/nodes/AuthorNode.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Handle, Position } from "reactflow";
import { useParams } from "react-router";
import "./author-node.css";
import { useCallback, useEffect, useRef, useState } from "react";
import AuthorDetails from "./AuthorDetails";
@@ -12,7 +11,6 @@ interface AuthorNode {
}
function AuthorNode({ data }) {
const [isExpanded, setIsExpanded] = useState(false);
const { investigationId } = useParams();
const ref = useRef(null);
const [contentHeight, setContentHeight] = useState(0);

@@ -45,12 +43,15 @@ function AuthorNode({ data }) {
>
<div ref={ref}>
<AuthorDetails
displayName={data.authorData.displayName}
orcid={data.authorData.orcid}
authorOpenalexId={data.authorData.authorOpenalexId}
orcid={data.authorData.orcid}
displayName={data.authorData.displayName}
worksCount={data.authorData.worksCount}
citedByCount={data.authorData.citedByCount}
lastKnownInstitution={data.authorData.lastKnownInstitution}
scopus={data.authorData.scopus}
wikipedia={data.authorData.wikipedia}
mag={data.authorData.mag}
/>
</div>
</div>
164 changes: 164 additions & 0 deletions react/src/components/graph/nodes/InstitutionDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
interface Props {
institutionOpenalexId: string;
ror: string;
countryCode: string;
institutionType: string;
homepageUrl: string;
imageUrl: string;
imageThumbnailUrl: string;
worksCount: number;
citedByCount: number;
wikidata: string;
wikipedia: string;
mag: string;
grid: string;
city: string;
region: string;
country: string;
latitude: number;
longitude: number;
}

function InstitutionDetails({
institutionOpenalexId,
ror,
countryCode,
institutionType,
homepageUrl,
imageUrl,
imageThumbnailUrl,
worksCount,
citedByCount,
wikidata,
wikipedia,
mag,
grid,
city,
region,
country,
latitude,
longitude
}: Props) {
return (
<>
<table>
<tbody
className="border-collapse border border-slate-500"
style={{ backgroundColor: "rgb(229, 190, 190)" }}
>
{institutionOpenalexId &&
<tr className="border border-slate-700">
<td>OpenAlex ID</td>
<td>{institutionOpenalexId}</td>
</tr>}

{ror &&
<tr className="border border-slate-700">
<td>ROR</td>
<td>{ror}</td>
</tr>}

{countryCode &&
<tr className="border border-slate-700">
<td>Country Code</td>
<td>{countryCode}</td>
</tr>}

{institutionType &&
<tr className="border border-slate-700">
<td>Institution Type</td>
<td>{institutionType}</td>
</tr>}

{homepageUrl &&
<tr className="border border-slate-700">
<td>Homepage URL</td>
<td>{homepageUrl}</td>
</tr>}

{imageUrl &&
<tr className="border border-slate-700">
<td>Image URL</td>
<td>{imageUrl}</td>
</tr>}

{imageThumbnailUrl &&
<tr className="border border-slate-700">
<td>Image Thumbnail URL</td>
<td>{imageThumbnailUrl}</td>
</tr>}

{worksCount &&
<tr className="border border-slate-700">
<td>Works Count</td>
<td>{worksCount}</td>
</tr>}

{citedByCount &&
<tr className="border border-slate-700">
<td>Cited By Count</td>
<td>{citedByCount}</td>
</tr>}

{wikidata &&
<tr className="border border-slate-700">
<td>Wikidata</td>
<td>{wikidata}</td>
</tr>}

{wikipedia &&
<tr className="border border-slate-700">
<td>Wikipedia</td>
<td>{wikidata}</td>
</tr>}

{mag &&
<tr className="border border-slate-700">
<td>MAG</td>
<td>{mag}</td>
</tr>}

{grid &&
<tr className="border border-slate-700">
<td>GRID</td>
<td>{grid}</td>
</tr>}

{city &&
<tr className="border border-slate-700">
<td>City</td>
<td>{city}</td>
</tr>}

{region &&
<tr className="border border-slate-700">
<td>Region</td>
<td>{region}</td>
</tr>}

{country &&
<tr className="border border-slate-700">
<td>Country</td>
<td>{country}</td>
</tr>}

{latitude &&
<tr className="border border-slate-700">
<td>Latitude</td>
<td>{latitude}</td>
</tr>}

{longitude &&
<tr className="border border-slate-700">
<td>Longitude</td>
<td>{longitude}</td>
</tr>}

</tbody>

</table>
</>
)
}

export default InstitutionDetails;
72 changes: 72 additions & 0 deletions react/src/components/graph/nodes/InstitutionNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Handle, Position } from "reactflow"
import "./institution-node.css"
import { useCallback, useEffect, useRef, useState } from "react";
import InstitutionDetails from "./InstitutionDetails";

interface InstitutionNode {
id: number;
xCoordinate: number;
yCoordinate: number;
visible: boolean;
}

function InstitutionNode({ data }) {
const [isExpanded, setIsExpanded] = useState(false);
const ref = useRef(null);
const [contentHeight, setContentHeight] = useState(0);

const toggleIsExpanded = useCallback(() => {
setIsExpanded((isExpanded) => !isExpanded);
}, []);

useEffect(() => {
if (ref.current) {
setContentHeight(ref.current["clientHeight"]);
}
}, []);

return (
<div className="institution-node">
<Handle type="target" position={Position.Top} />
<Handle type="source" position={Position.Bottom} />
<div>
<label htmlFor="text">{data.institutionData.displayName}</label>
</div>
<button onClick={toggleIsExpanded}>
{isExpanded ? "▼ Hide Details" : "▶ Show Details"}
</button>
<div
className="collapse"
style={{
height: isExpanded ? contentHeight : 0,
visibility: isExpanded ? "visible" : "collapse",
}}
>
<div ref={ref}>
<InstitutionDetails
institutionOpenalexId={data.institutionData.institutionOpenalexId}
ror={data.institutionData.ror}
countryCode={data.institutionData.countryCode}
institutionType={data.institutionData.institutionType}
homepageUrl={data.institutionData.imageUrl}
imageUrl={data.institutionData.imageUrl}
imageThumbnailUrl={data.institutionData.imageThumbnailUrl}
worksCount={data.institutionData.worksCount}
citedByCount={data.institutionData.citedByCount}
wikidata={data.institutionData.wikidata}
wikipedia={data.institutionData.wikipedia}
mag={data.institutionData.mag}
grid={data.institutionData.grid}
city={data.institutionData.city}
region={data.institutionData.region}
country={data.institutionData.country}
latitude={data.institutionData.latitude}
longitude={data.institutionData.longitude}
/>
</div>
</div>
</div>
)
}

export default InstitutionNode;
43 changes: 0 additions & 43 deletions react/src/components/graph/nodes/NoteNode.tsx

This file was deleted.

236 changes: 192 additions & 44 deletions react/src/components/graph/nodes/WorkDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,223 @@
import { useParams } from "react-router";

interface Props {
title: string;
workOpenalexId: string;
doi: string;
title: string;
displayName: string;
publicationYear: number;
publicationDate: number;
workType: string;
citedByCount: number;
isRetracted: boolean;
isParatext: boolean;
language: string;
// isOpenAccess: boolean;
workOpenalexId: string;
// abstract: string;
isOa: boolean;
oaStatus: string;
oaUrl: string;
volume: string;
issue: string;
firstPage: string;
lastPage: string;
pmid: string;
pmcid: string;
landingPageUrl: string;
pdfUrl: string;
license: string;
version: string;
anyRepositoryHasFulltext: boolean;
}

function WorkDetails({
title,
workOpenalexId,
doi,
title,
displayName,
publicationYear,
publicationDate,
workType,
citedByCount,
isRetracted,
isParatext,
language
language,
isOa,
oaStatus,
oaUrl,
volume,
issue,
firstPage,
lastPage,
pmid,
pmcid,
landingPageUrl,
pdfUrl,
license,
version,
anyRepositoryHasFulltext
}: Props) {

return (
<>
<table
className="border-collapse border border-slate-500"
style={{ backgroundColor: "rgb(229, 190, 190)" }}
style={{
backgroundColor: isOa
? "#FBD87F"
: "#B5F8FE",
}}
>
<tr className="border border-slate-700">
<td>Title</td>
<td>{title}</td>
</tr>
<tr className="border border-slate-700">
<td>DOI</td>
<td>{doi}</td>
</tr>
<tr className="border border-slate-700">
<td>Publication Year</td>
<td>{publicationYear}</td>
</tr>
<tr className="border border-slate-700">
<td>Publication Date</td>
<td>{publicationDate}</td>
</tr>
<tr className="border border-slate-700">
<td>Work Type</td>
<td>{workType}</td>
</tr>
<tr className="border border-slate-700">
<td>Language</td>
<td>{language}</td>
</tr>
<tr className="border border-slate-700">
<td>Cited By Count</td>
<td>{citedByCount}</td>
</tr>
<tr className="border border-slate-700">
<td>Is Retracted?</td>
<td>{isRetracted}</td>
</tr>
<tr className="border border-slate-700">
<td>Is Paratext?</td>
<td>{isParatext}</td>
</tr>

<tbody style={{textAlign:"left"}}>
{workOpenalexId &&
<tr className="border border-slate-700">
<td>Openalex ID</td>
<td>{workOpenalexId}</td>
</tr>
}
{doi &&
<tr className="border border-slate-700">
<td>DOI</td>
<td>{doi}</td>
</tr>
}
{title &&
<tr className="border border-slate-700">
<td>Title</td>
<td>{title}</td>
</tr>
}
{displayName &&
<tr className="border border-slate-700">
<td>Display Name</td>
<td>{displayName}</td>
</tr>
}
{publicationYear &&
<tr className="border border-slate-700">
<td>Publication Year</td>
<td>{publicationYear}</td>
</tr>
}
{publicationDate &&
<tr className="border border-slate-700">
<td>Publication Date</td>
<td>{publicationDate}</td>
</tr>
}
{workType &&
<tr className="border border-slate-700">
<td>Work Type</td>
<td>{workType}</td>
</tr>
}
{citedByCount &&
<tr className="border border-slate-700">
<td>Cited By Count</td>
<td>{citedByCount}</td>
</tr>
}
{isRetracted &&
<tr className="border border-slate-700">
<td>Is Retracted?</td>
<td>{isRetracted}</td>
</tr>
}
{isParatext &&
<tr className="border border-slate-700">
<td>Is Paratext?</td>
<td>{isParatext}</td>
</tr>
}
{language &&
<tr className="border border-slate-700">
<td>Language</td>
<td>{language}</td>
</tr>
}
{isOa &&
<tr className="border border-slate-700">
<td>Is Open Access?</td>
<td>{isOa}</td>
</tr>
}
{oaStatus &&
<tr className="border border-slate-700">
<td>Open Access Status</td>
<td>{oaStatus}</td>
</tr>
}
{oaUrl &&
<tr className="border border-slate-700">
<td>OA URL</td>
<td>{oaUrl}</td>
</tr>
}
{volume &&
<tr className="border border-slate-700">
<td>Volume</td>
<td>{volume}</td>
</tr>
}
{issue &&
<tr className="border border-slate-700">
<td>Issue</td>
<td>{issue}</td>
</tr>
}
{firstPage &&
<tr className="border border-slate-700">
<td>First Page</td>
<td>{firstPage}</td>
</tr>
}
{lastPage &&
<tr className="border border-slate-700">
<td>Last Page</td>
<td>{firstPage}</td>
</tr>
}
{pmid &&
<tr className="border border-slate-700">
<td>PMID</td>
<td>{pmid}</td>
</tr>
}
{pmcid &&
<tr className="border border-slate-700">
<td>PMCID</td>
<td>{pmcid}</td>
</tr>
}
{landingPageUrl &&
<tr className="border border-slate-700">
<td>Landing Page URL</td>
<td>{landingPageUrl}</td>
</tr>
}
{pdfUrl &&
<tr className="border border-slate-700">
<td>PDF URL</td>
<td>{pdfUrl}</td>
</tr>
}
{license &&
<tr className="border border-slate-700">
<td>License</td>
<td>{license}</td>
</tr>
}
{version &&
<tr className="border border-slate-700">
<td>Version</td>
<td>{version}</td>
</tr>
}
{anyRepositoryHasFulltext &&
<tr className="border border-slate-700">
<td>Any Repository Has Full Text?</td>
<td>{anyRepositoryHasFulltext}</td>
</tr>
}
</tbody>
</table>
</>
);
24 changes: 20 additions & 4 deletions react/src/components/graph/nodes/WorkNode.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Handle, Position } from "reactflow";
import { useParams } from "react-router";
import "./work-node.css";
import { useState, useCallback, useRef, useEffect } from "react";
import WorkDetails from "./WorkDetails";
@@ -31,7 +30,9 @@ function WorkNode({ data }) {
<div
className="work-node"
style={{
backgroundColor: "#B5F8FE",
backgroundColor: data.workData.isOa
? "#FBD87F"
: "#B5F8FE",
}}
ref={noderef}
>
@@ -50,16 +51,31 @@ function WorkNode({ data }) {
>
<div ref={ref}>
<WorkDetails
title={data.workData.title}
doi={data.workData.doi}
workOpenalexId={data.workData.workOpenalexId}
doi={data.workData.doi}
title={data.workData.title}
displayName={data.workData.displayName}
publicationYear={data.workData.publicationYear}
publicationDate={data.workData.publicationDate}
workType={data.workData.workType}
citedByCount={data.workData.citedByCount}
isRetracted={data.workData.isRetracted}
isParatext={data.workData.isParatext}
language={data.workData.language}
isOa={data.workData.isOa}
oaStatus={data.workData.oaStatus}
oaUrl={data.workData.oaUrl}
volume={data.workData.volume}
issue={data.workData.issue}
firstPage={data.workData.firstPage}
lastPage={data.workData.lastPage}
pmid={data.workData.pmid}
pmcid={data.workData.pmcid}
landingPageUrl={data.workData.landingPageUrl}
pdfUrl={data.workData.pdfUrl}
license={data.workData.license}
version={data.workData.version}
anyRepositoryHasFulltext={data.workData.anyRepositoryHasFulltext}
/>
</div>
</div>
15 changes: 15 additions & 0 deletions react/src/components/graph/nodes/institution-node.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.institution-node {
border: 1px solid black;
padding: 5px;
border-radius: 5px;
background: rgb(209, 168, 233);
width: 200px;
color: black;
}

.institution-node label {
display: block;
color: black;
font-size: 18px;
font-style: italic;
}
13 changes: 0 additions & 13 deletions react/src/components/graph/nodes/note-node.css

This file was deleted.

262 changes: 260 additions & 2 deletions react/src/components/pages/CustomQueryPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,269 @@
import Header from "../Header.tsx";
import GraphiQL from "graphiql";
import 'graphiql/graphiql.css';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import ReactFlow, {
useNodesState,
useEdgesState,
MarkerType,} from "reactflow";
import "reactflow/dist/style.css";
import WorkNode from "../graph/nodes/WorkNode.tsx";
import AuthorNode from "../graph/nodes/AuthorNode.tsx";
import InstitutionNode from "../graph/nodes/InstitutionNode.tsx";
import AuthorshipEdge from "../graph/edges/AuthorshipEdge.tsx";
import {
forceSimulation,
forceLink,
forceManyBody,
forceX,
forceY,
forceCenter,
forceCollide,
} from 'd3-force';
import SlidingPanel from 'react-sliding-side-panel';
import 'react-sliding-side-panel/lib/index.css';
import collide from "../graph/collide.tsx";

const NESTED_ELEMENT_OPTIONS = ["articles", "works", "institutions", "authors", "referencedWorks", "referencingWorks"];
const proOptions = { hideAttribution: true };


const getElementType = (data) => {
if ('authorByOpenalexId' in data
|| 'authorByOrcid' in data
|| 'orcid' in data
|| 'authorOpenalexId' in data) {
return "author"
} else if ('workByOpenalexId' in data
|| 'workByDoi' in data
|| 'workOpenalexId' in data
|| 'doi' in data
|| 'workType' in data) {
return "work"
} else if ('institutionByRor' in data
|| 'institutionByOpenalexId' in data
|| 'institutionByName' in data
|| 'institutionOpenalexId' in data) {
return "institution"
} else {
return null;
}
}

//iterate using 'depth' as index?


const createRootFlowNode = (nodesArr, edgesArr, inputJson) => {
if (getElementType(inputJson.data) == "author") {
nodesArr.push({
id: `author-${Object.values(inputJson.data)[0].id}`,
data: {
authorData: Object.values(inputJson.data)[0]
},
position: {
x: 0,
y: 0
},
type: "author"
})
} else if (getElementType(inputJson.data) == "institution") {
nodesArr.push({
id: `institution-${Object.values(inputJson.data)[0].id}`,
data: {
institutionData: Object.values(inputJson.data)[0]
},
position: {
x: 0,
y: 0
},
type: "institution"
})
} else if (getElementType(inputJson.data) == "work") {
nodesArr.push({
id: `work-${Object.values(inputJson.data)[0].id}`,
data: {
workData: Object.values(inputJson.data)[0]
},
position: {
x: 0,
y: 0
},
type: "work"
})
}

return {
nodes: nodesArr,
edges: edgesArr
}
}

const dedupeArrayById = (inputArr) => {
let i = 0;
for (let j = 0; j < inputArr.length; j++) {
if(inputArr[i].id !== inputArr[j].id) {
i++;
inputArr[i] = inputArr[j]
}
}
inputArr.length = i + 1;
}

const createChildFlowNodes = (nodesArr, edgesArr, rootNode) => {

const rootData = rootNode.hasOwnProperty('position') ? Object.values(rootNode.data)[0] : rootNode

const rootNodeType = getElementType(rootData)
console.log(rootNodeType)

for (const [key, value] of Object.entries(rootData)) {
if (Array.isArray(value)) {
for (let i = 0; i <= value.length - 1; i++) {
let childNodeType = getElementType(value[i]);
edgesArr.push({
source: `${rootNodeType}-${rootData.id}`,
target: `${childNodeType}-${value[i].id}`,
id: `edge-${rootNodeType}-${rootData.id}-${childNodeType}-${value[i].id}`,
style: {
strokeWidth: 2,
stroke: "#FF0072"
},
markerEnd: {
type: MarkerType.ArrowClosed,
width: 20,
height: 20,
color: "#FF0072"
}
})

if (childNodeType == "author") {
nodesArr.push({
id: `author-${value[i].id}`,
data: {
authorData: value[i]
},
position: {
x: 0,
y: 0
},
type: `author`
})
} else if (childNodeType == "work") {
nodesArr.push({
id: `work-${value[i].id}`,
data: {
workData: value[i]
},
position: {
x: 0,
y: 0
},
type: 'work'
})
} else if (childNodeType == "institution") {
nodesArr.push({
id: `institution-${value[i].id}`,
data: {
institutionData: value[i]
},
position: {
x: 0,
y: 0
},
type: 'institution'
})
}

createChildFlowNodes(nodesArr, edgesArr, value[i])
//recursion ursion ursion ursion
}
}
}
return {
nodes: nodesArr,
edges: edgesArr
}
}

function CustomQueryPage() {
const nodeTypes = useMemo(
() => ({
institution: InstitutionNode,
work: WorkNode,
author: AuthorNode,
}),
[]
);
const edgeTypes = useMemo(
() => ({
authorship: AuthorshipEdge
}),
[]
);
const [openPanel, setOpenPanel] = useState(false);
const initialNodes = [];
const initialEdges = [];
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

const gql_fetcher = (graphQLParams: any) => {
return fetch("url", {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(graphQLParams),
credentials: "omit"
}).then((response) => {
return response.json()
}).then(json => {
if (!json.data.hasOwnProperty("__schema")) { // only update response data if it's not a schema info response

createRootFlowNode(initialNodes, initialEdges, json);
createChildFlowNodes(initialNodes, initialEdges, initialNodes[0])
dedupeArrayById(initialEdges)
dedupeArrayById(initialNodes)

const simulation = forceSimulation(initialNodes)
.force('link', forceLink(edges).id(d => d.id).distance(100))
.force('charge', forceManyBody().strength(-5000))
.force('center', forceCenter(initialNodes[0].position.x,initialNodes[0].position.y))
.force('x', forceX())
.force('y', forceY())
.on('tick', () => {
setNodes(initialNodes.map(node => ({...node, position: { x: node.x, y: node.y }})))
setEdges(initialEdges.map(edge => ({...edge})))
});
}
return json;
});
}

return (
<>
eyy i'm a placeholder for a graphql interface
<Header />
<button onClick={() => setOpenPanel(true)}>Open</button>
<div>
<SlidingPanel type={'left'} isOpen={openPanel} size={70} >
<div style = {{textAlign:"left"}} className="graphiql-container">
<GraphiQL fetcher={gql_fetcher} />
<button onClick={() => setOpenPanel(false)}>close</button>
</div>
</SlidingPanel>
</div>
<div style = {{height:"100vh", width:"100vw"}}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
// edgeTypes={edgeTypes}
nodesDraggable={true}
proOptions={proOptions}
fitView
/>
</div>
</>
)
}

export default CustomQueryPage;
export default CustomQueryPage;
69 changes: 0 additions & 69 deletions react/src/components/pages/SignInPage.tsx

This file was deleted.

49 changes: 49 additions & 0 deletions react/src/components/pages/collide.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { quadtree } from 'd3-quadtree';

export function collide() {
let nodes = [];
let force = (alpha) => {
const tree = quadtree(
nodes,
(d) => d.x,
(d) => d.y,
);

for (const node of nodes) {
const r = node.measured.width / 2;
const nx1 = node.x - r;
const nx2 = node.x + r;
const ny1 = node.y - r;
const ny2 = node.y + r;

tree.visit((quad, x1, y1, x2, y2) => {
if (!quad.length) {
do {
if (quad.data !== node) {
const r = node.measured.width / 2 + quad.data.width / 2;
let x = node.x - quad.data.x;
let y = node.y - quad.data.y;
let l = Math.hypot(x, y);

if (l < r) {
l = ((l - r) / l) * alpha;
node.x -= x *= l;
node.y -= y *= l;
quad.data.x += x;
quad.data.y += y;
}
}
} while ((quad = quad.next));
}

return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
}
};

force.initialize = (newNodes) => (nodes = newNodes);

return force;
}

export default collide;
11 changes: 0 additions & 11 deletions react/src/hooks/CREATE_NOTE_NODE.tsx

This file was deleted.

11 changes: 0 additions & 11 deletions react/src/hooks/DELETE_NOTE_NODE.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions react/src/hooks/REGISTER_USER.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions react/src/hooks/SIGN_IN.tsx

This file was deleted.

7 changes: 1 addition & 6 deletions react/src/main.tsx
Original file line number Diff line number Diff line change
@@ -7,14 +7,11 @@ import {
HttpLink,
} from "@apollo/client";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

import Root from "./root.tsx";
import "./index.css";
import AuthorPage from "./components/pages/AuthorPage.tsx";
import WorkPage from "./components/pages/WorkPage.tsx";
import CustomQueryPage from "./components/pages/CustomQueryPage.tsx";
import RegistrationPage from "./components/pages/RegistrationPage.tsx";
import SignInPage from "./components/pages/SignInPage.tsx";
import { setContext } from "@apollo/client/link/context";

const httpLink = new HttpLink({
@@ -44,7 +41,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<ApolloProvider client={client}>
<React.StrictMode>
<Routes>
<Route path="/" element={<Root />} />
<Route path="/" element={<CustomQueryPage />} />
<Route
path="/author/:orcid"
element={<AuthorPage />}
@@ -57,8 +54,6 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
path="/customquery"
element={<CustomQueryPage />}
/>
<Route path="/register" element={<RegistrationPage />} />
<Route path="/signin" element={<SignInPage />} />
</Routes>
</React.StrictMode>
</ApolloProvider>
5 changes: 5 additions & 0 deletions react/src/root.css
Original file line number Diff line number Diff line change
@@ -50,3 +50,8 @@
.read-the-docs {
color: #888;
}

.flex-container {
display: flex;
flex-direction: row;
}
23 changes: 0 additions & 23 deletions react/src/signOutButton.tsx

This file was deleted.

16 changes: 8 additions & 8 deletions react/vite.config.ts
Original file line number Diff line number Diff line change
@@ -2,12 +2,12 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
base: "/",
plugins: [react()],
server: {
port: 8080,
strictPort: true,
host: true,
origin: "http://0.0.0.0:8080",
},
base: "/",
plugins: [react()],
server: {
port: 8080,
strictPort: true,
host: true,
origin: "http://0.0.0.0:8080",
},
});