frontend/cytoscape feature parity
This commit is contained in:
parent
725604c7e0
commit
5fafffc404
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,7 +2,7 @@
|
||||||
.idea/
|
.idea/
|
||||||
*.gexf
|
*.gexf
|
||||||
data/
|
data/
|
||||||
.vscode/
|
*.class
|
||||||
|
|
||||||
backend/.sobelow
|
backend/.sobelow
|
||||||
|
|
||||||
|
@ -80,6 +80,7 @@ yarn-error.log*
|
||||||
|
|
||||||
/gephi/.gradle/
|
/gephi/.gradle/
|
||||||
/gephi/build/
|
/gephi/build/
|
||||||
|
/gephi/bin/
|
||||||
/gephi/lib/*
|
/gephi/lib/*
|
||||||
/gephi/!lib/.gitkeep
|
/gephi/!lib/.gitkeep
|
||||||
# 64MB file but I don't have much faith that it'll remain available...
|
# 64MB file but I don't have much faith that it'll remain available...
|
||||||
|
|
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"jakebecker.elixir-ls",
|
||||||
|
"ms-vscode.vscode-typescript-tslint-plugin",
|
||||||
|
"kevinmcgowan.typescriptimport",
|
||||||
|
"msjsdiag.debugger-for-chrome"
|
||||||
|
]
|
||||||
|
}
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome",
|
||||||
|
"url": "http://localhost:3000",
|
||||||
|
"webRoot": "${workspaceFolder}/frontend/src",
|
||||||
|
"runtimeExecutable": "/usr/bin/chromium-browser"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
26
README.md
26
README.md
|
@ -6,8 +6,16 @@ Read the latest updates on Mastodon: [@fediversespace](https://cursed.technology
|
||||||
|
|
||||||
![A screenshot of fediverse.space](screenshot.png)
|
![A screenshot of fediverse.space](screenshot.png)
|
||||||
|
|
||||||
|
1. [Requirements](#requirements)
|
||||||
|
2. [Running it](#running-it)
|
||||||
|
3. [Commands](#commands)
|
||||||
|
4. [Privacy](#privacy)
|
||||||
|
5. [Acknowledgements](#acknowledgements)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
Though dockerized, backend development is easiest if you have the following installed.
|
||||||
|
|
||||||
- For the scraper + API:
|
- For the scraper + API:
|
||||||
- Elixir
|
- Elixir
|
||||||
- Postgres
|
- Postgres
|
||||||
|
@ -17,8 +25,6 @@ Read the latest updates on Mastodon: [@fediversespace](https://cursed.technology
|
||||||
- Node.js
|
- Node.js
|
||||||
- Yarn
|
- Yarn
|
||||||
|
|
||||||
All of the above can also be run through Docker with `docker-compose`.
|
|
||||||
|
|
||||||
## Running it
|
## Running it
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
@ -37,17 +43,21 @@ All of the above can also be run through Docker with `docker-compose`.
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
After running the backend in Docker:
|
|
||||||
|
|
||||||
- `docker-compose run gephi java -Xmx1g -jar build/libs/graphBuilder.jar` lays out the graph
|
|
||||||
|
|
||||||
`./gradlew shadowJar` compiles the graph layout program. `java -Xmx1g -jar build/libs/graphBuilder.jar` runs it.
|
`./gradlew shadowJar` compiles the graph layout program. `java -Xmx1g -jar build/libs/graphBuilder.jar` runs it.
|
||||||
|
If running in docker, this means you run
|
||||||
|
|
||||||
|
- `docker-compose build gephi`
|
||||||
|
- `docker-compose run gephi java -Xmx1g -jar build/libs/graphBuilder.jar` lays out the graph
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
- `yarn build` to create an optimized build for deployment
|
- `yarn build` creates an optimized build for deployment
|
||||||
|
|
||||||
### Acknowledgements
|
## Privacy
|
||||||
|
|
||||||
|
This project doesn't crawl personal instances: the goal is to understand communities, not individuals. The threshold for what makes an instance "personal" is defined in the [backend config](backend/config/config.exs) and the [graph builder SQL](gephi/src/main/java/space/fediverse/graph/GraphBuilder.java).
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
[![NLnet logo](https://i.imgur.com/huV3rvo.png)](https://nlnet.nl/project/fediverse_space/)
|
[![NLnet logo](https://i.imgur.com/huV3rvo.png)](https://nlnet.nl/project/fediverse_space/)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
defmodule Backend.Api do
|
defmodule Backend.Api do
|
||||||
alias Backend.{Crawl, Edge, Instance, Repo}
|
alias Backend.{Crawl, Edge, Instance, Repo}
|
||||||
|
import Backend.Util
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
@spec list_instances() :: [Instance.t()]
|
@spec list_instances() :: [Instance.t()]
|
||||||
|
@ -18,18 +19,27 @@ defmodule Backend.Api do
|
||||||
@doc """
|
@doc """
|
||||||
Returns a list of instances that
|
Returns a list of instances that
|
||||||
* have a user count (required to give the instance a size on the graph)
|
* have a user count (required to give the instance a size on the graph)
|
||||||
|
* the user count is > the threshold
|
||||||
* have x and y coordinates
|
* have x and y coordinates
|
||||||
"""
|
"""
|
||||||
@spec list_nodes() :: [Instance.t()]
|
@spec list_nodes() :: [Instance.t()]
|
||||||
def list_nodes() do
|
def list_nodes() do
|
||||||
|
user_threshold = get_config(:personal_instance_threshold)
|
||||||
|
|
||||||
Instance
|
Instance
|
||||||
|> where([i], not is_nil(i.x) and not is_nil(i.y) and not is_nil(i.user_count))
|
|> where(
|
||||||
|
[i],
|
||||||
|
not is_nil(i.x) and not is_nil(i.y) and not is_nil(i.user_count) and
|
||||||
|
i.user_count >= ^user_threshold
|
||||||
|
)
|
||||||
|> select([c], [:domain, :user_count, :x, :y])
|
|> select([c], [:domain, :user_count, :x, :y])
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec list_edges() :: [Edge.t()]
|
@spec list_edges() :: [Edge.t()]
|
||||||
def list_edges() do
|
def list_edges() do
|
||||||
|
user_threshold = get_config(:personal_instance_threshold)
|
||||||
|
|
||||||
Edge
|
Edge
|
||||||
|> join(:inner, [e], i1 in Instance, on: e.source_domain == i1.domain)
|
|> join(:inner, [e], i1 in Instance, on: e.source_domain == i1.domain)
|
||||||
|> join(:inner, [e], i2 in Instance, on: e.target_domain == i2.domain)
|
|> join(:inner, [e], i2 in Instance, on: e.target_domain == i2.domain)
|
||||||
|
@ -37,7 +47,8 @@ defmodule Backend.Api do
|
||||||
|> where(
|
|> where(
|
||||||
[e, i1, i2],
|
[e, i1, i2],
|
||||||
not is_nil(i1.x) and not is_nil(i1.y) and
|
not is_nil(i1.x) and not is_nil(i1.y) and
|
||||||
not is_nil(i2.x) and not is_nil(i2.y)
|
not is_nil(i2.x) and not is_nil(i2.y) and
|
||||||
|
i1.user_count >= ^user_threshold and i2.user_count >= ^user_threshold
|
||||||
)
|
)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
|
@ -188,6 +188,10 @@ defmodule Backend.Crawler do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp save(%{domain: domain, error: error}) do
|
defp save(%{domain: domain, error: error}) do
|
||||||
|
if error == nil do
|
||||||
|
error = "no api found"
|
||||||
|
end
|
||||||
|
|
||||||
Repo.insert!(%Crawl{
|
Repo.insert!(%Crawl{
|
||||||
instance_domain: domain,
|
instance_domain: domain,
|
||||||
error: error
|
error: error
|
||||||
|
|
|
@ -4,7 +4,7 @@ defmodule BackendWeb.InstanceController do
|
||||||
import Backend.Util
|
import Backend.Util
|
||||||
alias Backend.Api
|
alias Backend.Api
|
||||||
|
|
||||||
action_fallback BackendWeb.FallbackController
|
action_fallback(BackendWeb.FallbackController)
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
instances = Api.list_instances()
|
instances = Api.list_instances()
|
||||||
|
@ -13,7 +13,7 @@ defmodule BackendWeb.InstanceController do
|
||||||
|
|
||||||
def show(conn, %{"id" => domain}) do
|
def show(conn, %{"id" => domain}) do
|
||||||
instance = Api.get_instance!(domain)
|
instance = Api.get_instance!(domain)
|
||||||
last_crawl = get_last_crawl(domain)
|
last_crawl = get_last_successful_crawl(domain)
|
||||||
render(conn, "show.json", instance: instance, crawl: last_crawl)
|
render(conn, "show.json", instance: instance, crawl: last_crawl)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule BackendWeb.InstanceView do
|
defmodule BackendWeb.InstanceView do
|
||||||
use BackendWeb, :view
|
use BackendWeb, :view
|
||||||
alias BackendWeb.InstanceView
|
alias BackendWeb.InstanceView
|
||||||
|
import Backend.Util
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
def render("index.json", %{instances: instances}) do
|
def render("index.json", %{instances: instances}) do
|
||||||
|
@ -16,6 +17,8 @@ defmodule BackendWeb.InstanceView do
|
||||||
end
|
end
|
||||||
|
|
||||||
def render("instance_detail.json", %{instance: instance, crawl: crawl}) do
|
def render("instance_detail.json", %{instance: instance, crawl: crawl}) do
|
||||||
|
user_threshold = get_config(:personal_instance_threshold)
|
||||||
|
|
||||||
[status, last_updated] =
|
[status, last_updated] =
|
||||||
case crawl do
|
case crawl do
|
||||||
nil ->
|
nil ->
|
||||||
|
@ -28,17 +31,26 @@ defmodule BackendWeb.InstanceView do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
%{
|
cond do
|
||||||
name: instance.domain,
|
instance.user_count < user_threshold ->
|
||||||
description: instance.description,
|
%{
|
||||||
version: instance.version,
|
name: instance.domain,
|
||||||
userCount: instance.user_count,
|
status: "personal instance"
|
||||||
insularity: instance.insularity,
|
}
|
||||||
statusCount: instance.status_count,
|
|
||||||
domainCount: length(instance.peers),
|
true ->
|
||||||
peers: render_many(instance.peers, InstanceView, "instance.json"),
|
%{
|
||||||
lastUpdated: last_updated,
|
name: instance.domain,
|
||||||
status: status
|
description: instance.description,
|
||||||
}
|
version: instance.version,
|
||||||
|
userCount: instance.user_count,
|
||||||
|
insularity: instance.insularity,
|
||||||
|
statusCount: instance.status_count,
|
||||||
|
domainCount: length(instance.peers),
|
||||||
|
peers: render_many(instance.peers, InstanceView, "instance.json"),
|
||||||
|
lastUpdated: last_updated,
|
||||||
|
status: status
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"cross-fetch": "^3.0.4",
|
"cross-fetch": "^3.0.4",
|
||||||
"cytoscape": "^3.8.1",
|
"cytoscape": "^3.8.1",
|
||||||
"cytoscape-cola": "^2.3.0",
|
"cytoscape-popper": "^1.0.4",
|
||||||
"lodash": "^4.17.14",
|
"lodash": "^4.17.14",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"normalize.css": "^8.0.0",
|
"normalize.css": "^8.0.0",
|
||||||
|
@ -49,7 +49,8 @@
|
||||||
"redux": "^4.0.4",
|
"redux": "^4.0.4",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"sanitize-html": "^1.20.1",
|
"sanitize-html": "^1.20.1",
|
||||||
"styled-components": "^4.3.2"
|
"styled-components": "^4.3.2",
|
||||||
|
"tippy.js": "^4.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blueprintjs/tslint-config": "^1.8.1",
|
"@blueprintjs/tslint-config": "^1.8.1",
|
||||||
|
|
152
frontend/src/components/Cytoscape.tsx
Normal file
152
frontend/src/components/Cytoscape.tsx
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
import cytoscape from "cytoscape";
|
||||||
|
import popper from "cytoscape-popper";
|
||||||
|
import * as React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import tippy, { Instance } from "tippy.js";
|
||||||
|
import { DEFAULT_NODE_COLOR, SELECTED_NODE_COLOR } from "../constants";
|
||||||
|
|
||||||
|
const EntireWindowDiv = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 50px;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ICytoscapeProps {
|
||||||
|
elements: cytoscape.ElementsDefinition;
|
||||||
|
onInstanceSelect: (domain: string) => void;
|
||||||
|
onInstanceDeselect: () => void;
|
||||||
|
}
|
||||||
|
class Cytoscape extends React.Component<ICytoscapeProps> {
|
||||||
|
public cy?: cytoscape.Core;
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
const container = ReactDOM.findDOMNode(this);
|
||||||
|
cytoscape.use(popper as any);
|
||||||
|
this.cy = cytoscape({
|
||||||
|
autoungrabify: true,
|
||||||
|
container: container as any,
|
||||||
|
elements: this.props.elements,
|
||||||
|
hideEdgesOnViewport: true,
|
||||||
|
hideLabelsOnViewport: true,
|
||||||
|
layout: {
|
||||||
|
name: "preset"
|
||||||
|
},
|
||||||
|
maxZoom: 2,
|
||||||
|
minZoom: 0.03,
|
||||||
|
pixelRatio: 1.0,
|
||||||
|
selectionType: "single"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup node tooltip on hover
|
||||||
|
this.cy.nodes().forEach(n => {
|
||||||
|
const domain = n.data("id");
|
||||||
|
const ref = (n as any).popperRef();
|
||||||
|
const t = tippy(ref, {
|
||||||
|
animateFill: false,
|
||||||
|
animation: "fade",
|
||||||
|
content: domain,
|
||||||
|
duration: 100,
|
||||||
|
trigger: "manual"
|
||||||
|
});
|
||||||
|
n.on("mouseover", e => {
|
||||||
|
(t as Instance).show();
|
||||||
|
});
|
||||||
|
n.on("mouseout", e => {
|
||||||
|
(t as Instance).hide();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = this.cy.style() as any;
|
||||||
|
|
||||||
|
style
|
||||||
|
.clear()
|
||||||
|
.selector("node")
|
||||||
|
.style({
|
||||||
|
"background-color": DEFAULT_NODE_COLOR,
|
||||||
|
// The size from the backend is log_10(userCount), which from 10 <= userCount <= 1,000,000 gives us the range
|
||||||
|
// 1-6. We map this to the range of sizes we want.
|
||||||
|
// TODO: I should probably check that that the backend is actually using log_10 and not log_e, but it look
|
||||||
|
// quite good as it is, so...
|
||||||
|
height: "mapData(size, 1, 6, 20, 200)",
|
||||||
|
label: "data(id)",
|
||||||
|
width: "mapData(size, 1, 6, 20, 200)"
|
||||||
|
})
|
||||||
|
.selector("node:selected")
|
||||||
|
.style({
|
||||||
|
"background-color": SELECTED_NODE_COLOR
|
||||||
|
})
|
||||||
|
.selector("edge")
|
||||||
|
.style({
|
||||||
|
"curve-style": "haystack", // fast edges
|
||||||
|
"line-color": DEFAULT_NODE_COLOR,
|
||||||
|
width: "mapData(weight, 0, 0.5, 1, 20)"
|
||||||
|
})
|
||||||
|
.selector("node[label]")
|
||||||
|
.style({
|
||||||
|
color: DEFAULT_NODE_COLOR,
|
||||||
|
"font-size": 50,
|
||||||
|
"min-zoomed-font-size": 16
|
||||||
|
})
|
||||||
|
.selector(".hidden")
|
||||||
|
.style({
|
||||||
|
display: "none"
|
||||||
|
})
|
||||||
|
.selector(".thickEdge")
|
||||||
|
.style({
|
||||||
|
width: 2
|
||||||
|
})
|
||||||
|
.update();
|
||||||
|
|
||||||
|
this.cy.nodes().on("select", e => {
|
||||||
|
const instanceId = e.target.data("id");
|
||||||
|
if (instanceId) {
|
||||||
|
this.props.onInstanceSelect(instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const neighborhood = this.cy!.$id(instanceId).closedNeighborhood();
|
||||||
|
// Reset graph visibility
|
||||||
|
this.cy!.batch(() => {
|
||||||
|
this.cy!.nodes().removeClass("hidden");
|
||||||
|
this.cy!.edges().removeClass("thickEdge");
|
||||||
|
// Then hide everything except neighborhood
|
||||||
|
this.cy!.nodes()
|
||||||
|
.diff(neighborhood)
|
||||||
|
.left.addClass("hidden");
|
||||||
|
neighborhood.connectedEdges().addClass("thickEdge");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.cy.nodes().on("unselect", e => {
|
||||||
|
this.props.onInstanceDeselect();
|
||||||
|
this.cy!.batch(() => {
|
||||||
|
this.cy!.nodes().removeClass("hidden");
|
||||||
|
this.cy!.edges().removeClass("thickEdge");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.cy.on("click", e => {
|
||||||
|
// Clicking on the background should also deselect
|
||||||
|
const target = e.target;
|
||||||
|
if (!target) {
|
||||||
|
this.props.onInstanceDeselect();
|
||||||
|
}
|
||||||
|
this.cy!.batch(() => {
|
||||||
|
this.cy!.nodes().removeClass("hidden");
|
||||||
|
this.cy!.edges().removeClass("thickEdge");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
if (this.cy) {
|
||||||
|
this.cy.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return <EntireWindowDiv />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Cytoscape;
|
|
@ -1,218 +0,0 @@
|
||||||
import cytoscape from "cytoscape";
|
|
||||||
// import cola from "cytoscape-cola";
|
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import { DEFAULT_NODE_COLOR, SELECTED_NODE_COLOR } from "../constants";
|
|
||||||
import { selectAndLoadInstance } from "../redux/actions";
|
|
||||||
import { IAppState, IGraph } from "../redux/types";
|
|
||||||
import { ErrorState } from "./ErrorState";
|
|
||||||
// import { FloatingLayoutSelect } from "./FloatingLayoutSelect";
|
|
||||||
import { FloatingResetButton } from "./FloatingResetButton";
|
|
||||||
|
|
||||||
interface IGraphProps {
|
|
||||||
graph?: IGraph;
|
|
||||||
currentInstanceName: string | null;
|
|
||||||
selectAndLoadInstance: (name: string) => void;
|
|
||||||
}
|
|
||||||
interface IGraphState {
|
|
||||||
layoutAlgorithm: string;
|
|
||||||
isLayouting: boolean;
|
|
||||||
didError: boolean;
|
|
||||||
}
|
|
||||||
class GraphImpl extends React.Component<IGraphProps, IGraphState> {
|
|
||||||
private cy?: cytoscape.Core;
|
|
||||||
// private layout?: cytoscape.Layouts;
|
|
||||||
private cytoscapeDiv: React.RefObject<HTMLElement>;
|
|
||||||
|
|
||||||
public constructor(props: IGraphProps) {
|
|
||||||
super(props);
|
|
||||||
this.cytoscapeDiv = React.createRef();
|
|
||||||
this.state = { layoutAlgorithm: "cola", isLayouting: false, didError: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
if (this.state.didError) {
|
|
||||||
return <ErrorState />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FullDiv = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: 50px;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FullDiv id="cytoscape" ref={this.cytoscapeDiv as any} />
|
|
||||||
{/* <FloatingLayoutSelect
|
|
||||||
onItemSelect={this.handleLayoutSelect}
|
|
||||||
currentLayoutKey={this.state.layoutAlgorithm}
|
|
||||||
startLayout={this.startLayout}
|
|
||||||
stopLayout={this.stopLayout}
|
|
||||||
/> */}
|
|
||||||
<FloatingResetButton onClick={this.resetGraph} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount() {
|
|
||||||
let { graph } = this.props;
|
|
||||||
if (!graph) {
|
|
||||||
this.setState({ didError: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that all nodes have size & coordinates; otherwise the graph will look messed up
|
|
||||||
const lengthBeforeFilter = graph.nodes.length;
|
|
||||||
graph = { ...graph, nodes: graph.nodes.filter(n => n.data.size && n.position.x && n.position.y) };
|
|
||||||
if (graph.nodes.length !== lengthBeforeFilter) {
|
|
||||||
// tslint:disable-next-line:no-console
|
|
||||||
console.error(
|
|
||||||
"Some nodes were missing details: " +
|
|
||||||
graph.nodes.filter(n => !n.data.size || !n.position.x || !n.position.y).map(n => n.data.label)
|
|
||||||
);
|
|
||||||
this.setState({ didError: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// cytoscape.use(cola as any);
|
|
||||||
this.initGraph();
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidUpdate() {
|
|
||||||
this.initGraph();
|
|
||||||
}
|
|
||||||
|
|
||||||
// private handleLayoutSelect = (layout: string) => {
|
|
||||||
// this.setState({ layoutAlgorithm: layout });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// private startLayout = () => {
|
|
||||||
// if (!this.cy) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// const options = {
|
|
||||||
// cola: {
|
|
||||||
// animate: true,
|
|
||||||
// convergenceThreshold: 0.1,
|
|
||||||
// edgeLength: (edge: any) => 1 / edge.data("weight"),
|
|
||||||
// name: "cola"
|
|
||||||
// },
|
|
||||||
// cose: {
|
|
||||||
// animate: false,
|
|
||||||
// idealEdgeLength: (edge: any) => 1 / edge.data("weight"),
|
|
||||||
// name: "cose",
|
|
||||||
// numIter: 100
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// this.layout = this.cy.layout(options[this.state.layoutAlgorithm] as any);
|
|
||||||
// this.layout.run();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// private stopLayout = () => {
|
|
||||||
// if (!this.layout) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// this.layout.stop();
|
|
||||||
// };
|
|
||||||
|
|
||||||
private initGraph = () => {
|
|
||||||
const { graph } = this.props;
|
|
||||||
if (this.state.didError || !graph) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.cy = cytoscape({
|
|
||||||
autoungrabify: false,
|
|
||||||
container: this.cytoscapeDiv.current,
|
|
||||||
elements: {
|
|
||||||
edges: graph.edges.map(e => ({
|
|
||||||
...e,
|
|
||||||
data: {
|
|
||||||
...e.data,
|
|
||||||
weight: Math.min(Math.max(e.data.weight * 100, 2), 10)
|
|
||||||
},
|
|
||||||
selectable: false
|
|
||||||
})),
|
|
||||||
nodes: graph.nodes.map(n => ({
|
|
||||||
...n,
|
|
||||||
data: {
|
|
||||||
...n.data,
|
|
||||||
size: Math.min(Math.max(n.data.size * 10, 10), 80)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
name: "preset"
|
|
||||||
},
|
|
||||||
selectionType: "single",
|
|
||||||
style: [
|
|
||||||
{
|
|
||||||
selector: "node:selected",
|
|
||||||
style: {
|
|
||||||
"background-color": SELECTED_NODE_COLOR,
|
|
||||||
label: "data(id)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "node",
|
|
||||||
style: {
|
|
||||||
"background-color": DEFAULT_NODE_COLOR,
|
|
||||||
height: "data(size)",
|
|
||||||
label: "data(id)",
|
|
||||||
width: "data(size)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "edge",
|
|
||||||
style: {
|
|
||||||
width: "data(weight)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: "label",
|
|
||||||
style: {
|
|
||||||
color: DEFAULT_NODE_COLOR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
this.cy.nodes().on("select", e => {
|
|
||||||
const instanceId = e.target.data("id");
|
|
||||||
if (instanceId) {
|
|
||||||
// console.log(`selecting ${instanceId}`);
|
|
||||||
// console.log(`now selected: ${this.cy && this.cy.$(":selected")}`);
|
|
||||||
this.props.selectAndLoadInstance(instanceId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.cy.nodes().on("unselect", e => {
|
|
||||||
const instanceId = e.target.data("id");
|
|
||||||
if (instanceId) {
|
|
||||||
// console.log(`unselecting ${instanceId}`);
|
|
||||||
this.props.selectAndLoadInstance("");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private resetGraph = () => {
|
|
||||||
if (!this.cy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.cy.reset();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: IAppState) => ({
|
|
||||||
currentInstanceName: state.currentInstance.currentInstanceName,
|
|
||||||
graph: state.data.graph
|
|
||||||
});
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|
||||||
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any)
|
|
||||||
});
|
|
||||||
export const CytoscapeGraph = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(GraphImpl);
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { Button, H6, MenuItem } from "@blueprintjs/core";
|
|
||||||
import { IconNames } from "@blueprintjs/icons";
|
|
||||||
import { ItemRenderer, Select } from "@blueprintjs/select";
|
|
||||||
import * as React from "react";
|
|
||||||
import FloatingCard from "./FloatingCard";
|
|
||||||
|
|
||||||
interface ILayoutToDisplayName {
|
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
const layouts: ILayoutToDisplayName = {
|
|
||||||
cola: "COLA",
|
|
||||||
cose: "CoSE"
|
|
||||||
};
|
|
||||||
const LayoutSelect = Select.ofType<string>();
|
|
||||||
|
|
||||||
const LayoutItemRenderer: ItemRenderer<string> = (layout, { handleClick, modifiers }) => (
|
|
||||||
<MenuItem active={modifiers.active} key={layout} onClick={handleClick} text={layout} />
|
|
||||||
);
|
|
||||||
|
|
||||||
interface IFloatingLayoutSelectProps {
|
|
||||||
currentLayoutKey: string;
|
|
||||||
onItemSelect: (layout: string) => void;
|
|
||||||
startLayout: () => void;
|
|
||||||
stopLayout: () => void;
|
|
||||||
}
|
|
||||||
export const FloatingLayoutSelect: React.FC<IFloatingLayoutSelectProps> = ({
|
|
||||||
currentLayoutKey,
|
|
||||||
onItemSelect,
|
|
||||||
startLayout,
|
|
||||||
stopLayout
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<FloatingCard>
|
|
||||||
<H6>Layout</H6>
|
|
||||||
<LayoutSelect
|
|
||||||
items={Object.keys(layouts)}
|
|
||||||
itemRenderer={LayoutItemRenderer}
|
|
||||||
filterable={false}
|
|
||||||
onItemSelect={onItemSelect}
|
|
||||||
popoverProps={{ minimal: true }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon="film"
|
|
||||||
rightIcon="caret-down"
|
|
||||||
text={currentLayoutKey ? layouts[currentLayoutKey] : "(No selection)"}
|
|
||||||
/>
|
|
||||||
</LayoutSelect>
|
|
||||||
<br />
|
|
||||||
<Button icon={IconNames.PLAY} onClick={startLayout} />
|
|
||||||
<Button icon={IconNames.STOP} onClick={stopLayout} />
|
|
||||||
</FloatingCard>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,107 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Sigma, Filter, ForceAtlas2 } from "react-sigma";
|
|
||||||
|
|
||||||
import { selectAndLoadInstance } from "../redux/actions";
|
|
||||||
import { ErrorState } from "./ErrorState";
|
|
||||||
|
|
||||||
const STYLE = {
|
|
||||||
bottom: "0",
|
|
||||||
left: "0",
|
|
||||||
position: "absolute",
|
|
||||||
right: "0",
|
|
||||||
top: "50px"
|
|
||||||
};
|
|
||||||
const DEFAULT_NODE_COLOR = "#CED9E0";
|
|
||||||
const SELECTED_NODE_COLOR = "#48AFF0";
|
|
||||||
const SETTINGS = {
|
|
||||||
defaultEdgeColor: "#5C7080",
|
|
||||||
defaultLabelColor: "#F5F8FA",
|
|
||||||
defaultNodeColor: DEFAULT_NODE_COLOR,
|
|
||||||
drawEdges: true,
|
|
||||||
drawLabels: true,
|
|
||||||
edgeColor: "default",
|
|
||||||
labelColor: "default",
|
|
||||||
labelThreshold: 10,
|
|
||||||
maxEdgeSize: 1,
|
|
||||||
minEdgeSize: 0.3
|
|
||||||
};
|
|
||||||
|
|
||||||
class GraphImpl extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.sigmaComponent = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let graph = this.props.graph;
|
|
||||||
if (!graph) {
|
|
||||||
return <ErrorState />;
|
|
||||||
}
|
|
||||||
// Check that all nodes have size & coordinates; otherwise the graph will look messed up
|
|
||||||
const lengthBeforeFilter = graph.nodes.length;
|
|
||||||
graph = { ...graph, nodes: graph.nodes.filter(n => n.size && n.x && n.y) };
|
|
||||||
if (graph.nodes.length !== lengthBeforeFilter) {
|
|
||||||
// tslint:disable-next-line:no-console
|
|
||||||
console.error(
|
|
||||||
"Some nodes were missing details: " +
|
|
||||||
this.props.graph.nodes.filter(n => !n.size || !n.x || !n.y).map(n => n.label)
|
|
||||||
);
|
|
||||||
return <ErrorState />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Sigma
|
|
||||||
graph={graph}
|
|
||||||
renderer="webgl"
|
|
||||||
settings={SETTINGS}
|
|
||||||
style={STYLE}
|
|
||||||
onClickNode={this.onClickNode}
|
|
||||||
onClickStage={this.onClickStage}
|
|
||||||
ref={this.sigmaComponent}
|
|
||||||
>
|
|
||||||
<Filter neighborsOf={this.props.currentInstanceName} />
|
|
||||||
<ForceAtlas2 iterationsPerRender={1} timeout={10000} />
|
|
||||||
</Sigma>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
const sigma = this.sigmaComponent && this.sigmaComponent.current.sigma;
|
|
||||||
// Check if sigma exists s.t. nothing breaks if the graph didn't load (for whatever reason)
|
|
||||||
if (sigma) {
|
|
||||||
sigma.graph.nodes().map(this.colorNodes);
|
|
||||||
sigma.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickNode = e => {
|
|
||||||
this.props.selectAndLoadInstance(e.data.node.label);
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickStage = e => {
|
|
||||||
// Deselect the instance (unless this was a drag event)
|
|
||||||
if (!e.data.captor.isDragging) {
|
|
||||||
this.props.selectAndLoadInstance(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
colorNodes = n => {
|
|
||||||
if (this.props.currentInstanceName && n.id === this.props.currentInstanceName) {
|
|
||||||
n.color = SELECTED_NODE_COLOR;
|
|
||||||
} else {
|
|
||||||
n.color = DEFAULT_NODE_COLOR;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
currentInstanceName: state.currentInstance.currentInstanceName,
|
|
||||||
graph: state.data.graph
|
|
||||||
});
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
selectAndLoadInstance: instanceName => dispatch(selectAndLoadInstance(instanceName))
|
|
||||||
});
|
|
||||||
export const Graph = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(GraphImpl);
|
|
101
frontend/src/components/Graph.tsx
Normal file
101
frontend/src/components/Graph.tsx
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { get } from "lodash";
|
||||||
|
import * as React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import { selectAndLoadInstance } from "../redux/actions";
|
||||||
|
import { IAppState, IGraph } from "../redux/types";
|
||||||
|
import Cytoscape from "./Cytoscape";
|
||||||
|
import { ErrorState } from "./ErrorState";
|
||||||
|
import { FloatingResetButton } from "./FloatingResetButton";
|
||||||
|
|
||||||
|
interface IGraphProps {
|
||||||
|
graph?: IGraph;
|
||||||
|
currentInstanceName: string | null;
|
||||||
|
selectAndLoadInstance: (name: string) => void;
|
||||||
|
}
|
||||||
|
class GraphImpl extends React.Component<IGraphProps> {
|
||||||
|
private cytoscapeComponent: React.RefObject<Cytoscape>;
|
||||||
|
|
||||||
|
public constructor(props: IGraphProps) {
|
||||||
|
super(props);
|
||||||
|
this.cytoscapeComponent = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (!this.props.graph) {
|
||||||
|
return <ErrorState />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Cytoscape
|
||||||
|
elements={this.props.graph}
|
||||||
|
onInstanceSelect={this.onInstanceSelect}
|
||||||
|
onInstanceDeselect={this.onInstanceDeselect}
|
||||||
|
ref={this.cytoscapeComponent}
|
||||||
|
/>
|
||||||
|
<FloatingResetButton onClick={this.resetGraphPosition} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: IGraphProps) {
|
||||||
|
const { currentInstanceName } = this.props;
|
||||||
|
if (prevProps.currentInstanceName !== currentInstanceName) {
|
||||||
|
const cy = this.getCytoscape();
|
||||||
|
cy.$id(prevProps.currentInstanceName).unselect();
|
||||||
|
if (currentInstanceName) {
|
||||||
|
// Select instance
|
||||||
|
cy.$id(`${currentInstanceName}`).select();
|
||||||
|
// Center it
|
||||||
|
const selected = cy.$id(currentInstanceName);
|
||||||
|
cy.center(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetGraphPosition = () => {
|
||||||
|
const cy = this.getCytoscape();
|
||||||
|
const { currentInstanceName } = this.props;
|
||||||
|
if (currentInstanceName) {
|
||||||
|
cy.zoom({
|
||||||
|
level: 0.2,
|
||||||
|
position: cy.$id(currentInstanceName).position()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cy.zoom({
|
||||||
|
level: 0.2,
|
||||||
|
position: { x: 0, y: 0 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onInstanceSelect = (domain: string) => {
|
||||||
|
this.props.selectAndLoadInstance(domain);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onInstanceDeselect = () => {
|
||||||
|
this.props.selectAndLoadInstance("");
|
||||||
|
};
|
||||||
|
|
||||||
|
private getCytoscape = () => {
|
||||||
|
const cy = get(this.cytoscapeComponent, "current.cy");
|
||||||
|
if (!cy) {
|
||||||
|
throw new Error("Expected cytoscape component but did not find one.");
|
||||||
|
}
|
||||||
|
return cy;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const mapStateToProps = (state: IAppState) => ({
|
||||||
|
currentInstanceName: state.currentInstance.currentInstanceName,
|
||||||
|
graph: state.data.graph
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
|
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any)
|
||||||
|
});
|
||||||
|
const Graph = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(GraphImpl);
|
||||||
|
export default Graph;
|
|
@ -5,7 +5,6 @@ import * as React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import sanitize from "sanitize-html";
|
import sanitize from "sanitize-html";
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AnchorButton,
|
AnchorButton,
|
||||||
|
@ -30,6 +29,7 @@ import { IconNames } from "@blueprintjs/icons";
|
||||||
|
|
||||||
import { selectAndLoadInstance } from "../redux/actions";
|
import { selectAndLoadInstance } from "../redux/actions";
|
||||||
import { IAppState, IGraph, IInstanceDetails } from "../redux/types";
|
import { IAppState, IGraph, IInstanceDetails } from "../redux/types";
|
||||||
|
import FullDiv from "./atoms/FullDiv";
|
||||||
import { ErrorState } from "./ErrorState";
|
import { ErrorState } from "./ErrorState";
|
||||||
|
|
||||||
interface ISidebarProps {
|
interface ISidebarProps {
|
||||||
|
@ -104,22 +104,43 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderSidebarContents = () => {
|
private renderSidebarContents = () => {
|
||||||
|
let content;
|
||||||
if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors) {
|
if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors) {
|
||||||
return this.renderLoadingState();
|
content = this.renderLoadingState();
|
||||||
} else if (!this.props.instanceDetails) {
|
} else if (!this.props.instanceDetails) {
|
||||||
return this.renderEmptyState();
|
return this.renderEmptyState();
|
||||||
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personalinstance") > -1) {
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) {
|
||||||
return this.renderPersonalInstanceErrorState();
|
content = this.renderPersonalInstanceErrorState();
|
||||||
} else if (this.props.instanceDetails.status !== "success") {
|
} else if (this.props.instanceDetails.status !== "success") {
|
||||||
return this.renderMissingDataState();
|
content = this.renderMissingDataState();
|
||||||
} else if (this.props.instanceLoadError) {
|
} else if (this.props.instanceLoadError) {
|
||||||
return <ErrorState />;
|
return (content = <ErrorState />);
|
||||||
|
} else {
|
||||||
|
content = this.renderTabs();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<FullDiv>
|
||||||
{this.renderHeading()}
|
{this.renderHeading()}
|
||||||
|
{content}
|
||||||
|
</FullDiv>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderTabs = () => {
|
||||||
|
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
||||||
|
|
||||||
|
const insularCallout = hasNeighbors ? (
|
||||||
|
undefined
|
||||||
|
) : (
|
||||||
|
<Callout icon={IconNames.INFO_SIGN} title="Insular instance">
|
||||||
|
<p>This instance doesn't have any neighbors that we know of, so it's hidden from the graph.</p>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{insularCallout}
|
||||||
<Tabs>
|
<Tabs>
|
||||||
{this.props.instanceDetails.description && (
|
{this.props.instanceDetails!.description && (
|
||||||
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
||||||
)}
|
)}
|
||||||
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
||||||
|
@ -138,7 +159,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
private renderHeading = () => {
|
private renderHeading = () => {
|
||||||
let content: JSX.Element;
|
let content: JSX.Element;
|
||||||
if (!this.props.instanceName) {
|
if (!this.props.instanceName) {
|
||||||
content = <span>{"No instance selected"}</span>;
|
return;
|
||||||
} else {
|
} else {
|
||||||
content = (
|
content = (
|
||||||
<span>
|
<span>
|
||||||
|
@ -150,19 +171,9 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
|
||||||
|
|
||||||
const insularCallout = hasNeighbors ? (
|
|
||||||
undefined
|
|
||||||
) : (
|
|
||||||
<Callout icon={IconNames.INFO_SIGN} title="Insular instance">
|
|
||||||
<p>This instance doesn't have any neighbors that we know of, so it's hidden from the graph.</p>
|
|
||||||
</Callout>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<H2>{content}</H2>
|
<H2>{content}</H2>
|
||||||
{insularCallout}
|
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -346,8 +357,8 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
title="No data"
|
title="No data"
|
||||||
description="This instance has fewer than 10 users. It was not crawled in order to protect their privacy, but if it's your instance you can opt in."
|
description="This instance has fewer than 10 users. It was not crawled in order to protect their privacy, but if it's your instance you can opt in."
|
||||||
action={
|
action={
|
||||||
<AnchorButton icon={IconNames.CONFIRM} href="https://cursed.technology/@tao" target="_blank">
|
<AnchorButton icon={IconNames.CONFIRM} href="https://cursed.technology/@fediversespace" target="_blank">
|
||||||
Message @tao to opt in
|
Message @fediversespace to opt in
|
||||||
</AnchorButton>
|
</AnchorButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -355,12 +366,8 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderMissingDataState = () => {
|
private renderMissingDataState = () => {
|
||||||
const FillDiv = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
return (
|
return (
|
||||||
<FillDiv>
|
<FullDiv>
|
||||||
<NonIdealState
|
<NonIdealState
|
||||||
icon={IconNames.ERROR}
|
icon={IconNames.ERROR}
|
||||||
title="No data"
|
title="No data"
|
||||||
|
@ -369,7 +376,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
||||||
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
|
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
|
||||||
{this.props.instanceDetails && this.props.instanceDetails.status}
|
{this.props.instanceDetails && this.props.instanceDetails.status}
|
||||||
</span>
|
</span>
|
||||||
</FillDiv>
|
</FullDiv>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
6
frontend/src/components/atoms/FullDiv.tsx
Normal file
6
frontend/src/components/atoms/FullDiv.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export default styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`;
|
|
@ -6,8 +6,8 @@ import { NonIdealState, Spinner } from "@blueprintjs/core";
|
||||||
|
|
||||||
import { fetchGraph, fetchInstances } from "../../redux/actions";
|
import { fetchGraph, fetchInstances } from "../../redux/actions";
|
||||||
import { IAppState, IGraph, IInstance } from "../../redux/types";
|
import { IAppState, IGraph, IInstance } from "../../redux/types";
|
||||||
import { CytoscapeGraph } from "../CytoscapeGraph";
|
|
||||||
import { ErrorState } from "../ErrorState";
|
import { ErrorState } from "../ErrorState";
|
||||||
|
import Graph from "../Graph";
|
||||||
import { Sidebar } from "../Sidebar";
|
import { Sidebar } from "../Sidebar";
|
||||||
|
|
||||||
interface IGraphScreenProps {
|
interface IGraphScreenProps {
|
||||||
|
@ -48,7 +48,7 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private graphState = () => {
|
private graphState = () => {
|
||||||
const content = this.props.graphLoadError ? <ErrorState /> : <CytoscapeGraph />;
|
const content = this.props.graphLoadError ? <ErrorState /> : <Graph />;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
3
frontend/src/typings/cytoscape-cola.d.ts
vendored
3
frontend/src/typings/cytoscape-cola.d.ts
vendored
|
@ -1,3 +0,0 @@
|
||||||
declare module "cytoscape-cola" {
|
|
||||||
const prototype: {};
|
|
||||||
}
|
|
3
frontend/src/typings/cytoscape-popper.d.ts
vendored
Normal file
3
frontend/src/typings/cytoscape-popper.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module "cytoscape-popper" {
|
||||||
|
const prototype: {};
|
||||||
|
}
|
|
@ -4,7 +4,10 @@
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"lib": ["esnext", "dom"],
|
"lib": [
|
||||||
|
"es2015",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
@ -20,11 +23,19 @@
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"typeRoots": ["./node_modules/@types", "./src/typings"],
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./src/typings"
|
||||||
|
],
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "build"],
|
"exclude": [
|
||||||
"include": ["src"]
|
"node_modules",
|
||||||
|
"build"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -3413,12 +3413,12 @@ cyclist@~0.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
||||||
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
|
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
|
||||||
|
|
||||||
cytoscape-cola@^2.3.0:
|
cytoscape-popper@^1.0.4:
|
||||||
version "2.3.0"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/cytoscape-cola/-/cytoscape-cola-2.3.0.tgz#24104eaab46e0a88dfeedb02199610450525208d"
|
resolved "https://registry.yarnpkg.com/cytoscape-popper/-/cytoscape-popper-1.0.4.tgz#13388223d6d84f66f121643fb1213c26d1c693f8"
|
||||||
integrity sha512-xblxlCH8JXGLdH6XMUBJY3xBUJuL1rLy8bLMGvqkvoPHSbBfV+/klMWoqwervVKWOmFHPwUdihtxy8stG4RM5g==
|
integrity sha512-rKzWiuhGDw0+1UwKK+2eXkZVSNR8TxAXp3k0QOOkH0GW1w55mnCDUVUKZuQyXqHtcZie+TnW1FJ5ml2Uc/lkOg==
|
||||||
dependencies:
|
dependencies:
|
||||||
webcola "^3.3.6"
|
popper.js "^1.0.0"
|
||||||
|
|
||||||
cytoscape@^3.8.1:
|
cytoscape@^3.8.1:
|
||||||
version "3.8.1"
|
version "3.8.1"
|
||||||
|
@ -3428,29 +3428,6 @@ cytoscape@^3.8.1:
|
||||||
heap "^0.2.6"
|
heap "^0.2.6"
|
||||||
lodash.debounce "^4.0.8"
|
lodash.debounce "^4.0.8"
|
||||||
|
|
||||||
d3-dispatch@1, d3-dispatch@^1.0.3:
|
|
||||||
version "1.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.5.tgz#e25c10a186517cd6c82dd19ea018f07e01e39015"
|
|
||||||
integrity sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==
|
|
||||||
|
|
||||||
d3-drag@^1.0.4:
|
|
||||||
version "1.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.3.tgz#46e206ad863ec465d88c588098a1df444cd33c64"
|
|
||||||
integrity sha512-8S3HWCAg+ilzjJsNtWW1Mutl74Nmzhb9yU6igspilaJzeZVFktmY6oO9xOh5TDk+BM2KrNFjttZNoJJmDnkjkg==
|
|
||||||
dependencies:
|
|
||||||
d3-dispatch "1"
|
|
||||||
d3-selection "1"
|
|
||||||
|
|
||||||
d3-selection@1:
|
|
||||||
version "1.4.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.0.tgz#ab9ac1e664cf967ebf1b479cc07e28ce9908c474"
|
|
||||||
integrity sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==
|
|
||||||
|
|
||||||
d3-timer@^1.0.5:
|
|
||||||
version "1.0.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.9.tgz#f7bb8c0d597d792ff7131e1c24a36dd471a471ba"
|
|
||||||
integrity sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==
|
|
||||||
|
|
||||||
damerau-levenshtein@^1.0.4:
|
damerau-levenshtein@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
|
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
|
||||||
|
@ -7920,7 +7897,7 @@ pnp-webpack-plugin@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ts-pnp "^1.0.0"
|
ts-pnp "^1.0.0"
|
||||||
|
|
||||||
popper.js@^1.14.1, popper.js@^1.14.4:
|
popper.js@^1.0.0, popper.js@^1.14.1, popper.js@^1.14.4, popper.js@^1.14.7:
|
||||||
version "1.15.0"
|
version "1.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
|
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
|
||||||
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
|
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
|
||||||
|
@ -10347,6 +10324,13 @@ tiny-warning@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||||
|
|
||||||
|
tippy.js@^4.3.4:
|
||||||
|
version "4.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-4.3.4.tgz#9a91fd5ce8c401f181b7adaa6b2c27f3d105f3ba"
|
||||||
|
integrity sha512-O2ukxHOJTLVYZ/TfHjNd8WgAWoefX9uk5QiWRdHfX2PR2lBpUU4BJQLl7U2Ykc8K7o16gTeHEElpuRfgD5b0aA==
|
||||||
|
dependencies:
|
||||||
|
popper.js "^1.14.7"
|
||||||
|
|
||||||
tmp@^0.0.33:
|
tmp@^0.0.33:
|
||||||
version "0.0.33"
|
version "0.0.33"
|
||||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||||
|
@ -10930,15 +10914,6 @@ web-namespaces@^1.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.2.tgz#c8dc267ab639505276bae19e129dbd6ae72b22b4"
|
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.2.tgz#c8dc267ab639505276bae19e129dbd6ae72b22b4"
|
||||||
integrity sha512-II+n2ms4mPxK+RnIxRPOw3zwF2jRscdJIUE9BfkKHm4FYEg9+biIoTMnaZF5MpemE3T+VhMLrhbyD4ilkPCSbg==
|
integrity sha512-II+n2ms4mPxK+RnIxRPOw3zwF2jRscdJIUE9BfkKHm4FYEg9+biIoTMnaZF5MpemE3T+VhMLrhbyD4ilkPCSbg==
|
||||||
|
|
||||||
webcola@^3.3.6:
|
|
||||||
version "3.3.9"
|
|
||||||
resolved "https://registry.yarnpkg.com/webcola/-/webcola-3.3.9.tgz#a54cae07478f1b25968d280b0d4a369c0b4352b3"
|
|
||||||
integrity sha512-hjE23yiRU+7AGajxuDdUDW8txyMVgXHCW71erA0UVKYx0lruqs1o4QFQ0OSpSdNS6wlAwgk0IPxhuEiQq1MXfQ==
|
|
||||||
dependencies:
|
|
||||||
d3-dispatch "^1.0.3"
|
|
||||||
d3-drag "^1.0.4"
|
|
||||||
d3-timer "^1.0.5"
|
|
||||||
|
|
||||||
webidl-conversions@^4.0.2:
|
webidl-conversions@^4.0.2:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||||
|
|
|
@ -87,7 +87,7 @@ public class GraphBuilder {
|
||||||
importController.process(container, new DefaultProcessor(), workspace);
|
importController.process(container, new DefaultProcessor(), workspace);
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
AutoLayout autoLayout = new AutoLayout(1, TimeUnit.MINUTES);
|
AutoLayout autoLayout = new AutoLayout(2, TimeUnit.MINUTES);
|
||||||
autoLayout.setGraphModel(graphModel);
|
autoLayout.setGraphModel(graphModel);
|
||||||
// YifanHuLayout firstLayout = new YifanHuLayout(null, new
|
// YifanHuLayout firstLayout = new YifanHuLayout(null, new
|
||||||
// StepDisplacement(1f));
|
// StepDisplacement(1f));
|
||||||
|
|
Loading…
Reference in a new issue