add sidebar with instance details

This commit is contained in:
Tao Bojlen 2018-09-01 19:24:05 +02:00
parent 93932c5196
commit ef18276c21
14 changed files with 387 additions and 62 deletions

View file

@ -19,11 +19,27 @@ class InstanceListSerializer(serializers.ModelSerializer):
class InstanceDetailSerializer(serializers.ModelSerializer): class InstanceDetailSerializer(serializers.ModelSerializer):
userCount = serializers.SerializerMethodField()
statusCount = serializers.SerializerMethodField()
domainCount = serializers.SerializerMethodField()
lastUpdated = serializers.SerializerMethodField()
peers = InstanceListSerializer(many=True, read_only=True) peers = InstanceListSerializer(many=True, read_only=True)
def get_userCount(self, obj):
return obj.user_count
def get_statusCount(self, obj):
return obj.status_count
def get_domainCount(self, obj):
return obj.domain_count
def get_lastUpdated(self, obj):
return obj.last_updated
class Meta: class Meta:
model = Instance model = Instance
fields = '__all__' fields = ('name', 'description', 'version', 'userCount', 'statusCount', 'domainCount', 'peers', 'lastUpdated')
class EdgeSerializer(serializers.ModelSerializer): class EdgeSerializer(serializers.ModelSerializer):

View file

@ -16,7 +16,8 @@
"react-sigma": "^1.2.30", "react-sigma": "^1.2.30",
"react-virtualized": "^9.20.1", "react-virtualized": "^9.20.1",
"redux": "^4.0.0", "redux": "^4.0.0",
"redux-thunk": "^2.3.0" "redux-thunk": "^2.3.0",
"sanitize-html": "^1.18.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts-ts start", "start": "react-scripts-ts start",
@ -32,6 +33,7 @@
"@types/react-dom": "^16.0.7", "@types/react-dom": "^16.0.7",
"@types/react-redux": "^6.0.6", "@types/react-redux": "^6.0.6",
"@types/react-virtualized": "^9.18.7", "@types/react-virtualized": "^9.18.7",
"@types/sanitize-html": "^1.18.0",
"typescript": "^3.0.1" "typescript": "^3.0.1"
} }
} }

View file

@ -7,11 +7,11 @@ import { IconNames } from '@blueprintjs/icons';
import { Graph } from './components/Graph'; import { Graph } from './components/Graph';
import { Nav } from './components/Nav'; import { Nav } from './components/Nav';
import { Sidebar } from './components/Sidebar';
import { fetchGraph, fetchInstances } from './redux/actions'; import { fetchGraph, fetchInstances } from './redux/actions';
import { IAppState, IGraph, IInstance } from './redux/types'; import { IAppState, IGraph, IInstance } from './redux/types';
interface IAppProps { interface IAppProps {
currentInstanceName?: string | null;
graph?: IGraph; graph?: IGraph;
instances?: IInstance[], instances?: IInstance[],
isLoadingGraph: boolean; isLoadingGraph: boolean;
@ -27,7 +27,7 @@ class AppImpl extends React.Component<IAppProps> {
} else if (this.props.isLoadingGraph) { } else if (this.props.isLoadingGraph) {
body = this.loadingState("Loading graph..."); body = this.loadingState("Loading graph...");
} else if (!!this.props.graph) { } else if (!!this.props.graph) {
body = <Graph />; body = this.graphState();
} }
return ( return (
<div className="App bp3-dark"> <div className="App bp3-dark">
@ -41,6 +41,15 @@ class AppImpl extends React.Component<IAppProps> {
this.props.fetchInstances(); this.props.fetchInstances();
} }
private graphState = () => {
return (
<div>
<Sidebar />
<Graph />
</div>
)
}
private welcomeState = () => { private welcomeState = () => {
const numInstances = this.props.instances ? this.props.instances.length : "lots of"; const numInstances = this.props.instances ? this.props.instances.length : "lots of";
const description = `There are ${numInstances} known instances, so loading the graph might take a little while. Ready?` const description = `There are ${numInstances} known instances, so loading the graph might take a little while. Ready?`
@ -67,7 +76,6 @@ class AppImpl extends React.Component<IAppProps> {
} }
const mapStateToProps = (state: IAppState) => ({ const mapStateToProps = (state: IAppState) => ({
currentInstanceName: state.currentInstanceName,
graph: state.data.graph, graph: state.data.graph,
instances: state.data.instances, instances: state.data.instances,
isLoadingGraph: state.data.isLoadingGraph, isLoadingGraph: state.data.isLoadingGraph,

View file

@ -1,8 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { NodeShapes, RandomizeNodePositions, RelativeSize, Sigma, SigmaEnableWebGL, LoadGEXF, Filter } from 'react-sigma'; import { RandomizeNodePositions, RelativeSize, Sigma, SigmaEnableWebGL, Filter } from 'react-sigma';
import { selectInstance } from '../redux/actions'; import { selectAndLoadInstance } from '../redux/actions';
const STYLE = { const STYLE = {
bottom: "0", bottom: "0",
@ -14,6 +14,7 @@ const STYLE = {
const SETTINGS = { const SETTINGS = {
defaultEdgeColor: "#5C7080", defaultEdgeColor: "#5C7080",
defaultNodeColor: "#CED9E0", defaultNodeColor: "#CED9E0",
defaultNodeHoverColor: "#48AFF0",
drawEdges: true, drawEdges: true,
drawLabels: true, drawLabels: true,
edgeColor: "default", edgeColor: "default",
@ -31,8 +32,8 @@ class GraphImpl extends React.Component {
renderer="webgl" renderer="webgl"
settings={SETTINGS} settings={SETTINGS}
style={STYLE} style={STYLE}
onClickNode={(e) => this.props.selectInstance(e.data.node.label)} onClickNode={(e) => this.props.selectAndLoadInstance(e.data.node.label)}
onClickStage={(e) => this.props.selectInstance(null)} onClickStage={(e) => this.props.selectAndLoadInstance(null)}
> >
<RandomizeNodePositions /> <RandomizeNodePositions />
<Filter neighborsOf={this.props.currentInstanceName} /> <Filter neighborsOf={this.props.currentInstanceName} />
@ -40,21 +41,13 @@ class GraphImpl extends React.Component {
</Sigma> </Sigma>
) )
} }
// onClickNode = (e) => {
// this.props.selectInstance(e.data.node.label);
// }
// zoomToNode = (camera, node) => {
// s
// }
} }
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
currentInstanceName: state.currentInstanceName, currentInstanceName: state.currentInstance.currentInstanceName,
graph: state.data.graph, graph: state.data.graph,
}) })
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
selectInstance: (instanceName) => dispatch(selectInstance(instanceName)), selectAndLoadInstance: (instanceName) => dispatch(selectAndLoadInstance(instanceName)),
}) })
export const Graph = connect(mapStateToProps, mapDispatchToProps)(GraphImpl) export const Graph = connect(mapStateToProps, mapDispatchToProps)(GraphImpl)

View file

@ -6,13 +6,13 @@ import { Button, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import { IItemRendererProps, ItemPredicate, Select } from '@blueprintjs/select'; import { IItemRendererProps, ItemPredicate, Select } from '@blueprintjs/select';
import { selectInstance } from '../redux/actions'; import { selectAndLoadInstance } from '../redux/actions';
import { IAppState, IInstance } from '../redux/types'; import { IAppState, IInstance } from '../redux/types';
interface IInstanceSearchProps { interface IInstanceSearchProps {
currentInstanceName: string | null; currentInstanceName: string | null;
instances?: IInstance[]; instances?: IInstance[];
selectInstance: (instanceName: string) => void; selectAndLoadInstance: (instanceName: string) => void;
} }
const InstanceSelect = Select.ofType<IInstance>(); const InstanceSelect = Select.ofType<IInstance>();
@ -75,15 +75,15 @@ class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
} }
private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => { private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => {
this.props.selectInstance(item.name); this.props.selectAndLoadInstance(item.name);
} }
} }
const mapStateToProps = (state: IAppState) => ({ const mapStateToProps = (state: IAppState) => ({
currentInstanceName: state.currentInstanceName, currentInstanceName: state.currentInstance.currentInstanceName,
instances: state.data.instances, instances: state.data.instances,
}) })
const mapDispatchToProps = (dispatch: Dispatch) => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
selectInstance: (instanceName: string) => dispatch(selectInstance(instanceName)), selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any),
}) })
export const InstanceSearch = connect(mapStateToProps, mapDispatchToProps)(InstanceSearchImpl) export const InstanceSearch = connect(mapStateToProps, mapDispatchToProps)(InstanceSearchImpl)

View file

@ -1,20 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { Alignment, Button, Icon, Navbar } from '@blueprintjs/core'; import { Alignment, Button, Icon, Navbar } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import { selectInstance } from '../redux/actions';
import { IAppState, IInstance } from '../redux/types';
import { InstanceSearch } from './InstanceSearch'; import { InstanceSearch } from './InstanceSearch';
interface INavProps { export class Nav extends React.Component {
instances?: IInstance[];
selectInstance: (instance: string) => void;
}
class NavImpl extends React.Component<INavProps> {
public render() { public render() {
return ( return (
<Navbar> <Navbar>
@ -38,11 +29,3 @@ class NavImpl extends React.Component<INavProps> {
) )
} }
} }
const mapStateToProps = (state: IAppState) => ({
instances: state.data.instances,
})
const mapDispatchToProps = (dispatch: Dispatch) => ({
selectInstance: (instanceName: string) => dispatch(selectInstance(instanceName)),
})
export const Nav = connect(mapStateToProps, mapDispatchToProps)(NavImpl)

View file

@ -0,0 +1,176 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import * as sanitize from 'sanitize-html';
import { Card, Classes, Divider, Elevation, HTMLTable, NonIdealState } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { selectAndLoadInstance } from '../redux/actions';
import { IAppState, IInstanceDetails } from '../redux/types';
interface ISidebarProps {
instanceName: string | null,
instanceDetails: IInstanceDetails | null,
isLoadingInstanceDetails: boolean;
selectAndLoadInstance: (instanceName: string) => void;
}
class SidebarImpl extends React.Component<ISidebarProps> {
public render() {
return (
<Card className="fediverse-sidebar" elevation={Elevation.TWO}>
{this.renderSidebarContents()}
</Card>
)
}
private renderSidebarContents = () => {
if (this.props.isLoadingInstanceDetails) {
return this.renderLoadingState();
} else if (!this.props.instanceDetails) {
return this.renderEmptyState();
}
return (
<div>
<h2>{this.props.instanceName || "No instance selected"}</h2>
<Divider />
{this.renderDescription()}
{this.renderVersion()}
{this.renderCounts()}
{this.renderPeers()}
</div>
);
}
private renderDescription = () => {
const description = this.props.instanceDetails!.description;
if (!description) {
return;
}
return (
<div>
<h4>Description</h4>
<div className={Classes.RUNNING_TEXT} dangerouslySetInnerHTML={{__html: sanitize(description)}} />
<Divider />
</div>
)
}
private renderVersion = () => {
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 statusCount = this.props.instanceDetails!.statusCount;
const domainCount = this.props.instanceDetails!.domainCount;
if (!userCount && !statusCount && !domainCount) {
return;
}
return (
<div>
<h4>Stats</h4>
<HTMLTable small={true} striped={true} className="fediverse-sidebar-table">
<tbody>
<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>
</tbody>
</HTMLTable>
<Divider />
</div>
)
}
private renderPeers = () => {
const peers = this.props.instanceDetails!.peers;
if (!peers) {
return;
}
const peerRows = peers.map(instance => (
<tr key={instance.name} onClick={this.selectInstance}>
<td>{instance.name}</td>
</tr>
));
return (
<div>
<h4>Known instances</h4>
<HTMLTable small={true} striped={true} interactive={true} className="fediverse-sidebar-table">
<tbody>
{peerRows}
</tbody>
</HTMLTable>
</div>
)
}
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 = () => {
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>
);
}
private selectInstance = (e: any)=> {
this.props.selectAndLoadInstance(e.target.innerText);
}
}
const mapStateToProps = (state: IAppState) => ({
instanceDetails: state.currentInstance.currentInstanceDetails,
instanceName: state.currentInstance.currentInstanceName,
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails,
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
selectAndLoadInstance: (instanceName: string) => dispatch(selectAndLoadInstance(instanceName) as any),
});
export const Sidebar = connect(mapStateToProps, mapDispatchToProps)(SidebarImpl);

