2019-04-17 10:44:48 +00:00
|
|
|
import { orderBy } from "lodash";
|
2019-04-17 14:44:23 +00:00
|
|
|
import moment from "moment";
|
2019-07-18 15:20:09 +00:00
|
|
|
import * as numeral from "numeral";
|
2019-07-23 12:20:34 +00:00
|
|
|
import React from "react";
|
2019-04-17 10:44:48 +00:00
|
|
|
import { connect } from "react-redux";
|
2019-04-17 14:44:23 +00:00
|
|
|
import sanitize from "sanitize-html";
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2018-09-03 18:15:28 +00:00
|
|
|
import {
|
2019-04-17 10:44:48 +00:00
|
|
|
AnchorButton,
|
|
|
|
Button,
|
2019-07-18 20:05:16 +00:00
|
|
|
Callout,
|
2019-04-17 10:44:48 +00:00
|
|
|
Classes,
|
|
|
|
Code,
|
|
|
|
Divider,
|
|
|
|
H2,
|
|
|
|
HTMLTable,
|
2019-07-18 15:20:09 +00:00
|
|
|
Icon,
|
2019-04-17 10:44:48 +00:00
|
|
|
NonIdealState,
|
|
|
|
Position,
|
2019-07-23 12:20:34 +00:00
|
|
|
Spinner,
|
2019-04-17 10:44:48 +00:00
|
|
|
Tab,
|
|
|
|
Tabs,
|
|
|
|
Tooltip
|
|
|
|
} from "@blueprintjs/core";
|
|
|
|
import { IconNames } from "@blueprintjs/icons";
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-07-23 12:20:34 +00:00
|
|
|
import { push } from "connected-react-router";
|
2019-07-21 18:05:07 +00:00
|
|
|
import { Link } from "react-router-dom";
|
2019-07-23 12:20:34 +00:00
|
|
|
import { Dispatch } from "redux";
|
2019-07-21 18:05:07 +00:00
|
|
|
import styled from "styled-components";
|
2019-08-06 18:35:26 +00:00
|
|
|
import { IAppState, IGraph, IGraphResponse, IInstanceDetails } from "../../redux/types";
|
2019-07-23 16:32:43 +00:00
|
|
|
import { domainMatchSelector, getFromApi, isSmallScreen } from "../../util";
|
2019-07-24 16:25:20 +00:00
|
|
|
import { InstanceType } from "../atoms";
|
2019-07-23 16:32:43 +00:00
|
|
|
import { Cytoscape, ErrorState } from "../molecules/";
|
2019-08-29 16:54:34 +00:00
|
|
|
import { FederationTab } from "../organisms";
|
2019-07-21 18:05:07 +00:00
|
|
|
|
2019-07-23 12:20:34 +00:00
|
|
|
const InstanceScreenContainer = styled.div`
|
|
|
|
margin-bottom: auto;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
flex: 1;
|
2019-07-21 18:05:07 +00:00
|
|
|
`;
|
2019-07-23 12:20:34 +00:00
|
|
|
const HeadingContainer = styled.div`
|
|
|
|
display: flex;
|
|
|
|
flex-direction: row;
|
|
|
|
align-items: center;
|
2019-07-21 18:05:07 +00:00
|
|
|
width: 100%;
|
2019-07-24 09:43:36 +00:00
|
|
|
padding: 0 20px;
|
2019-07-21 18:05:07 +00:00
|
|
|
`;
|
2019-07-23 12:20:34 +00:00
|
|
|
const StyledHeadingH2 = styled(H2)`
|
|
|
|
margin: 0;
|
|
|
|
`;
|
|
|
|
const StyledCloseButton = styled(Button)`
|
|
|
|
justify-self: flex-end;
|
|
|
|
`;
|
|
|
|
const StyledHeadingTooltip = styled(Tooltip)`
|
|
|
|
margin-left: 5px;
|
|
|
|
flex-grow: 1;
|
2019-07-21 18:05:07 +00:00
|
|
|
`;
|
|
|
|
const StyledHTMLTable = styled(HTMLTable)`
|
|
|
|
width: 100%;
|
|
|
|
`;
|
|
|
|
const StyledLinkToFdNetwork = styled.div`
|
|
|
|
text-align: center;
|
2019-07-23 12:20:34 +00:00
|
|
|
margin-top: auto;
|
2019-07-21 18:05:07 +00:00
|
|
|
`;
|
2019-07-24 09:43:36 +00:00
|
|
|
const StyledCallout = styled(Callout)`
|
|
|
|
margin: 10px 20px;
|
|
|
|
width: auto;
|
|
|
|
`;
|
2019-08-29 15:15:04 +00:00
|
|
|
const NeighborsCallout = styled(Callout)`
|
|
|
|
margin: 10px 0;
|
|
|
|
width: auto;
|
|
|
|
`;
|
2019-07-23 12:20:34 +00:00
|
|
|
const StyledTabs = styled(Tabs)`
|
|
|
|
width: 100%;
|
2019-07-24 09:43:36 +00:00
|
|
|
padding: 0 20px;
|
2019-07-23 12:20:34 +00:00
|
|
|
`;
|
2019-07-23 16:32:43 +00:00
|
|
|
const StyledGraphContainer = styled.div`
|
2019-07-24 09:43:36 +00:00
|
|
|
flex-grow: 1;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
2019-07-23 16:32:43 +00:00
|
|
|
margin-bottom: 10px;
|
|
|
|
`;
|
2019-07-23 12:20:34 +00:00
|
|
|
interface IInstanceScreenProps {
|
2019-04-17 10:44:48 +00:00
|
|
|
graph?: IGraph;
|
|
|
|
instanceName: string | null;
|
|
|
|
instanceLoadError: boolean;
|
|
|
|
instanceDetails: IInstanceDetails | null;
|
|
|
|
isLoadingInstanceDetails: boolean;
|
2019-07-23 12:20:34 +00:00
|
|
|
navigateToRoot: () => void;
|
2019-07-23 16:32:43 +00:00
|
|
|
navigateToInstance: (domain: string) => void;
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
2019-07-23 12:20:34 +00:00
|
|
|
interface IInstanceScreenState {
|
2019-07-18 20:05:16 +00:00
|
|
|
neighbors?: string[];
|
|
|
|
isProcessingNeighbors: boolean;
|
2019-07-23 16:32:43 +00:00
|
|
|
// Local (neighborhood) graph. Used only on small screens (mobile devices).
|
|
|
|
isLoadingLocalGraph: boolean;
|
|
|
|
localGraph?: IGraph;
|
|
|
|
localGraphLoadError?: boolean;
|
2018-09-04 19:29:37 +00:00
|
|
|
}
|
2019-07-23 12:20:34 +00:00
|
|
|
class InstanceScreenImpl extends React.PureComponent<IInstanceScreenProps, IInstanceScreenState> {
|
|
|
|
public constructor(props: IInstanceScreenProps) {
|
2019-04-17 10:44:48 +00:00
|
|
|
super(props);
|
2019-07-23 16:32:43 +00:00
|
|
|
this.state = { isProcessingNeighbors: false, isLoadingLocalGraph: false, localGraphLoadError: false };
|
2019-07-23 12:20:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public render() {
|
|
|
|
let content;
|
2019-07-23 16:32:43 +00:00
|
|
|
if (this.props.isLoadingInstanceDetails || this.state.isProcessingNeighbors || this.state.isLoadingLocalGraph) {
|
2019-07-23 12:20:34 +00:00
|
|
|
content = this.renderLoadingState();
|
2019-08-30 12:33:59 +00:00
|
|
|
} else if (
|
|
|
|
this.props.instanceLoadError ||
|
|
|
|
this.state.localGraphLoadError ||
|
|
|
|
!this.props.instanceDetails ||
|
|
|
|
!this.props.instanceDetails.status
|
|
|
|
) {
|
2019-08-22 10:09:06 +00:00
|
|
|
content = <ErrorState />;
|
2019-07-23 12:20:34 +00:00
|
|
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personal instance") > -1) {
|
|
|
|
content = this.renderPersonalInstanceErrorState();
|
|
|
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("robots.txt") > -1) {
|
|
|
|
content = this.renderRobotsTxtState();
|
|
|
|
} else if (this.props.instanceDetails.status !== "success") {
|
|
|
|
content = this.renderMissingDataState();
|
|
|
|
} else {
|
|
|
|
content = this.renderTabs();
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<InstanceScreenContainer>
|
|
|
|
<HeadingContainer>
|
|
|
|
<StyledHeadingH2>{this.props.instanceName}</StyledHeadingH2>
|
|
|
|
<StyledHeadingTooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
|
|
|
|
<AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
|
|
|
|
</StyledHeadingTooltip>
|
|
|
|
<StyledCloseButton icon={IconNames.CROSS} onClick={this.props.navigateToRoot} />
|
|
|
|
</HeadingContainer>
|
|
|
|
<Divider />
|
|
|
|
{content}
|
|
|
|
</InstanceScreenContainer>
|
|
|
|
);
|
2019-07-18 20:05:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public componentDidMount() {
|
2019-07-23 16:32:43 +00:00
|
|
|
this.loadLocalGraphOnSmallScreen();
|
2019-07-18 20:05:16 +00:00
|
|
|
this.processEdgesToFindNeighbors();
|
|
|
|
}
|
|
|
|
|
2019-07-23 16:32:43 +00:00
|
|
|
public componentDidUpdate(prevProps: IInstanceScreenProps, prevState: IInstanceScreenState) {
|
2019-07-23 14:08:43 +00:00
|
|
|
const isNewInstance = prevProps.instanceName !== this.props.instanceName;
|
|
|
|
const receivedNewEdges = !!this.props.graph && !this.state.isProcessingNeighbors && !this.state.neighbors;
|
2019-07-23 16:32:43 +00:00
|
|
|
const receivedNewLocalGraph = !!this.state.localGraph && !prevState.localGraph;
|
|
|
|
if (isNewInstance || receivedNewEdges || receivedNewLocalGraph) {
|
2019-07-18 20:05:16 +00:00
|
|
|
this.processEdgesToFindNeighbors();
|
|
|
|
}
|
2019-04-17 10:44:48 +00:00
|
|
|
}
|
2018-09-04 19:29:37 +00:00
|
|
|
|
2019-07-18 20:05:16 +00:00
|
|
|
private processEdgesToFindNeighbors = () => {
|
2019-07-24 15:51:44 +00:00
|
|
|
// TODO: use cytoscape to replace this method
|
|
|
|
// simply cy.$id(nodeId).outgoers() (and/or incomers())
|
2019-07-18 20:05:16 +00:00
|
|
|
const { graph, instanceName } = this.props;
|
2019-07-23 16:32:43 +00:00
|
|
|
const { localGraph } = this.state;
|
|
|
|
if ((!graph && !localGraph) || !instanceName) {
|
2019-07-18 20:05:16 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.setState({ isProcessingNeighbors: true });
|
2019-07-23 16:32:43 +00:00
|
|
|
|
|
|
|
const graphToUse = !!graph ? graph : localGraph;
|
|
|
|
const edges = graphToUse!.edges.filter(e => [e.data.source, e.data.target].indexOf(instanceName!) > -1);
|
2019-07-18 20:05:16 +00:00
|
|
|
const neighbors: any[] = [];
|
|
|
|
edges.forEach(e => {
|
|
|
|
if (e.data.source === instanceName) {
|
|
|
|
neighbors.push({ neighbor: e.data.target, weight: e.data.weight });
|
|
|
|
} else {
|
|
|
|
neighbors.push({ neighbor: e.data.source, weight: e.data.weight });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.setState({ neighbors, isProcessingNeighbors: false });
|
|
|
|
};
|
|
|
|
|
2019-07-23 16:32:43 +00:00
|
|
|
private loadLocalGraphOnSmallScreen = () => {
|
|
|
|
if (!isSmallScreen) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.setState({ isLoadingLocalGraph: true });
|
|
|
|
getFromApi(`graph/${this.props.instanceName}`)
|
2019-08-06 18:35:26 +00:00
|
|
|
.then((response: IGraphResponse) => {
|
2019-07-23 16:32:43 +00:00
|
|
|
// 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.
|
2019-08-06 18:35:26 +00:00
|
|
|
const graph = response.graph;
|
|
|
|
const nodeIds = new Set(graph.nodes.map(n => n.data.id));
|
|
|
|
const edges = graph.edges.filter(e => nodeIds.has(e.data.source) && nodeIds.has(e.data.target));
|
|
|
|
this.setState({ isLoadingLocalGraph: false, localGraph: { ...graph, edges } });
|
2019-07-23 16:32:43 +00:00
|
|
|
})
|
|
|
|
.catch(() => this.setState({ isLoadingLocalGraph: false, localGraphLoadError: true }));
|
|
|
|
};
|
|
|
|
|
2019-07-19 18:19:53 +00:00
|
|
|
private renderTabs = () => {
|
|
|
|
const hasNeighbors = this.state.neighbors && this.state.neighbors.length > 0;
|
2019-08-29 16:54:34 +00:00
|
|
|
const federationRestrictions = this.props.instanceDetails && this.props.instanceDetails.federationRestrictions;
|
2019-07-19 18:19:53 +00:00
|
|
|
|
2019-07-24 09:43:36 +00:00
|
|
|
const hasLocalGraph =
|
|
|
|
!!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0;
|
2019-07-23 12:20:34 +00:00
|
|
|
const insularCallout =
|
2019-07-24 09:43:36 +00:00
|
|
|
this.props.graph && !this.state.isProcessingNeighbors && !hasNeighbors && !hasLocalGraph ? (
|
|
|
|
<StyledCallout icon={IconNames.INFO_SIGN} title="Insular instance">
|
2019-07-23 12:20:34 +00:00
|
|
|
<p>This instance doesn't have any neighbors that we know of, so it's hidden from the graph.</p>
|
2019-07-24 09:43:36 +00:00
|
|
|
</StyledCallout>
|
2019-07-23 12:20:34 +00:00
|
|
|
) : (
|
|
|
|
undefined
|
|
|
|
);
|
2019-07-19 18:19:53 +00:00
|
|
|
return (
|
2019-07-23 12:20:34 +00:00
|
|
|
<>
|
2019-07-19 18:19:53 +00:00
|
|
|
{insularCallout}
|
2019-07-23 16:32:43 +00:00
|
|
|
{this.maybeRenderLocalGraph()}
|
2019-07-23 12:20:34 +00:00
|
|
|
<StyledTabs>
|
2019-07-19 18:19:53 +00:00
|
|
|
{this.props.instanceDetails!.description && (
|
2019-04-17 10:44:48 +00:00
|
|
|
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
|
|
|
)}
|
|
|
|
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
2019-08-29 16:54:34 +00:00
|
|
|
{federationRestrictions && Object.keys(federationRestrictions).length > 0 && (
|
|
|
|
<Tab
|
|
|
|
id="federationRestrictions"
|
|
|
|
title="Federation"
|
|
|
|
panel={<FederationTab restrictions={federationRestrictions} />}
|
|
|
|
/>
|
|
|
|
)}
|
2019-04-17 10:44:48 +00:00
|
|
|
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
|
|
|
|
<Tab id="peers" title="Known peers" panel={this.renderPeers()} />
|
2019-07-23 12:20:34 +00:00
|
|
|
</StyledTabs>
|
2019-07-21 18:05:07 +00:00
|
|
|
<StyledLinkToFdNetwork>
|
2019-07-23 12:20:34 +00:00
|
|
|
<AnchorButton
|
2019-07-21 18:05:07 +00:00
|
|
|
href={`https://fediverse.network/${this.props.instanceName}`}
|
2019-07-23 12:20:34 +00:00
|
|
|
minimal={true}
|
|
|
|
rightIcon={IconNames.SHARE}
|
2019-07-21 18:05:07 +00:00
|
|
|
target="_blank"
|
2019-07-23 12:20:34 +00:00
|
|
|
text="See more statistics at fediverse.network"
|
|
|
|
/>
|
2019-07-21 18:05:07 +00:00
|
|
|
</StyledLinkToFdNetwork>
|
2019-07-23 12:20:34 +00:00
|
|
|
</>
|
2019-04-17 10:44:48 +00:00
|
|
|
);
|
|
|
|
};
|
2018-09-04 19:29:37 +00:00
|
|
|
|
2019-07-23 16:32:43 +00:00
|
|
|
private maybeRenderLocalGraph = () => {
|
2019-07-24 09:43:36 +00:00
|
|
|
const { localGraph } = this.state;
|
|
|
|
const hasLocalGraph =
|
|
|
|
!!this.state.localGraph && this.state.localGraph.nodes.length > 0 && this.state.localGraph.edges.length > 0;
|
|
|
|
if (!hasLocalGraph) {
|
2019-07-23 16:32:43 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<StyledGraphContainer>
|
|
|
|
<Cytoscape
|
2019-07-24 09:43:36 +00:00
|
|
|
elements={localGraph!}
|
2019-07-23 16:32:43 +00:00
|
|
|
currentNodeId={this.props.instanceName}
|
|
|
|
navigateToInstancePath={this.props.navigateToInstance}
|
2019-08-04 12:41:44 +00:00
|
|
|
showEdges={true}
|
2019-07-23 16:32:43 +00:00
|
|
|
/>
|
|
|
|
<Divider />
|
|
|
|
</StyledGraphContainer>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private shouldRenderStats = () => {
|
|
|
|
const details = this.props.instanceDetails;
|
|
|
|
return details && (details.version || details.userCount || details.statusCount || details.domainCount);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderDescription = () => {
|
|
|
|
const description = this.props.instanceDetails!.description;
|
|
|
|
if (!description) {
|
|
|
|
return;
|
2018-09-03 18:15:28 +00:00
|
|
|
}
|
2019-04-17 10:44:48 +00:00
|
|
|
return <p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{ __html: sanitize(description) }} />;
|
|
|
|
};
|
2018-09-03 18:15:28 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderVersionAndCounts = () => {
|
2019-07-18 15:20:09 +00:00
|
|
|
if (!this.props.instanceDetails) {
|
|
|
|
throw new Error("Did not receive instance details as expected!");
|
|
|
|
}
|
2019-07-27 17:58:40 +00:00
|
|
|
const {
|
|
|
|
version,
|
|
|
|
userCount,
|
|
|
|
statusCount,
|
|
|
|
domainCount,
|
|
|
|
lastUpdated,
|
|
|
|
insularity,
|
|
|
|
type,
|
2019-08-02 16:37:26 +00:00
|
|
|
statusesPerDay,
|
|
|
|
statusesPerUserPerDay
|
2019-07-27 17:58:40 +00:00
|
|
|
} = this.props.instanceDetails;
|
2019-04-17 10:44:48 +00:00
|
|
|
return (
|
2019-07-23 12:20:34 +00:00
|
|
|
<StyledHTMLTable small={true} striped={true}>
|
|
|
|
<tbody>
|
|
|
|
<tr>
|
|
|
|
<td>Version</td>
|
|
|
|
<td>{<Code>{version}</Code> || "Unknown"}</td>
|
|
|
|
</tr>
|
2019-07-24 16:25:20 +00:00
|
|
|
<tr>
|
|
|
|
<td>Instance type</td>
|
|
|
|
<td>{(type && <InstanceType type={type} colorAfterName={true} />) || "Unknown"}</td>
|
|
|
|
</tr>
|
2019-07-23 12:20:34 +00:00
|
|
|
<tr>
|
|
|
|
<td>Users</td>
|
|
|
|
<td>{(userCount && numeral.default(userCount).format("0,0")) || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Statuses</td>
|
|
|
|
<td>{(statusCount && numeral.default(statusCount).format("0,0")) || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>
|
2019-08-02 16:37:26 +00:00
|
|
|
Insularity{" "}
|
2019-07-23 12:20:34 +00:00
|
|
|
<Tooltip
|
|
|
|
content={
|
|
|
|
<span>
|
|
|
|
The percentage of mentions that are directed
|
|
|
|
<br />
|
|
|
|
toward users on the same instance.
|
|
|
|
</span>
|
|
|
|
}
|
|
|
|
position={Position.TOP}
|
|
|
|
className={Classes.DARK}
|
|
|
|
>
|
|
|
|
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
|
|
|
|
</Tooltip>
|
|
|
|
</td>
|
|
|
|
<td>{(insularity && numeral.default(insularity).format("0.0%")) || "Unknown"}</td>
|
|
|
|
</tr>
|
2019-07-27 17:58:40 +00:00
|
|
|
<tr>
|
|
|
|
<td>
|
2019-08-02 16:37:26 +00:00
|
|
|
Statuses / day{" "}
|
2019-07-27 17:58:40 +00:00
|
|
|
<Tooltip
|
|
|
|
content={
|
|
|
|
<span>
|
2019-08-02 16:37:26 +00:00
|
|
|
The average number of statuses written each day on this instance,
|
2019-07-27 17:58:40 +00:00
|
|
|
<br />
|
|
|
|
over the last month.
|
|
|
|
</span>
|
|
|
|
}
|
|
|
|
position={Position.TOP}
|
|
|
|
className={Classes.DARK}
|
|
|
|
>
|
|
|
|
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
|
|
|
|
</Tooltip>
|
|
|
|
</td>
|
|
|
|
<td>{(statusesPerDay && numeral.default(statusesPerDay).format("0.0")) || "Unknown"}</td>
|
|
|
|
</tr>
|
2019-08-02 16:37:26 +00:00
|
|
|
<tr>
|
|
|
|
<td>
|
|
|
|
Statuses / person / day{" "}
|
|
|
|
<Tooltip
|
|
|
|
content={
|
|
|
|
<span>
|
|
|
|
The average number of statuses written per person each day,
|
|
|
|
<br />
|
|
|
|
over the last month.
|
|
|
|
</span>
|
|
|
|
}
|
|
|
|
position={Position.TOP}
|
|
|
|
className={Classes.DARK}
|
|
|
|
>
|
|
|
|
<Icon icon={IconNames.HELP} iconSize={Icon.SIZE_STANDARD} />
|
|
|
|
</Tooltip>
|
|
|
|
</td>
|
|
|
|
<td>{(statusesPerUserPerDay && numeral.default(statusesPerUserPerDay).format("0.000")) || "Unknown"}</td>
|
|
|
|
</tr>
|
2019-07-23 12:20:34 +00:00
|
|
|
<tr>
|
|
|
|
<td>Known peers</td>
|
|
|
|
<td>{(domainCount && numeral.default(domainCount).format("0,0")) || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Last updated</td>
|
|
|
|
<td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
</tbody>
|
|
|
|
</StyledHTMLTable>
|
2019-04-17 10:44:48 +00:00
|
|
|
);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderNeighbors = () => {
|
2019-07-23 16:32:43 +00:00
|
|
|
if (!this.state.neighbors) {
|
2019-04-17 10:44:48 +00:00
|
|
|
return;
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
2019-07-23 16:32:43 +00:00
|
|
|
const neighborRows = orderBy(this.state.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>
|
|
|
|
)
|
2019-07-18 10:21:12 +00:00
|
|
|
);
|
2019-04-17 10:44:48 +00:00
|
|
|
return (
|
|
|
|
<div>
|
2019-08-29 15:15:04 +00:00
|
|
|
<NeighborsCallout icon={IconNames.INFO_SIGN} title="Warning">
|
|
|
|
<p>
|
2019-08-29 16:54:34 +00:00
|
|
|
Instances that {this.props.instanceName} has blocked may appear on this list and vice versa. This can happen
|
|
|
|
if users attempt to mention someone on an instance that has blocked them.
|
2019-08-29 15:15:04 +00:00
|
|
|
</p>
|
|
|
|
</NeighborsCallout>
|
2019-04-17 10:44:48 +00:00
|
|
|
<p className={Classes.TEXT_MUTED}>
|
2019-08-29 15:15:04 +00:00
|
|
|
The mention ratio is how often people on the two instances mention each other per status. A mention ratio of 1
|
|
|
|
would mean that every single status on {this.props.instanceName} contained a mention of someone on the other
|
|
|
|
instance, and vice versa.
|
2019-04-17 10:44:48 +00:00
|
|
|
</p>
|
2019-07-21 18:05:07 +00:00
|
|
|
<StyledHTMLTable small={true} striped={true} interactive={false}>
|
2019-04-17 10:44:48 +00:00
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<th>Instance</th>
|
|
|
|
<th>Mention ratio</th>
|
2018-09-03 19:30:11 +00:00
|
|
|
</tr>
|
2019-04-17 10:44:48 +00:00
|
|
|
</thead>
|
|
|
|
<tbody>{neighborRows}</tbody>
|
2019-07-21 18:05:07 +00:00
|
|
|
</StyledHTMLTable>
|
2019-04-17 10:44:48 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2018-09-03 19:30:11 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderPeers = () => {
|
|
|
|
const peers = this.props.instanceDetails!.peers;
|
|
|
|
if (!peers || peers.length === 0) {
|
|
|
|
return;
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
2019-04-17 10:44:48 +00:00
|
|
|
const peerRows = peers.map(instance => (
|
2019-07-21 18:05:07 +00:00
|
|
|
<tr key={instance.name}>
|
2019-04-17 10:44:48 +00:00
|
|
|
<td>
|
2019-07-21 18:05:07 +00:00
|
|
|
<Link to={`/instance/${instance.name}`} className={`${Classes.BUTTON} ${Classes.MINIMAL}`} role="button">
|
2019-04-17 10:44:48 +00:00
|
|
|
{instance.name}
|
2019-07-21 18:05:07 +00:00
|
|
|
</Link>
|
2019-04-17 10:44:48 +00:00
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
));
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<p className={Classes.TEXT_MUTED}>
|
|
|
|
All the instances, past and present, that {this.props.instanceName} knows about.
|
|
|
|
</p>
|
2019-07-21 18:05:07 +00:00
|
|
|
<StyledHTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
|
2019-04-17 10:44:48 +00:00
|
|
|
<tbody>{peerRows}</tbody>
|
2019-07-21 18:05:07 +00:00
|
|
|
</StyledHTMLTable>
|
2019-04-17 10:44:48 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-07-23 12:20:34 +00:00
|
|
|
private renderLoadingState = () => <NonIdealState icon={<Spinner />} />;
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderPersonalInstanceErrorState = () => {
|
|
|
|
return (
|
|
|
|
<NonIdealState
|
|
|
|
icon={IconNames.BLOCKED_PERSON}
|
|
|
|
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."
|
|
|
|
action={
|
2019-07-26 22:30:11 +00:00
|
|
|
<Link to={"/admin"} className={Classes.BUTTON} role="button">
|
|
|
|
{"Opt in"}
|
|
|
|
</Link>
|
2019-04-17 10:44:48 +00:00
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
2018-09-03 18:15:28 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderMissingDataState = () => {
|
|
|
|
return (
|
2019-07-23 12:20:34 +00:00
|
|
|
<>
|
2019-07-18 20:05:16 +00:00
|
|
|
<NonIdealState
|
|
|
|
icon={IconNames.ERROR}
|
|
|
|
title="No data"
|
|
|
|
description="This instance could not be crawled. Either it was down or it's an instance type we don't support yet."
|
|
|
|
/>
|
|
|
|
<span className="sidebar-hidden-instance-status" style={{ display: "none" }}>
|
|
|
|
{this.props.instanceDetails && this.props.instanceDetails.status}
|
|
|
|
</span>
|
2019-07-23 12:20:34 +00:00
|
|
|
</>
|
2019-04-19 14:29:45 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2019-07-20 17:43:37 +00:00
|
|
|
private renderRobotsTxtState = () => {
|
|
|
|
return (
|
|
|
|
<NonIdealState
|
2019-07-20 18:08:56 +00:00
|
|
|
icon={
|
|
|
|
<span role="img" aria-label="robot">
|
|
|
|
🤖
|
|
|
|
</span>
|
|
|
|
}
|
2019-07-20 17:43:37 +00:00
|
|
|
title="No data"
|
|
|
|
description="This instance was not crawled because its robots.txt did not allow us to."
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private openInstanceLink = () => {
|
|
|
|
window.open("https://" + this.props.instanceName, "_blank");
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
|
|
|
|
2019-07-21 18:05:07 +00:00
|
|
|
const mapStateToProps = (state: IAppState) => {
|
|
|
|
const match = domainMatchSelector(state);
|
|
|
|
return {
|
2019-07-27 17:58:40 +00:00
|
|
|
graph: state.data.graphResponse && state.data.graphResponse.graph,
|
2019-07-21 18:05:07 +00:00
|
|
|
instanceDetails: state.currentInstance.currentInstanceDetails,
|
|
|
|
instanceLoadError: state.currentInstance.error,
|
|
|
|
instanceName: match && match.params.domain,
|
|
|
|
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
|
|
|
|
};
|
|
|
|
};
|
2019-07-23 12:20:34 +00:00
|
|
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
2019-07-23 16:32:43 +00:00
|
|
|
navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)),
|
2019-07-23 12:20:34 +00:00
|
|
|
navigateToRoot: () => dispatch(push("/"))
|
|
|
|
});
|
|
|
|
const InstanceScreen = connect(
|
|
|
|
mapStateToProps,
|
|
|
|
mapDispatchToProps
|
|
|
|
)(InstanceScreenImpl);
|
|
|
|
export default InstanceScreen;
|