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):
userCount = serializers.SerializerMethodField()
statusCount = serializers.SerializerMethodField()
domainCount = serializers.SerializerMethodField()
lastUpdated = serializers.SerializerMethodField()
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:
model = Instance
fields = '__all__'
fields = ('name', 'description', 'version', 'userCount', 'statusCount', 'domainCount', 'peers', 'lastUpdated')
class EdgeSerializer(serializers.ModelSerializer):

View file

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

View file

@ -7,11 +7,11 @@ import { IconNames } from '@blueprintjs/icons';
import { Graph } from './components/Graph';
import { Nav } from './components/Nav';
import { Sidebar } from './components/Sidebar';
import { fetchGraph, fetchInstances } from './redux/actions';
import { IAppState, IGraph, IInstance } from './redux/types';
interface IAppProps {
currentInstanceName?: string | null;
graph?: IGraph;
instances?: IInstance[],
isLoadingGraph: boolean;
@ -27,7 +27,7 @@ class AppImpl extends React.Component<IAppProps> {
} else if (this.props.isLoadingGraph) {
body = this.loadingState("Loading graph...");
} else if (!!this.props.graph) {
body = <Graph />;
body = this.graphState();
}
return (
<div className="App bp3-dark">
@ -41,6 +41,15 @@ class AppImpl extends React.Component<IAppProps> {
this.props.fetchInstances();
}
private graphState = () => {
return (
<div>
<Sidebar />
<Graph />
</div>
)
}
private welcomeState = () => {
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?`
@ -67,7 +76,6 @@ class AppImpl extends React.Component<IAppProps> {
}
const mapStateToProps = (state: IAppState) => ({
currentInstanceName: state.currentInstanceName,
graph: state.data.graph,
instances: state.data.instances,
isLoadingGraph: state.data.isLoadingGraph,

View file

@ -1,8 +1,8 @@
import * as React from 'react';
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 = {
bottom: "0",
@ -14,6 +14,7 @@ const STYLE = {
const SETTINGS = {
defaultEdgeColor: "#5C7080",
defaultNodeColor: "#CED9E0",
defaultNodeHoverColor: "#48AFF0",
drawEdges: true,
drawLabels: true,
edgeColor: "default",
@ -31,8 +32,8 @@ class GraphImpl extends React.Component {
renderer="webgl"
settings={SETTINGS}
style={STYLE}
onClickNode={(e) => this.props.selectInstance(e.data.node.label)}
onClickStage={(e) => this.props.selectInstance(null)}
onClickNode={(e) => this.props.selectAndLoadInstance(e.data.node.label)}
onClickStage={(e) => this.props.selectAndLoadInstance(null)}
>
<RandomizeNodePositions />
<Filter neighborsOf={this.props.currentInstanceName} />
@ -40,21 +41,13 @@ class GraphImpl extends React.Component {
</Sigma>
)
}
// onClickNode = (e) => {
// this.props.selectInstance(e.data.node.label);
// }
// zoomToNode = (camera, node) => {
// s
// }
}
const mapStateToProps = (state) => ({
currentInstanceName: state.currentInstanceName,
currentInstanceName: state.currentInstance.currentInstanceName,
graph: state.data.graph,
})
const mapDispatchToProps = (dispatch) => ({
selectInstance: (instanceName) => dispatch(selectInstance(instanceName)),
selectAndLoadInstance: (instanceName) => dispatch(selectAndLoadInstance(instanceName)),
})
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 { IItemRendererProps, ItemPredicate, Select } from '@blueprintjs/select';
import { selectInstance } from '../redux/actions';
import { selectAndLoadInstance } from '../redux/actions';
import { IAppState, IInstance } from '../redux/types';
interface IInstanceSearchProps {
currentInstanceName: string | null;
instances?: IInstance[];
selectInstance: (instanceName: string) => void;
selectAndLoadInstance: (instanceName: string) => void;
}
const InstanceSelect = Select.ofType<IInstance>();
@ -75,15 +75,15 @@ class InstanceSearchImpl extends React.Component<IInstanceSearchProps> {
}
private onItemSelect = (item: IInstance, event?: React.SyntheticEvent<HTMLElement>) => {
this.props.selectInstance(item.name);
this.props.selectAndLoadInstance(item.name);
}
}
const mapStateToProps = (state: IAppState) => ({
currentInstanceName: state.currentInstanceName,
currentInstanceName: state.currentInstance.currentInstanceName,
instances: state.data.instances,
})
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)

View file

@ -1,20 +1,11 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { Alignment, Button, Icon, Navbar } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import { selectInstance } from '../redux/actions';
import { IAppState, IInstance } from '../redux/types';
import { InstanceSearch } from './InstanceSearch';
interface INavProps {
instances?: IInstance[];
selectInstance: (instance: string) => void;
}
class NavImpl extends React.Component<INavProps> {
export class Nav extends React.Component {
public render() {
return (
<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;
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 { 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 {
payload: instanceName,
type: ActionType.SELECT_INSTANCE,
}
}
const deselectInstance = () => {
return {
type: ActionType.DESELECT_INSTANCE,
}
}
export const requestInstances = () => {
return {
@ -22,7 +28,6 @@ export const receiveInstances = (instances: IInstance[]) => {
type: ActionType.RECEIVE_INSTANCES,
}
}
export const requestGraph = () => {
return {
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 */
export const fetchInstances = () => {
@ -43,8 +56,20 @@ export const fetchInstances = () => {
return (dispatch: Dispatch) => {
dispatch(requestInstances());
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],
};
})
.then(graph => dispatch(receiveGraph(graph)))
.then(graph => dispatch(receiveGraph(graph)));
}
}

View file

@ -1,6 +1,6 @@
import { combineReducers } from 'redux';
import { ActionType, IAction, IDataState } from './types';
import { ActionType, IAction, ICurrentInstanceState, IDataState } from './types';
const initialDataState = {
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) {
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:
return state;
}
}
export const rootReducer = combineReducers({
currentInstanceName,
currentInstance,
data,
})

View file

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

View file

@ -100,6 +100,10 @@
"@types/prop-types" "*"
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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
@ -309,7 +313,7 @@ array-union@^1.0.1:
dependencies:
array-uniq "^1.0.1"
array-uniq@^1.0.1:
array-uniq@^1.0.1, array-uniq@^1.0.2:
version "1.0.3"
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"
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"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
@ -2195,6 +2199,12 @@ domhandler@2.1:
dependencies:
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:
version "1.1.6"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
@ -2208,6 +2218,13 @@ domutils@1.5.1:
dom-serializer "0"
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:
version "4.2.0"
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"
tapable "^0.2.7"
entities@~1.1.1:
entities@^1.1.1, entities@~1.1.1:
version "1.1.1"
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"
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:
version "3.3.0"
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"
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:
version "4.0.8"
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"
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:
version "3.0.9"
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:
version "4.0.1"
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"
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:
version "4.7.0"
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"
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"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
dependencies:
@ -6231,6 +6275,21 @@ sane@^2.0.0:
optionalDependencies:
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:
version "1.2.4"
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"
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:
version "1.14.2"
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
import django.db.models.deletion
@ -37,7 +37,7 @@ class Migration(migrations.Migration):
),
migrations.AddField(
model_name='instance',
name='following',
field=models.ManyToManyField(related_name='followers', through='scraper.PeerRelationship', to='scraper.Instance'),
name='peers',
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)
# Foreign keys
following = models.ManyToManyField('self', symmetrical=False, through='PeerRelationship', related_name="followers")
peers = models.ManyToManyField('self', symmetrical=False, through='PeerRelationship')
# Automatic fields
first_seen = models.DateTimeField(auto_now_add=True)