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-04-17 10:44:48 +00:00
|
|
|
import * as React from "react";
|
|
|
|
import { connect } from "react-redux";
|
|
|
|
import { Dispatch } from "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,
|
|
|
|
Card,
|
|
|
|
Classes,
|
|
|
|
Code,
|
|
|
|
Divider,
|
|
|
|
Elevation,
|
|
|
|
H2,
|
|
|
|
H4,
|
|
|
|
HTMLTable,
|
|
|
|
NonIdealState,
|
|
|
|
Position,
|
|
|
|
Tab,
|
|
|
|
Tabs,
|
|
|
|
Tooltip
|
|
|
|
} from "@blueprintjs/core";
|
|
|
|
import { IconNames } from "@blueprintjs/icons";
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
import { selectAndLoadInstance } from "../redux/actions";
|
|
|
|
import { IAppState, IGraph, IInstanceDetails } from "../redux/types";
|
|
|
|
import { ErrorState } from "./ErrorState";
|
2018-09-01 17:24:05 +00:00
|
|
|
|
|
|
|
interface ISidebarProps {
|
2019-04-17 10:44:48 +00:00
|
|
|
graph?: IGraph;
|
|
|
|
instanceName: string | null;
|
|
|
|
instanceLoadError: boolean;
|
|
|
|
instanceDetails: IInstanceDetails | null;
|
|
|
|
isLoadingInstanceDetails: boolean;
|
|
|
|
selectAndLoadInstance: (instanceName: string) => void;
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
2018-09-04 19:29:37 +00:00
|
|
|
interface ISidebarState {
|
2019-04-17 10:44:48 +00:00
|
|
|
isOpen: boolean;
|
2018-09-04 19:29:37 +00:00
|
|
|
}
|
|
|
|
class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
|
2019-04-17 10:44:48 +00:00
|
|
|
constructor(props: ISidebarProps) {
|
|
|
|
super(props);
|
|
|
|
const isOpen = window.innerWidth >= 900 ? true : false;
|
|
|
|
this.state = { isOpen };
|
|
|
|
}
|
2018-09-04 19:29:37 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
public render() {
|
|
|
|
const closedClass = this.state.isOpen ? "" : " closed";
|
|
|
|
const buttonIcon = this.state.isOpen ? IconNames.DOUBLE_CHEVRON_RIGHT : IconNames.DOUBLE_CHEVRON_LEFT;
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<Button
|
|
|
|
onClick={this.handleToggle}
|
|
|
|
large={true}
|
|
|
|
icon={buttonIcon}
|
|
|
|
className={"fediverse-sidebar-toggle-button" + closedClass}
|
|
|
|
minimal={true}
|
|
|
|
/>
|
2019-07-14 11:47:06 +00:00
|
|
|
<Card className={"fediverse-sidebar" + closedClass} elevation={Elevation.TWO}>
|
2019-04-17 10:44:48 +00:00
|
|
|
{this.renderSidebarContents()}
|
|
|
|
</Card>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2018-09-04 19:29:37 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private handleToggle = () => {
|
|
|
|
this.setState({ isOpen: !this.state.isOpen });
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderSidebarContents = () => {
|
|
|
|
if (this.props.isLoadingInstanceDetails) {
|
|
|
|
return this.renderLoadingState();
|
|
|
|
} else if (!this.props.instanceDetails) {
|
|
|
|
return this.renderEmptyState();
|
|
|
|
} else if (this.props.instanceDetails.status.toLowerCase().indexOf("personalinstance") > -1) {
|
|
|
|
return this.renderPersonalInstanceErrorState();
|
|
|
|
} else if (this.props.instanceDetails.status !== "success") {
|
|
|
|
return this.renderMissingDataState();
|
|
|
|
} else if (this.props.instanceLoadError) {
|
|
|
|
return <ErrorState />;
|
2019-04-19 14:29:45 +00:00
|
|
|
} else if (
|
|
|
|
this.props.graph &&
|
|
|
|
this.props.instanceName &&
|
2019-07-18 10:21:12 +00:00
|
|
|
this.props.graph.nodes.map(n => n.data.id).indexOf(this.props.instanceName) < 0
|
2019-04-19 14:29:45 +00:00
|
|
|
) {
|
|
|
|
return this.renderQuietInstanceState();
|
2018-09-04 19:29:37 +00:00
|
|
|
}
|
2019-04-17 10:44:48 +00:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
{this.renderHeading()}
|
|
|
|
<Tabs>
|
|
|
|
{this.props.instanceDetails.description && (
|
|
|
|
<Tab id="description" title="Description" panel={this.renderDescription()} />
|
|
|
|
)}
|
|
|
|
{this.shouldRenderStats() && <Tab id="stats" title="Details" panel={this.renderVersionAndCounts()} />}
|
|
|
|
<Tab id="neighbors" title="Neighbors" panel={this.renderNeighbors()} />
|
|
|
|
<Tab id="peers" title="Known peers" panel={this.renderPeers()} />
|
|
|
|
</Tabs>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2018-09-04 19:29:37 +00:00
|
|
|
|
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 renderHeading = () => {
|
|
|
|
let content: JSX.Element;
|
|
|
|
if (!this.props.instanceName) {
|
|
|
|
content = <span>{"No instance selected"}</span>;
|
|
|
|
} else {
|
|
|
|
content = (
|
|
|
|
<span>
|
|
|
|
{this.props.instanceName + " "}
|
|
|
|
<Tooltip content="Open link in new tab" position={Position.TOP} className={Classes.DARK}>
|
|
|
|
<AnchorButton icon={IconNames.LINK} minimal={true} onClick={this.openInstanceLink} />
|
|
|
|
</Tooltip>
|
|
|
|
</span>
|
|
|
|
);
|
2018-09-03 19:30:11 +00:00
|
|
|
}
|
2019-04-17 10:44:48 +00:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<H2>{content}</H2>
|
|
|
|
<Divider />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2018-09-03 19:30:11 +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 = () => {
|
|
|
|
const version = this.props.instanceDetails!.version;
|
|
|
|
const userCount = this.props.instanceDetails!.userCount;
|
|
|
|
const statusCount = this.props.instanceDetails!.statusCount;
|
|
|
|
const domainCount = this.props.instanceDetails!.domainCount;
|
|
|
|
const lastUpdated = this.props.instanceDetails!.lastUpdated;
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<HTMLTable small={true} striped={true} className="fediverse-sidebar-table">
|
|
|
|
<tbody>
|
|
|
|
<tr>
|
|
|
|
<td>Version</td>
|
|
|
|
<td>{<Code>{version}</Code> || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Users</td>
|
|
|
|
<td>{userCount || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Statuses</td>
|
|
|
|
<td>{statusCount || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Known peers</td>
|
|
|
|
<td>{domainCount || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
<tr>
|
|
|
|
<td>Last updated</td>
|
|
|
|
<td>{moment(lastUpdated + "Z").fromNow() || "Unknown"}</td>
|
|
|
|
</tr>
|
|
|
|
</tbody>
|
|
|
|
</HTMLTable>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderNeighbors = () => {
|
|
|
|
if (!this.props.graph || !this.props.instanceName) {
|
|
|
|
return;
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
2019-07-18 10:21:12 +00:00
|
|
|
const edges = this.props.graph.edges.filter(
|
|
|
|
e => [e.data.source, e.data.target].indexOf(this.props.instanceName!) > -1
|
|
|
|
);
|
2019-04-17 10:44:48 +00:00
|
|
|
const neighbors: any[] = [];
|
|
|
|
edges.forEach(e => {
|
2019-07-18 10:21:12 +00:00
|
|
|
if (e.data.source === this.props.instanceName) {
|
|
|
|
neighbors.push({ neighbor: e.data.target, weight: e.data.weight });
|
2019-04-17 10:44:48 +00:00
|
|
|
} else {
|
2019-07-18 10:21:12 +00:00
|
|
|
neighbors.push({ neighbor: e.data.source, weight: e.data.weight });
|
2019-04-17 10:44:48 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
const neighborRows = orderBy(neighbors, ["weight"], ["desc"]).map((neighborDetails: any, idx: number) => (
|
|
|
|
<tr key={idx}>
|
|
|
|
<td>
|
|
|
|
<AnchorButton minimal={true} onClick={this.selectInstance}>
|
|
|
|
{neighborDetails.neighbor}
|
|
|
|
</AnchorButton>
|
|
|
|
</td>
|
|
|
|
<td>{neighborDetails.weight.toFixed(4)}</td>
|
|
|
|
</tr>
|
|
|
|
));
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<p className={Classes.TEXT_MUTED}>
|
|
|
|
The mention ratio is the average of how many times the two instances mention each other per status. A mention
|
|
|
|
ratio of 1 would mean that every single status contained a mention of a user on the other instance.
|
|
|
|
</p>
|
|
|
|
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
|
|
|
|
<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>
|
|
|
|
</HTMLTable>
|
|
|
|
</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 => (
|
|
|
|
<tr key={instance.name} onClick={this.selectInstance}>
|
|
|
|
<td>
|
|
|
|
<AnchorButton minimal={true} onClick={this.selectInstance}>
|
|
|
|
{instance.name}
|
|
|
|
</AnchorButton>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
));
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<p className={Classes.TEXT_MUTED}>
|
|
|
|
All the instances, past and present, that {this.props.instanceName} knows about.
|
|
|
|
</p>
|
|
|
|
<HTMLTable small={true} striped={true} interactive={false} className="fediverse-sidebar-table">
|
|
|
|
<tbody>{peerRows}</tbody>
|
|
|
|
</HTMLTable>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
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."
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderLoadingState = () => {
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<H4>
|
|
|
|
<span className={Classes.SKELETON}>Description</span>
|
|
|
|
</H4>
|
|
|
|
<p className={Classes.SKELETON}>
|
|
|
|
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit
|
|
|
|
voluptates quia nulla et saepe id suscipit. Voluptas sed rerum placeat consectetur pariatur necessitatibus
|
|
|
|
tempora. Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt.
|
|
|
|
Cupiditate sit voluptates quia nulla et saepe id suscipit. Voluptas sed rerum placeat consectetur pariatur
|
|
|
|
necessitatibus tempora.
|
|
|
|
</p>
|
|
|
|
<H4>
|
|
|
|
<span className={Classes.SKELETON}>Version</span>
|
|
|
|
</H4>
|
|
|
|
<p className={Classes.SKELETON}>Eaque rerum sequi unde omnis voluptatibus non quia fugit.</p>
|
|
|
|
<H4>
|
|
|
|
<span className={Classes.SKELETON}>Stats</span>
|
|
|
|
</H4>
|
|
|
|
<p className={Classes.SKELETON}>
|
|
|
|
Eaque rerum sequi unde omnis voluptatibus non quia fugit. Dignissimos asperiores aut incidunt. Cupiditate sit
|
|
|
|
voluptates quia nulla et saepe id suscipit. Eaque rerum sequi unde omnis voluptatibus non quia fugit.
|
|
|
|
Dignissimos asperiores aut incidunt. Cupiditate sit voluptates quia nulla et saepe id suscipit.
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
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={
|
|
|
|
<AnchorButton icon={IconNames.CONFIRM} href="https://cursed.technology/@tao" target="_blank">
|
|
|
|
Message @tao to opt in
|
|
|
|
</AnchorButton>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
2018-09-03 18:15:28 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private renderMissingDataState = () => {
|
|
|
|
return (
|
|
|
|
<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."
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
2018-09-03 18:15:28 +00:00
|
|
|
|
2019-04-19 14:29:45 +00:00
|
|
|
private renderQuietInstanceState = () => {
|
|
|
|
return (
|
|
|
|
<NonIdealState
|
|
|
|
icon={IconNames.CLEAN}
|
|
|
|
title="No interactions"
|
|
|
|
description="Users on this instance have not publicly interacted with any other instances recently. "
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private openInstanceLink = () => {
|
|
|
|
window.open("https://" + this.props.instanceName, "_blank");
|
|
|
|
};
|
2018-09-03 18:15:28 +00:00
|
|
|
|
2019-04-17 10:44:48 +00:00
|
|
|
private selectInstance = (e: any) => {
|
|
|
|
this.props.selectAndLoadInstance(e.target.innerText);
|
|
|
|
};
|
2018-09-01 17:24:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const mapStateToProps = (state: IAppState) => ({
|
2019-04-17 10:44:48 +00:00
|
|
|
graph: state.data.graph,
|
|
|
|
instanceDetails: state.currentInstance.currentInstanceDetails,
|
|
|
|
instanceLoadError: state.currentInstance.error,
|
|
|
|
instanceName: state.currentInstance.currentInstanceName,
|
|
|
|
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails
|
2018-09-01 17:24:05 +00:00
|
|
|
});
|
|
|
|
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
2019-04-17 10:44:48 +00:00
|
|
|
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any)
|
2018-09-01 17:24:05 +00:00
|
|
|
});
|
2019-04-17 10:44:48 +00:00
|
|
|
export const Sidebar = connect(
|
|
|
|
mapStateToProps,
|
|
|
|
mapDispatchToProps
|
|
|
|
)(SidebarImpl);
|