View file

@ -16,3 +16,19 @@ html, body {
min-width: 300px; min-width: 300px;
overflow-x: hidden; overflow-x: hidden;
} }
.fediverse-sidebar {
position: fixed;
top: 50px;
bottom: 0;
right: 0;
min-width: 300px;
width: 25%;
z-index: 20;
overflow: scroll;
overflow-x: hidden;
}
.fediverse-sidebar-table {
width: 100%;
}

View file

@ -1,14 +1,20 @@
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { getFromApi } from '../util'; import { getFromApi } from '../util';
import { ActionType, IGraph, IInstance } from './types'; import { ActionType, IGraph, IInstance, IInstanceDetails } from './types';
export const selectInstance = (instanceName: string) => { // selectInstance and deselectInstance are not exported since we only call them from selectAndLoadInstance()
const selectInstance = (instanceName: string) => {
return { return {
payload: instanceName, payload: instanceName,
type: ActionType.SELECT_INSTANCE, type: ActionType.SELECT_INSTANCE,
} }
} }
const deselectInstance = () => {
return {
type: ActionType.DESELECT_INSTANCE,
}
}
export const requestInstances = () => { export const requestInstances = () => {
return { return {
@ -22,7 +28,6 @@ export const receiveInstances = (instances: IInstance[]) => {
type: ActionType.RECEIVE_INSTANCES, type: ActionType.RECEIVE_INSTANCES,
} }
} }
export const requestGraph = () => { export const requestGraph = () => {
return { return {
type: ActionType.REQUEST_GRAPH, type: ActionType.REQUEST_GRAPH,
@ -36,6 +41,14 @@ export const receiveGraph = (graph: IGraph) => {
} }
} }
export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => {
return {
payload: instanceDetails,
type: ActionType.RECEIVE_INSTANCE_DETAILS,
}
}
/** Async actions: https://redux.js.org/advanced/asyncactions */ /** Async actions: https://redux.js.org/advanced/asyncactions */
export const fetchInstances = () => { export const fetchInstances = () => {
@ -43,8 +56,20 @@ export const fetchInstances = () => {
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
dispatch(requestInstances()); dispatch(requestInstances());
return getFromApi("instances") return getFromApi("instances")
.then(instances => dispatch(receiveInstances(instances)) .then(instances => dispatch(receiveInstances(instances)));
); }
}
export const selectAndLoadInstance = (instanceName: string) => {
// TODO: handle errors
return (dispatch: Dispatch) => {
if (!instanceName) {
dispatch(deselectInstance());
return;
}
dispatch(selectInstance(instanceName));
return getFromApi("instances/" + instanceName)
.then(details => dispatch(receiveInstanceDetails(details)));
} }
} }
@ -59,6 +84,6 @@ export const fetchGraph = () => {
nodes: responses[1], nodes: responses[1],
}; };
}) })
.then(graph => dispatch(receiveGraph(graph))) .then(graph => dispatch(receiveGraph(graph)));
} }
} }

View file

@ -1,6 +1,6 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { ActionType, IAction, IDataState } from './types'; import { ActionType, IAction, ICurrentInstanceState, IDataState } from './types';
const initialDataState = { const initialDataState = {
isLoadingGraph: false, isLoadingGraph: false,
@ -36,16 +36,37 @@ const data = (state: IDataState = initialDataState, action: IAction) => {
} }
} }
const currentInstanceName = (state: string | null = null, action: IAction): string | null => { const initialCurrentInstanceState = {
currentInstanceDetails: null,
currentInstanceName: null,
isLoadingInstanceDetails: false
};
const currentInstance = (state = initialCurrentInstanceState , action: IAction): ICurrentInstanceState => {
switch (action.type) { switch (action.type) {
case ActionType.SELECT_INSTANCE: case ActionType.SELECT_INSTANCE:
return action.payload; return {
...state,
currentInstanceName: action.payload,
isLoadingInstanceDetails: true,
};
case ActionType.RECEIVE_INSTANCE_DETAILS:
return {
...state,
currentInstanceDetails: action.payload,
isLoadingInstanceDetails: false,
}
case ActionType.DESELECT_INSTANCE:
return {
...state,
currentInstanceDetails: null,
currentInstanceName: null,
}
default: default:
return state; return state;
} }
} }
export const rootReducer = combineReducers({ export const rootReducer = combineReducers({
currentInstanceName, currentInstance,
data, data,
}) })

View file

