experimental mobile support
This commit is contained in:
parent
1ff7cd7290
commit
e490b30ebb
|
@ -21,9 +21,11 @@ defmodule Backend.Api do
|
||||||
* 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
|
* the user count is > the threshold
|
||||||
* have x and y coordinates
|
* have x and y coordinates
|
||||||
|
|
||||||
|
If `domain` is passed, then this function only returns nodes that are neighbors of that instance.
|
||||||
"""
|
"""
|
||||||
@spec list_nodes() :: [Instance.t()]
|
@spec list_nodes() :: [Instance.t()]
|
||||||
def list_nodes() do
|
def list_nodes(domain \\ nil) do
|
||||||
user_threshold = get_config(:personal_instance_threshold)
|
user_threshold = get_config(:personal_instance_threshold)
|
||||||
|
|
||||||
Instance
|
Instance
|
||||||
|
@ -32,17 +34,38 @@ defmodule Backend.Api do
|
||||||
not is_nil(i.x) and not is_nil(i.y) and not is_nil(i.user_count) and
|
not is_nil(i.x) and not is_nil(i.y) and not is_nil(i.user_count) and
|
||||||
i.user_count >= ^user_threshold
|
i.user_count >= ^user_threshold
|
||||||
)
|
)
|
||||||
|
|> maybe_filter_nodes_to_neighborhood(domain)
|
||||||
|> select([c], [:domain, :user_count, :x, :y])
|
|> select([c], [:domain, :user_count, :x, :y])
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# if we're getting the sub-graph around a given domain, only return neighbors.
|
||||||
|
defp maybe_filter_nodes_to_neighborhood(query, domain) do
|
||||||
|
case domain do
|
||||||
|
nil ->
|
||||||
|
query
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
query
|
||||||
|
|> join(:inner, [i], outgoing_edges in Edge, on: outgoing_edges.source_domain == i.domain)
|
||||||
|
|> join(:inner, [i], incoming_edges in Edge, on: incoming_edges.target_domain == i.domain)
|
||||||
|
|> where(
|
||||||
|
[i, outgoing_edges, incoming_edges],
|
||||||
|
outgoing_edges.target_domain == ^domain or incoming_edges.source_domain == ^domain or
|
||||||
|
i.domain == ^domain
|
||||||
|
)
|
||||||
|
|> distinct(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec list_edges() :: [Edge.t()]
|
@spec list_edges() :: [Edge.t()]
|
||||||
def list_edges() do
|
def list_edges(domain \\ nil) do
|
||||||
user_threshold = get_config(:personal_instance_threshold)
|
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)
|
||||||
|
|> maybe_filter_edges_to_neighborhood(domain)
|
||||||
|> select([e], [:id, :source_domain, :target_domain, :weight])
|
|> select([e], [:id, :source_domain, :target_domain, :weight])
|
||||||
|> where(
|
|> where(
|
||||||
[e, i1, i2],
|
[e, i1, i2],
|
||||||
|
@ -53,6 +76,28 @@ defmodule Backend.Api do
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_filter_edges_to_neighborhood(query, domain) do
|
||||||
|
case domain do
|
||||||
|
nil ->
|
||||||
|
query
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# we want all edges in the neighborhood -- not just edges connected to `domain`
|
||||||
|
query
|
||||||
|
|> join(:inner, [e], neighbor_edges in Edge,
|
||||||
|
on:
|
||||||
|
neighbor_edges.source_domain == e.target_domain or
|
||||||
|
neighbor_edges.target_domain == e.source_domain
|
||||||
|
)
|
||||||
|
|> where(
|
||||||
|
[e, i1, i2, neighbor_edges],
|
||||||
|
e.source_domain == ^domain or e.target_domain == ^domain or
|
||||||
|
neighbor_edges.source_domain == ^domain or neighbor_edges.target_domain == ^domain
|
||||||
|
)
|
||||||
|
|> distinct(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def search_instances(query, cursor_after \\ nil) do
|
def search_instances(query, cursor_after \\ nil) do
|
||||||
ilike_query = "%#{query}%"
|
ilike_query = "%#{query}%"
|
||||||
|
|
||||||
|
|
|
@ -10,4 +10,10 @@ defmodule BackendWeb.GraphController do
|
||||||
edges = Api.list_edges()
|
edges = Api.list_edges()
|
||||||
render(conn, "index.json", nodes: nodes, edges: edges)
|
render(conn, "index.json", nodes: nodes, edges: edges)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show(conn, %{"id" => domain}) do
|
||||||
|
nodes = Api.list_nodes(domain)
|
||||||
|
edges = Api.list_edges(domain)
|
||||||
|
render(conn, "index.json", nodes: nodes, edges: edges)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ defmodule BackendWeb.Router do
|
||||||
pipe_through(:api)
|
pipe_through(:api)
|
||||||
|
|
||||||
resources("/instances", InstanceController, only: [:index, :show])
|
resources("/instances", InstanceController, only: [:index, :show])
|
||||||
resources("/graph", GraphController, only: [:index])
|
resources("/graph", GraphController, only: [:index, :show])
|
||||||
resources("/search", SearchController, only: [:index])
|
resources("/search", SearchController, only: [:index])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,73 +1,21 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { Button, Classes, Dialog } from "@blueprintjs/core";
|
import { Classes } from "@blueprintjs/core";
|
||||||
import { IconNames } from "@blueprintjs/icons";
|
|
||||||
|
|
||||||
import { ConnectedRouter } from "connected-react-router";
|
import { ConnectedRouter } from "connected-react-router";
|
||||||
import { Route } from "react-router-dom";
|
import { Route } from "react-router-dom";
|
||||||
import { Nav } from "./components/organisms/";
|
import { Nav } from "./components/organisms/";
|
||||||
import { AboutScreen, GraphScreen } from "./components/screens/";
|
import { AboutScreen, GraphScreen } from "./components/screens/";
|
||||||
import { DESKTOP_WIDTH_THRESHOLD } from "./constants";
|
|
||||||
import { history } from "./index";
|
import { history } from "./index";
|
||||||
|
|
||||||
interface IAppLocalState {
|
const AppRouter: React.FC = () => (
|
||||||
mobileDialogOpen: boolean;
|
<ConnectedRouter history={history}>
|
||||||
}
|
<div className={`${Classes.DARK} App`}>
|
||||||
export class AppRouter extends React.Component<{}, IAppLocalState> {
|
<Nav />
|
||||||
constructor(props: {}) {
|
<Route path="/about" component={AboutScreen} />
|
||||||
super(props);
|
{/* We always want the GraphScreen to be rendered (since un- and re-mounting it is expensive */}
|
||||||
this.state = { mobileDialogOpen: false };
|
<GraphScreen />
|
||||||
}
|
</div>
|
||||||
|
</ConnectedRouter>
|
||||||
public render() {
|
);
|
||||||
return (
|
export default AppRouter;
|
||||||
<ConnectedRouter history={history}>
|
|
||||||
<div className={`${Classes.DARK} App`}>
|
|
||||||
<Nav />
|
|
||||||
<Route path="/about" component={AboutScreen} />
|
|
||||||
{/* We always want the GraphScreen to be rendered (since un- and re-mounting it is expensive */}
|
|
||||||
<GraphScreen />
|
|
||||||
{this.renderMobileDialog()}
|
|
||||||
</div>
|
|
||||||
</ConnectedRouter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount() {
|
|
||||||
if (window.innerWidth < DESKTOP_WIDTH_THRESHOLD) {
|
|
||||||
this.handleMobileDialogOpen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderMobileDialog = () => {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
icon={IconNames.DESKTOP}
|
|
||||||
title="Desktop-optimized site"
|
|
||||||
onClose={this.handleMobileDialogClose}
|
|
||||||
isOpen={this.state.mobileDialogOpen}
|
|
||||||
className={Classes.DARK + " fediverse-about-dialog"}
|
|
||||||
>
|
|
||||||
<div className={Classes.DIALOG_BODY}>
|
|
||||||
<p className={Classes.RUNNING_TEXT}>
|
|
||||||
fediverse.space is optimized for desktop computers. Feel free to check it out on your phone (ideally in
|
|
||||||
landscape mode) but for best results, open it on a computer.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
|
||||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
|
||||||
<Button icon={IconNames.THUMBS_UP} text="OK!" onClick={this.handleMobileDialogClose} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleMobileDialogOpen = () => {
|
|
||||||
this.setState({ mobileDialogOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleMobileDialogClose = () => {
|
|
||||||
this.setState({ mobileDialogOpen: false });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,8 +13,8 @@ const CytoscapeContainer = styled.div`
|
||||||
interface ICytoscapeProps {
|
interface ICytoscapeProps {
|
||||||
currentNodeId: string | null;
|
currentNodeId: string | null;
|
||||||
elements: cytoscape.ElementsDefinition;
|
elements: cytoscape.ElementsDefinition;
|
||||||
navigateToInstancePath: (domain: string) => void;
|
navigateToInstancePath?: (domain: string) => void;
|
||||||
navigateToRoot: () => void;
|
navigateToRoot?: () => void;
|
||||||
}
|
}
|
||||||
class Cytoscape extends React.Component<ICytoscapeProps> {
|
class Cytoscape extends React.Component<ICytoscapeProps> {
|
||||||
private cy?: cytoscape.Core;
|
private cy?: cytoscape.Core;
|
||||||
|
@ -105,7 +105,9 @@ class Cytoscape extends React.Component<ICytoscapeProps> {
|
||||||
this.cy.nodes().on("select", e => {
|
this.cy.nodes().on("select", e => {
|
||||||
const instanceId = e.target.data("id");
|
const instanceId = e.target.data("id");
|
||||||
if (instanceId && instanceId !== this.props.currentNodeId) {
|
if (instanceId && instanceId !== this.props.currentNodeId) {
|
||||||
this.props.navigateToInstancePath(instanceId);
|
if (this.props.navigateToInstancePath) {
|
||||||
|
this.props.navigateToInstancePath(instanceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const neighborhood = this.cy!.$id(instanceId).closedNeighborhood();
|
const neighborhood = this.cy!.$id(instanceId).closedNeighborhood();
|
||||||
|
@ -130,8 +132,10 @@ class Cytoscape extends React.Component<ICytoscapeProps> {
|
||||||
// Clicking on the background should also deselect
|
// Clicking on the background should also deselect
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if (!target || target === this.cy || target.isEdge()) {
|
if (!target || target === this.cy || target.isEdge()) {
|
||||||
// Go to the URL "/"
|
if (this.props.navigateToRoot) {
|
||||||
this.props.navigateToRoot();
|
// Go to the URL "/"
|
||||||
|
this.props.navigateToRoot();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { InstanceScreen, SearchScreen } from ".";
|
||||||
import { INSTANCE_DOMAIN_PATH } from "../../constants";
|
import { INSTANCE_DOMAIN_PATH } from "../../constants";
|
||||||
import { loadInstance } from "../../redux/actions";
|
import { loadInstance } from "../../redux/actions";
|
||||||
import { IAppState } from "../../redux/types";
|
import { IAppState } from "../../redux/types";
|
||||||
import { domainMatchSelector } from "../../util";
|
import { domainMatchSelector, isSmallScreen } from "../../util";
|
||||||
import { Graph, SidebarContainer } from "../organisms/";
|
import { Graph, SidebarContainer } from "../organisms/";
|
||||||
|
|
||||||
const GraphContainer = styled.div`
|
const GraphContainer = styled.div`
|
||||||
|
@ -50,7 +50,8 @@ class GraphScreenImpl extends React.Component<IGraphScreenProps> {
|
||||||
private renderRoutes = ({ location }: RouteComponentProps) => (
|
private renderRoutes = ({ location }: RouteComponentProps) => (
|
||||||
<FullDiv>
|
<FullDiv>
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph />
|
{/* Smaller screens never load the entire graph. Instead, `InstanceScreen` shows only the neighborhood. */}
|
||||||
|
{isSmallScreen || <Graph />}
|
||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={INSTANCE_DOMAIN_PATH} component={InstanceScreen} />
|
<Route path={INSTANCE_DOMAIN_PATH} component={InstanceScreen} />
|
||||||
|
|
|
@ -29,8 +29,8 @@ import { Link } from "react-router-dom";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { IAppState, IGraph, IInstanceDetails } from "../../redux/types";
|
import { IAppState, IGraph, IInstanceDetails } from "../../redux/types";
|
||||||
import { domainMatchSelector } from "../../util";
|
import { domainMatchSelector, getFromApi, isSmallScreen } from "../../util";
|
||||||
import { ErrorState } from "../molecules/";
|
import { Cytoscape, ErrorState } from "../molecules/";
|
||||||
|
|
||||||
const InstanceScreenContainer = styled.div`
|
const InstanceScreenContainer = styled.div`
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
|
@ -64,6 +64,10 @@ const StyledLinkToFdNetwork = styled.div`
|
||||||
const StyledTabs = styled(Tabs)`
|
const StyledTabs = styled(Tabs)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
const StyledGraphContainer = styled.div`
|
||||||
|
height: 50%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
`;
|
||||||
interface IInstanceScreenProps {
|
interface IInstanceScreenProps {
|
||||||
graph?: IGraph;
|
graph?: IGraph;
|
||||||
instanceName: string | null;
|
instanceName: string | null;
|
||||||
|
@ -71,31 +75,34 @@ interface IInstanceScreenProps {
|
||||||
instanceDetails: IInstanceDetails | null;
|
instanceDetails: IInstanceDetails | null;
|
||||||
isLoadingInstanceDetails: boolean;
|
isLoadingInstanceDetails: boolean;
|
||||||
navigateToRoot: () => void;
|
navigateToRoot: () => void;
|
||||||
|
navigateToInstance: (domain: string) => void;
|
||||||
}
|
}
|
||||||
interface IInstanceScreenState {
|
interface IInstanceScreenState {
|
||||||
neighbors?: string[];
|
neighbors?: string[];
|
||||||
isProcessingNeighbors: boolean;
|
isProcessingNeighbors: boolean;
|
||||||
|
// Local (neighborhood) graph. Used only on small screens (mobile devices).
|
||||||
|
isLoadingLocalGraph: boolean;
|
||||||
|
localGraph?: IGraph;
|
||||||
|
localGraphLoadError?: boolean;
|
||||||
}
|
}
|
||||||
class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInstanceScreenState> {
|
class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInstanceScreenState> {
|
||||||
public constructor(props: IInstanceScreenProps) {
|
public constructor(props: IInstanceScreenProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { isProcessingNeighbors: false };
|
this.state = { isProcessingNeighbors: false, isLoadingLocalGraph: false, localGraphLoadError: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
let content;
|
let content;
|
||||||
if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors) {
|
if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors || this.state.isLoadingLocalGraph) {
|
||||||
content = this.renderLoadingState();
|
content = this.renderLoadingState();
|
||||||
} else if (!this.props.instanceDetails) {
|
} else if (this.props.instanceLoadError || this.state.localGraphLoadError || !this.props.instanceDetails) {
|
||||||
return this.renderEmptyState();
|
return (content = <ErrorState />);
|
||||||
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) {
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) {
|
||||||
content = this.renderPersonalInstanceErrorState();
|
content = this.renderPersonalInstanceErrorState();
|
||||||
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("robots.txt") > -1) {
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("robots.txt") > -1) {
|
||||||
content = this.renderRobotsTxtState();
|
content = this.renderRobotsTxtState();
|
||||||
} else if (this.props.instanceDetails.status !== "success") {
|
} else if (this.props.instanceDetails.status !== "success") {
|
||||||
content = this.renderMissingDataState();
|
content = this.renderMissingDataState();
|
||||||
} else if (this.props.instanceLoadError) {
|
|
||||||
return (content = <ErrorState />);
|
|
||||||
} else {
|
} else {
|
||||||
content = this.renderTabs();
|
content = this.renderTabs();
|
||||||
}
|
}
|
||||||
|
@ -115,24 +122,29 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
this.loadLocalGraphOnSmallScreen();
|
||||||
this.processEdgesToFindNeighbors();
|
this.processEdgesToFindNeighbors();
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IInstanceScreenProps) {
|
public componentDidUpdate(prevProps: IInstanceScreenProps, prevState: IInstanceScreenState) {
|
||||||
const isNewInstance = prevProps.instanceName !== this.props.instanceName;
|
const isNewInstance = prevProps.instanceName !== this.props.instanceName;
|
||||||
const receivedNewEdges = !!this.props.graph && !this.state.isProcessingNeighbors && !this.state.neighbors;
|
const receivedNewEdges = !!this.props.graph && !this.state.isProcessingNeighbors && !this.state.neighbors;
|
||||||
if (isNewInstance || receivedNewEdges) {
|
const receivedNewLocalGraph = !!this.state.localGraph && !prevState.localGraph;
|
||||||
|
if (isNewInstance || receivedNewEdges || receivedNewLocalGraph) {
|
||||||
this.processEdgesToFindNeighbors();
|
this.processEdgesToFindNeighbors();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processEdgesToFindNeighbors = () => {
|
private processEdgesToFindNeighbors = () => {
|
||||||
const { graph, instanceName } = this.props;
|
const { graph, instanceName } = this.props;
|
||||||
if (!graph || !instanceName) {
|
const { localGraph } = this.state;
|
||||||
|
if ((!graph && !localGraph) || !instanceName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ isProcessingNeighbors: true });
|
this.setState({ isProcessingNeighbors: true });
|
||||||
const edges = graph.edges.filter(e => [e.data.source, e.data.target].indexOf(instanceName!) > -1);
|
|
||||||
|
const graphToUse = !!graph ? graph : localGraph;
|
||||||
|
const edges = graphToUse!.edges.filter(e => [e.data.source, e.data.target].indexOf(instanceName!) > -1);
|
||||||
const neighbors: any[] = [];
|
const neighbors: any[] = [];
|
||||||
edges.forEach(e => {
|
edges.forEach(e => {
|
||||||
if (e.data.source === instanceName) {
|
if (e.data.source === instanceName) {
|
||||||
|
@ -144,11 +156,29 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
this.setState({ neighbors, isProcessingNeighbors: false });
|
this.setState({ neighbors, isProcessingNeighbors: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private loadLocalGraphOnSmallScreen = () => {
|
||||||
|
if (!isSmallScreen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ isLoadingLocalGraph: true });
|
||||||
|
getFromApi(`graph/${this.props.instanceName}`)
|
||||||
|
.then((response: IGraph) => {
|
||||||
|
// We do some processing of edges here to make sure that every edge's source and target are in the neighborhood
|
||||||
|
// We could (and should) be doing this in the backend, but I don't want to mess around with complex SQL
|
||||||
|
// queries.
|
||||||
|
// TODO: think more about moving the backend to a graph database that would make this easier.
|
||||||
|
const nodeIds = new Set(response.nodes.map(n => n.data.id));
|
||||||
|
const edges = response.edges.filter(e => nodeIds.has(e.data.source) && nodeIds.has(e.data.target));
|
||||||
|
this.setState({ isLoadingLocalGraph: false, localGraph: { ...response, edges } });
|
||||||
|
})
|
||||||
|
.catch(() => this.setState({ isLoadingLocalGraph: false, localGraphLoadError: true }));
|
||||||
|
};
|
||||||
|
|
||||||
private renderTabs = () => {
|
private renderTabs = () => {
|
||||||
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
||||||
|
|
||||||
const insularCallout =
|
const insularCallout =
|
||||||
this.props.graph && !this.state.isProcessingNeighbors && !hasNeighbors ? (
|
this.props.graph && !this.state.isProcessingNeighbors && !hasNeighbors && !this.state.localGraph ? (
|
||||||
<Callout icon={IconNames.INFO_SIGN} title="Insular instance">
|
<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>
|
<p>This instance doesn't have any neighbors that we know of, so it's hidden from the graph.</p>
|
||||||
</Callout>
|
</Callout>
|
||||||
|
@ -158,6 +188,7 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{insularCallout}
|
{insularCallout}
|
||||||
|
{this.maybeRenderLocalGraph()}
|
||||||
<StyledTabs>
|
<StyledTabs>
|
||||||
{this.props.instanceDetails!.description && (
|
{this.props.instanceDetails!.description && (
|
||||||
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
||||||
|
@ -179,6 +210,22 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private maybeRenderLocalGraph = () => {
|
||||||
|
if (!this.state.localGraph) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<StyledGraphContainer>
|
||||||
|
<Cytoscape
|
||||||
|
elements={this.state.localGraph}
|
||||||
|
currentNodeId={this.props.instanceName}
|
||||||
|
navigateToInstancePath={this.props.navigateToInstance}
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
</StyledGraphContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
private shouldRenderStats = () => {
|
private shouldRenderStats = () => {
|
||||||
const details = this.props.instanceDetails;
|
const details = this.props.instanceDetails;
|
||||||
return details && (details.version || details.userCount || details.statusCount || details.domainCount);
|
return details && (details.version || details.userCount || details.statusCount || details.domainCount);
|
||||||
|
@ -245,34 +292,25 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderNeighbors = () => {
|
private renderNeighbors = () => {
|
||||||
if (!this.props.graph || !this.props.instanceName) {
|
if (!this.state.neighbors) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const edges = this.props.graph.edges.filter(
|
const neighborRows = orderBy(this.state.neighbors, ["weight"], ["desc"]).map(
|
||||||
e => [e.data.source, e.data.target].indexOf(this.props.instanceName!) > -1
|
(neighborDetails: any, idx: number) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td>
|
||||||
|
<Link
|
||||||
|
to={`/instance/${neighborDetails.neighbor}`}
|
||||||
|
className={`${Classes.BUTTON} ${Classes.MINIMAL}`}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
{neighborDetails.neighbor}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>{neighborDetails.weight.toFixed(4)}</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const neighbors: any[] = [];
|
|
||||||
edges.forEach(e => {
|
|
||||||
if (e.data.source === this.props.instanceName) {
|
|
||||||
neighbors.push({ neighbor: e.data.target, weight: e.data.weight });
|
|
||||||
} else {
|
|
||||||
neighbors.push({ neighbor: e.data.source, weight: e.data.weight });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const neighborRows = orderBy(neighbors, ["weight"], ["desc"]).map((neighborDetails: any, idx: number) => (
|
|
||||||
<tr key={idx}>
|
|
||||||
<td>
|
|
||||||
<Link
|
|
||||||
to={`/instance/${neighborDetails.neighbor}`}
|
|
||||||
className={`${Classes.BUTTON} ${Classes.MINIMAL}`}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
{neighborDetails.neighbor}
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td>{neighborDetails.weight.toFixed(4)}</td>
|
|
||||||
</tr>
|
|
||||||
));
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className={Classes.TEXT_MUTED}>
|
<p className={Classes.TEXT_MUTED}>
|
||||||
|
@ -318,16 +356,6 @@ class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInst
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderEmptyState = () => {
|
|
||||||
return (
|
|
||||||
<NonIdealState
|
|
||||||
icon={IconNames.CIRCLE}
|
|
||||||
title="No instance selected"
|
|
||||||
description="Select an instance from the graph or the top-right dropdown to see its details."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderLoadingState = () => <NonIdealState icon={<Spinner />} />;
|
private renderLoadingState = () => <NonIdealState icon={<Spinner />} />;
|
||||||
|
|
||||||
private renderPersonalInstanceErrorState = () => {
|
private renderPersonalInstanceErrorState = () => {
|
||||||
|
@ -390,6 +418,7 @@ const mapStateToProps = (state: IAppState) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||||
|
navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)),
|
||||||
navigateToRoot: () => dispatch(push("/"))
|
navigateToRoot: () => dispatch(push("/"))
|
||||||
});
|
});
|
||||||
const InstanceScreen = connect(
|
const InstanceScreen = connect(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* Screen widths less than this will be treated as mobile */
|
/* Screen widths less than this will be treated as mobile */
|
||||||
export const DESKTOP_WIDTH_THRESHOLD = 800;
|
export const DESKTOP_WIDTH_THRESHOLD = 1000;
|
||||||
|
|
||||||
export const DEFAULT_NODE_COLOR = "#CED9E0";
|
export const DEFAULT_NODE_COLOR = "#CED9E0";
|
||||||
export const SELECTED_NODE_COLOR = "#48AFF0";
|
export const SELECTED_NODE_COLOR = "#48AFF0";
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { FocusStyleManager } from "@blueprintjs/core";
|
||||||
|
|
||||||
import { routerMiddleware } from "connected-react-router";
|
import { routerMiddleware } from "connected-react-router";
|
||||||
import { createBrowserHistory } from "history";
|
import { createBrowserHistory } from "history";
|
||||||
import { AppRouter } from "./AppRouter";
|
import AppRouter from "./AppRouter";
|
||||||
import createRootReducer from "./redux/reducers";
|
import createRootReducer from "./redux/reducers";
|
||||||
|
|
||||||
// https://blueprintjs.com/docs/#core/accessibility.focus-management
|
// https://blueprintjs.com/docs/#core/accessibility.focus-management
|
||||||
|
|
|
@ -13,6 +13,7 @@ const data = (state: IDataState = initialDataState, action: IAction) => {
|
||||||
case ActionType.REQUEST_GRAPH:
|
case ActionType.REQUEST_GRAPH:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
graph: undefined,
|
||||||
isLoadingGraph: true
|
isLoadingGraph: true
|
||||||
};
|
};
|
||||||
case ActionType.RECEIVE_GRAPH:
|
case ActionType.RECEIVE_GRAPH:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createMatchSelector } from "connected-react-router";
|
import { createMatchSelector } from "connected-react-router";
|
||||||
import fetch from "cross-fetch";
|
import fetch from "cross-fetch";
|
||||||
import { IInstanceDomainPath, INSTANCE_DOMAIN_PATH } from "./constants";
|
import { DESKTOP_WIDTH_THRESHOLD, IInstanceDomainPath, INSTANCE_DOMAIN_PATH } from "./constants";
|
||||||
import { IAppState } from "./redux/types";
|
import { IAppState } from "./redux/types";
|
||||||
|
|
||||||
let API_ROOT = "http://localhost:4000/api/";
|
let API_ROOT = "http://localhost:4000/api/";
|
||||||
|
@ -16,3 +16,5 @@ export const getFromApi = (path: string): Promise<any> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const domainMatchSelector = createMatchSelector<IAppState, IInstanceDomainPath>(INSTANCE_DOMAIN_PATH);
|
export const domainMatchSelector = createMatchSelector<IAppState, IInstanceDomainPath>(INSTANCE_DOMAIN_PATH);
|
||||||
|
|
||||||
|
export const isSmallScreen = window.innerWidth < DESKTOP_WIDTH_THRESHOLD;
|
||||||
|
|
Loading…
Reference in a new issue