various UI improvements
This commit is contained in:
parent
3cf584cc96
commit
7bf02ea60c
|
@ -1,6 +1,7 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
import math
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from scraper.models import Instance, PeerRelationship
|
from scraper.models import Instance, Edge
|
||||||
|
|
||||||
|
|
||||||
class InstanceListSerializer(serializers.ModelSerializer):
|
class InstanceListSerializer(serializers.ModelSerializer):
|
||||||
|
@ -39,19 +40,23 @@ class InstanceDetailSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Instance
|
model = Instance
|
||||||
fields = ('name', 'description', 'version', 'userCount', 'statusCount', 'domainCount', 'peers', 'lastUpdated')
|
fields = ('name', 'description', 'version', 'userCount', 'statusCount', 'domainCount', 'peers', 'lastUpdated', 'status')
|
||||||
|
|
||||||
|
|
||||||
class EdgeSerializer(serializers.ModelSerializer):
|
class EdgeSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.SerializerMethodField('get_pk')
|
id = serializers.SerializerMethodField('get_pk')
|
||||||
|
size = serializers.SerializerMethodField('get_weight')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PeerRelationship
|
model = Edge
|
||||||
fields = ('source', 'target', 'id')
|
fields = ('source', 'target', 'id', 'size')
|
||||||
|
|
||||||
def get_pk(self, obj):
|
def get_pk(self, obj):
|
||||||
return obj.pk
|
return obj.pk
|
||||||
|
|
||||||
|
def get_weight(self, obj):
|
||||||
|
return obj.weight
|
||||||
|
|
||||||
|
|
||||||
class NodeSerializer(serializers.ModelSerializer):
|
class NodeSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.SerializerMethodField('get_name')
|
id = serializers.SerializerMethodField('get_name')
|
||||||
|
@ -68,7 +73,7 @@ class NodeSerializer(serializers.ModelSerializer):
|
||||||
return obj.name
|
return obj.name
|
||||||
|
|
||||||
def get_size(self, obj):
|
def get_size(self, obj):
|
||||||
return obj.user_count or 1
|
return math.log(obj.user_count) if obj.user_count else 1
|
||||||
|
|
||||||
def get_x(self, obj):
|
def get_x(self, obj):
|
||||||
return obj.x_coord
|
return obj.x_coord
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { RandomizeNodePositions, RelativeSize, Sigma, SigmaEnableWebGL, Filter } from 'react-sigma';
|
import { Sigma, SigmaEnableWebGL, Filter, ForceAtlas2 } from 'react-sigma';
|
||||||
|
|
||||||
import { selectAndLoadInstance } from '../redux/actions';
|
import { selectAndLoadInstance } from '../redux/actions';
|
||||||
|
|
||||||
|
@ -19,6 +19,9 @@ const SETTINGS = {
|
||||||
drawLabels: true,
|
drawLabels: true,
|
||||||
edgeColor: "default",
|
edgeColor: "default",
|
||||||
labelColor: "default",
|
labelColor: "default",
|
||||||
|
labelThreshold: 10,
|
||||||
|
maxEdgeSize: 1,
|
||||||
|
minEdgeSize: 0.3,
|
||||||
}
|
}
|
||||||
|
|
||||||
class GraphImpl extends React.Component {
|
class GraphImpl extends React.Component {
|
||||||
|
@ -37,6 +40,7 @@ class GraphImpl extends React.Component {
|
||||||
onClickStage={(e) => this.props.selectAndLoadInstance(null)}
|
onClickStage={(e) => this.props.selectAndLoadInstance(null)}
|
||||||
>
|
>
|
||||||
<Filter neighborsOf={this.props.currentInstanceName} />
|
<Filter neighborsOf={this.props.currentInstanceName} />
|
||||||
|
<ForceAtlas2 iterationsPerRender={1} timeout={6000}/>
|
||||||
</Sigma>
|
</Sigma>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@ import { connect } from 'react-redux';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import * as sanitize from 'sanitize-html';
|
import * as sanitize from 'sanitize-html';
|
||||||
|
|
||||||
import { Card, Classes, Divider, Elevation, HTMLTable, NonIdealState } from '@blueprintjs/core';
|
import {
|
||||||
|
AnchorButton, Card, Classes, Divider, Elevation, HTMLTable, NonIdealState, Position, Tooltip
|
||||||
|
} from '@blueprintjs/core';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
|
|
||||||
import { selectAndLoadInstance } from '../redux/actions';
|
import { selectAndLoadInstance } from '../redux/actions';
|
||||||
|
@ -30,11 +32,14 @@ class SidebarImpl extends React.Component<ISidebarProps> {
|
||||||
return this.renderLoadingState();
|
return 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) {
|
||||||
|
return this.renderPersonalInstanceErrorState();
|
||||||
|
} else if (this.props.instanceDetails.status !== 'success') {
|
||||||
|
return this.renderMissingDataState();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>{this.props.instanceName || "No instance selected"}</h2>
|
{this.renderHeading()}
|
||||||
<Divider />
|
|
||||||
{this.renderDescription()}
|
{this.renderDescription()}
|
||||||
{this.renderVersion()}
|
{this.renderVersion()}
|
||||||
{this.renderCounts()}
|
{this.renderCounts()}
|
||||||
|
@ -43,6 +48,32 @@ class SidebarImpl extends React.Component<ISidebarProps> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>{content}</h2>
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private renderDescription = () => {
|
private renderDescription = () => {
|
||||||
const description = this.props.instanceDetails!.description;
|
const description = this.props.instanceDetails!.description;
|
||||||
if (!description) {
|
if (!description) {
|
||||||
|
@ -166,6 +197,30 @@ class SidebarImpl extends React.Component<ISidebarProps> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderPersonalInstanceErrorState = () => {
|
||||||
|
return (
|
||||||
|
<NonIdealState
|
||||||
|
icon={IconNames.BLOCKED_PERSON}
|
||||||
|
title="No data"
|
||||||
|
description="This instance has fewer than 5 users and was not crawled."
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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."
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private openInstanceLink = () => {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export interface IInstanceDetails {
|
||||||
userCount?: number;
|
userCount?: number;
|
||||||
version?: string;
|
version?: string;
|
||||||
lastUpdated?: string;
|
lastUpdated?: string;
|
||||||
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IGraphNode {
|
interface IGraphNode {
|
||||||
|
@ -42,6 +43,7 @@ interface IGraphEdge {
|
||||||
source: string;
|
source: string;
|
||||||
target: string;
|
target: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGraph {
|
export interface IGraph {
|
||||||
|
|
|
@ -83,7 +83,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 StepDisplacement(1f));
|
// YifanHuLayout firstLayout = new YifanHuLayout(null, new StepDisplacement(1f));
|
||||||
ForceAtlas2 forceAtlas2Layout = new ForceAtlas2(null);
|
ForceAtlas2 forceAtlas2Layout = new ForceAtlas2(null);
|
||||||
|
|
|
@ -31,7 +31,7 @@ from scraper.management.commands._util import require_lock, InvalidResponseExcep
|
||||||
|
|
||||||
SEED = 'mastodon.social'
|
SEED = 'mastodon.social'
|
||||||
TIMEOUT = 20 # seconds
|
TIMEOUT = 20 # seconds
|
||||||
NUM_THREADS = 64
|
NUM_THREADS = 64 # roughly 40MB each
|
||||||
PERSONAL_INSTANCE_THRESHOLD = 5 # instances with <= this many users won't be scraped
|
PERSONAL_INSTANCE_THRESHOLD = 5 # instances with <= this many users won't be scraped
|
||||||
STATUS_SCRAPE_LIMIT = 5000
|
STATUS_SCRAPE_LIMIT = 5000
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue