show neighbors in sidebar

This commit is contained in:
Tao Bojlen 2018-09-03 21:30:11 +02:00
parent 7bf02ea60c
commit 58b36250e2
4 changed files with 92 additions and 35 deletions

View File

@ -5,6 +5,9 @@ from scraper.models import Instance, Edge
class InstanceListSerializer(serializers.ModelSerializer): class InstanceListSerializer(serializers.ModelSerializer):
"""
Minimal instance details used in the full list of instances.
"""
class Meta: class Meta:
model = Instance model = Instance
fields = ('name', 'user_count') fields = ('name', 'user_count')
@ -20,6 +23,9 @@ class InstanceListSerializer(serializers.ModelSerializer):
class InstanceDetailSerializer(serializers.ModelSerializer): class InstanceDetailSerializer(serializers.ModelSerializer):
"""
Detailed instance view.
"""
userCount = serializers.SerializerMethodField() userCount = serializers.SerializerMethodField()
statusCount = serializers.SerializerMethodField() statusCount = serializers.SerializerMethodField()
domainCount = serializers.SerializerMethodField() domainCount = serializers.SerializerMethodField()
@ -40,10 +46,15 @@ class InstanceDetailSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Instance model = Instance
fields = ('name', 'description', 'version', 'userCount', 'statusCount', 'domainCount', 'peers', 'lastUpdated', 'status') fields = ('name', 'description', 'version', 'userCount',
'statusCount', 'domainCount', 'peers', 'lastUpdated',
'status')
class EdgeSerializer(serializers.ModelSerializer): class EdgeSerializer(serializers.ModelSerializer):
"""
Used for displaying the graph.
"""
id = serializers.SerializerMethodField('get_pk') id = serializers.SerializerMethodField('get_pk')
size = serializers.SerializerMethodField('get_weight') size = serializers.SerializerMethodField('get_weight')
@ -59,6 +70,9 @@ class EdgeSerializer(serializers.ModelSerializer):
class NodeSerializer(serializers.ModelSerializer): class NodeSerializer(serializers.ModelSerializer):
"""
Used for displaying the graph.
"""
id = serializers.SerializerMethodField('get_name') id = serializers.SerializerMethodField('get_name')
label = serializers.SerializerMethodField('get_name') label = serializers.SerializerMethodField('get_name')
size = serializers.SerializerMethodField() size = serializers.SerializerMethodField()

View File