@ -4,6 +4,8 @@ export enum ActionType {
RECEIVE_INSTANCES = 'RECEIVE_INSTANCES', RECEIVE_INSTANCES = 'RECEIVE_INSTANCES',
REQUEST_GRAPH = 'REQUEST_GRAPH', REQUEST_GRAPH = 'REQUEST_GRAPH',
RECEIVE_GRAPH = 'RECEIVE_GRAPH', RECEIVE_GRAPH = 'RECEIVE_GRAPH',
RECEIVE_INSTANCE_DETAILS = 'RECEIVE_INSTANCE_DETAILS',
DESELECT_INSTANCE = "DESELECT_INSTANCE",
} }
export interface IAction { export interface IAction {
@ -16,6 +18,17 @@ export interface IInstance {
numUsers?: number, numUsers?: number,
} }
export interface IInstanceDetails {
name: string;
peers?: IInstance[];
description?: string;
domainCount?: number;
statusCount?: number;
userCount?: number;
version?: string;
lastUpdated?: string;
}
interface IGraphNode { interface IGraphNode {
id: string; id: string;
label: string; label: string;
@ -36,6 +49,12 @@ export interface IGraph {
// Redux state // Redux state
export interface ICurrentInstanceState {
currentInstanceDetails: IInstanceDetails | null,
currentInstanceName: string | null,
isLoadingInstanceDetails: boolean,
}
export interface IDataState { export interface IDataState {
instances?: IInstance[], instances?: IInstance[],
graph?: IGraph, graph?: IGraph,
@ -44,6 +63,6 @@ export interface IDataState {
} }
export interface IAppState { export interface IAppState {
currentInstanceName: string | null, currentInstance: ICurrentInstanceState;
data: IDataState, data: IDataState,
} }

View file

@ -100,6 +100,10 @@
"@types/prop-types" "*" "@types/prop-types" "*"
csstype "^2.2.0" csstype "^2.2.0"
"@types/sanitize-html@^1.18.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-1.18.0.tgz#de5cb560a41308ea8474e93b9d10bbb4050692f5"
abab@^2.0.0: abab@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
@ -309,7 +313,7 @@ array-union@^1.0.1:
dependencies: dependencies:
array-uniq "^1.0.1" array-uniq "^1.0.1"
array-uniq@^1.0.1: array-uniq@^1.0.1, array-uniq@^1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
@ -2175,7 +2179,7 @@ domain-browser@^1.1.1:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
domelementtype@1: domelementtype@1, domelementtype@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
@ -2195,6 +2199,12 @@ domhandler@2.1:
dependencies: dependencies:
domelementtype "1" domelementtype "1"
domhandler@^2.3.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
dependencies:
domelementtype "1"
domutils@1.1: domutils@1.1:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
@ -2208,6 +2218,13 @@ domutils@1.5.1:
dom-serializer "0" dom-serializer "0"
domelementtype "1" domelementtype "1"
domutils@^1.5.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
dependencies:
dom-serializer "0"
domelementtype "1"
dot-prop@^4.1.0: dot-prop@^4.1.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
@ -2295,7 +2312,7 @@ enhanced-resolve@^3.0.0, enhanced-resolve@^3.4.0:
object-assign "^4.0.1" object-assign "^4.0.1"
tapable "^0.2.7" tapable "^0.2.7"
entities@~1.1.1: entities@^1.1.1, entities@~1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
@ -3221,6 +3238,17 @@ html-webpack-plugin@2.29.0:
pretty-error "^2.0.2" pretty-error "^2.0.2"
toposort "^1.0.0" toposort "^1.0.0"
htmlparser2@^3.9.0:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
dependencies:
domelementtype "^1.3.0"
domhandler "^2.3.0"
domutils "^1.5.1"
entities "^1.1.1"
inherits "^2.0.1"
readable-stream "^2.0.2"
htmlparser2@~3.3.0: htmlparser2@~3.3.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
@ -4318,6 +4346,10 @@ lodash.camelcase@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
lodash.debounce@^4.0.8: lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@ -4330,10 +4362,18 @@ lodash.endswith@^4.2.1:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09" resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09"
lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
lodash.isfunction@^3.0.8: lodash.isfunction@^3.0.8:
version "3.0.9" version "3.0.9"
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
lodash.isstring@^4.0.1: lodash.isstring@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
@ -4342,6 +4382,10 @@ lodash.memoize@^4.1.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
lodash.mergewith@^4.6.0:
version "4.6.1"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
lodash.sortby@^4.7.0: lodash.sortby@^4.7.0:
version "4.7.0" version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@ -5494,7 +5538,7 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
source-map "^0.5.6" source-map "^0.5.6"
supports-color "^3.2.3" supports-color "^3.2.3"
postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.13: postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.13, postcss@^6.0.14:
version "6.0.23" version "6.0.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
dependencies: dependencies:
@ -6231,6 +6275,21 @@ sane@^2.0.0:
optionalDependencies: optionalDependencies:
fsevents "^1.2.3" fsevents "^1.2.3"
sanitize-html@^1.18.4:
version "1.18.4"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.18.4.tgz#ffdeea13b555dd5e872e9a68b79e5e716cd8c543"
dependencies:
chalk "^2.3.0"
htmlparser2 "^3.9.0"
lodash.clonedeep "^4.5.0"
lodash.escaperegexp "^4.1.2"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.mergewith "^4.6.0"
postcss "^6.0.14"
srcset "^1.0.0"
xtend "^4.0.0"
sax@^1.2.4, sax@~1.2.1: sax@^1.2.4, sax@~1.2.1:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@ -6560,6 +6619,13 @@ sprintf-js@~1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
srcset@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef"
dependencies:
array-uniq "^1.0.2"
number-is-nan "^1.0.0"
sshpk@^1.7.0: sshpk@^1.7.0:
version "1.14.2" version "1.14.2"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.2.tgz#c6fc61648a3d9c4e764fd3fcdf4ea105e492ba98"

View file

@ -1,4 +1,4 @@
# Generated by Django 2.1 on 2018-08-30 19:57 # Generated by Django 2.1 on 2018-09-01 14:00
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -37,7 +37,7 @@ class Migration(migrations.Migration):
), ),
migrations.AddField( migrations.AddField(
model_name='instance', model_name='instance',
name='following', name='peers',
field=models.ManyToManyField(related_name='followers', through='scraper.PeerRelationship', to='scraper.Instance'), field=models.ManyToManyField(through='scraper.PeerRelationship', to='scraper.Instance'),
), ),
] ]

View file

@ -20,7 +20,7 @@ class Instance(models.Model):
status = models.CharField(max_length=100) status = models.CharField(max_length=100)
# Foreign keys # Foreign keys
following = models.ManyToManyField('self', symmetrical=False, through='PeerRelationship', related_name="followers") peers = models.ManyToManyField('self', symmetrical=False, through='PeerRelationship')
# Automatic fields # Automatic fields
first_seen = models.DateTimeField(auto_now_add=True) first_seen = models.DateTimeField(auto_now_add=True)