@ -8,6 +8,7 @@
"@blueprintjs/select": "^3.1.0", "@blueprintjs/select": "^3.1.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"cross-fetch": "^2.2.2", "cross-fetch": "^2.2.2",
"lodash": "^4.17.10",
"moment": "^2.22.2", "moment": "^2.22.2",
"normalize.css": "^8.0.0", "normalize.css": "^8.0.0",
"react": "^16.4.2", "react": "^16.4.2",
@ -29,6 +30,7 @@
"devDependencies": { "devDependencies": {
"@types/classnames": "^2.2.6", "@types/classnames": "^2.2.6",
"@types/jest": "^23.3.1", "@types/jest": "^23.3.1",
"@types/lodash": "^4.14.116",
"@types/node": "^10.9.2", "@types/node": "^10.9.2",
"@types/react": "^16.4.12", "@types/react": "^16.4.12",
"@types/react-dom": "^16.0.7", "@types/react-dom": "^16.0.7",

View File

@ -1,3 +1,4 @@
import { orderBy } from 'lodash';
import * as moment from 'moment'; import * as moment from 'moment';
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -5,14 +6,16 @@ import { Dispatch } from 'redux';
import * as sanitize from 'sanitize-html'; import * as sanitize from 'sanitize-html';
import { import {
AnchorButton, Card, Classes, Divider, Elevation, HTMLTable, NonIdealState, Position, Tooltip AnchorButton, Card, Classes, Divider, Elevation, HTMLTable, NonIdealState, Position, Tab, Tabs,
Tooltip
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import { selectAndLoadInstance } from '../redux/actions'; import { selectAndLoadInstance } from '../redux/actions';
import { IAppState, IInstanceDetails } from '../redux/types'; import { IAppState, IGraph, IInstanceDetails } from '../redux/types';
interface ISidebarProps { interface ISidebarProps {
graph?: IGraph,
instanceName: string | null, instanceName: string | null,
instanceDetails: IInstanceDetails | null, instanceDetails: IInstanceDetails | null,
isLoadingInstanceDetails: boolean; isLoadingInstanceDetails: boolean;
@ -40,14 +43,23 @@ class SidebarImpl extends React.Component<ISidebarProps> {
return ( return (
<div> <div>
{this.renderHeading()} {this.renderHeading()}
{this.renderDescription()} <Tabs>
{this.renderVersion()} {this.props.instanceDetails.description &&
{this.renderCounts()} <Tab id="description" title="Description" panel={this.renderDescription()} />}
{this.renderPeers()} {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> </div>
); );
} }
private shouldRenderStats = () => {
const details = this.props.instanceDetails;
return details && (details.version || details.userCount || details.statusCount || details.domainCount);
}
private renderHeading = () => { private renderHeading = () => {
let content: JSX.Element; let content: JSX.Element;
if (!this.props.instanceName) { if (!this.props.instanceName) {
@ -80,41 +92,24 @@ class SidebarImpl extends React.Component<ISidebarProps> {
return; return;
} }
return ( return (
<div> <p className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{__html: sanitize(description)}} />
<h4>Description</h4>
<div className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{__html: sanitize(description)}} />
<Divider />
</div>
) )
} }
private renderVersion = () => { private renderVersionAndCounts = () => {
const version = this.props.instanceDetails!.version; const version = this.props.instanceDetails!.version;
if (!version) {
return;
}
return (
<div>
<h4>Version</h4>
<code className={Classes.CODE}>{version}</code>
<Divider />
</div>
)
}
private renderCounts = () => {
const userCount = this.props.instanceDetails!.userCount; const userCount = this.props.instanceDetails!.userCount;
const statusCount = this.props.instanceDetails!.statusCount; const statusCount = this.props.instanceDetails!.statusCount;
const domainCount = this.props.instanceDetails!.domainCount; const domainCount = this.props.instanceDetails!.domainCount;
const lastUpdated = this.props.instanceDetails!.lastUpdated; const lastUpdated = this.props.instanceDetails!.lastUpdated;
if (!userCount && !statusCount && !domainCount) {
return;
}
return ( return (
<div> <div>
<h4>Stats</h4>
<HTMLTable small={true} striped={true} className="fediverse-sidebar-table"> <HTMLTable small={true} striped={true} className="fediverse-sidebar-table">
<tbody> <tbody>
<tr>
<td>Version</td>
<td>{<code>{version}</code> || "Unknown"}</td>
</tr>
<tr> <tr>
<td>Users</td> <td>Users</td>
<td>{userCount || "Unknown"}</td> <td>{userCount || "Unknown"}</td>
@ -133,11 +128,50 @@ class SidebarImpl extends React.Component<ISidebarProps> {
</tr> </tr>
</tbody> </tbody>
</HTMLTable> </HTMLTable>
<Divider />
</div> </div>
) )
} }
private renderNeighbors = () => {
if (!this.props.graph || !this.props.instanceName) {
return;
}
const edges = this.props.graph.edges.filter(e => [e.source, e.target].indexOf(this.props.instanceName!) > -1);
const neighbors: any[] = [];
edges.forEach(e => {
if (e.source === this.props.instanceName) {
neighbors.push({neighbor: e.target, weight: e.size});
} else {
neighbors.push({neighbor: e.source, weight: e.size});
}
})
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>
</tr>
</thead>
<tbody>
{neighborRows}
</tbody>
</HTMLTable>
</div>
);
}
private renderPeers = () => { private renderPeers = () => {
const peers = this.props.instanceDetails!.peers; const peers = this.props.instanceDetails!.peers;
if (!peers || peers.length === 0) { if (!peers || peers.length === 0) {
@ -145,13 +179,15 @@ class SidebarImpl extends React.Component<ISidebarProps> {
} }
const peerRows = peers.map(instance => ( const peerRows = peers.map(instance => (
<tr key={instance.name} onClick={this.selectInstance}> <tr key={instance.name} onClick={this.selectInstance}>
<td>{instance.name}</td> <td><AnchorButton minimal={true} onClick={this.selectInstance}>{instance.name}</AnchorButton></td>
</tr> </tr>
)); ));
return ( return (
<div> <div>
<h4>Known instances</h4> <p className={Classes.TEXT_MUTED}>
<HTMLTable small={true} striped={true} interactive={true} className="fediverse-sidebar-table"> 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> <tbody>
{peerRows} {peerRows}
</tbody> </tbody>
@ -221,12 +257,13 @@ class SidebarImpl extends React.Component<ISidebarProps> {
window.open("https://" + this.props.instanceName, "_blank"); window.open("https://" + this.props.instanceName, "_blank");
} }
private selectInstance = (e: any)=> { private selectInstance = (e: any) => {
this.props.selectAndLoadInstance(e.target.innerText); this.props.selectAndLoadInstance(e.target.innerText);
} }
} }
const mapStateToProps = (state: IAppState) => ({ const mapStateToProps = (state: IAppState) => ({
graph: state.data.graph,
instanceDetails: state.currentInstance.currentInstanceDetails, instanceDetails: state.currentInstance.currentInstanceDetails,
instanceName: state.currentInstance.currentInstanceName, instanceName: state.currentInstance.currentInstanceName,
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails, isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails,

View File

@ -62,6 +62,10 @@
version "0.0.29" version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
"@types/lodash@^4.14.116":
version "4.14.116"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
"@types/node@*", "@types/node@^10.9.2": "@types/node@*", "@types/node@^10.9.2":
version "10.9.2" version "10.9.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.2.tgz#f0ab8dced5cd6c56b26765e1c0d9e4fdcc9f2a00" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.2.tgz#f0ab8dced5cd6c56b26765e1c0d9e4fdcc9f2a00"