diff --git a/apiv1/serializers.py b/apiv1/serializers.py index 4b87d58..06ce009 100644 --- a/apiv1/serializers.py +++ b/apiv1/serializers.py @@ -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): diff --git a/frontend/package.json b/frontend/package.json index 1f7fd0f..371677c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 03c8ad7..0dd6ec3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 { } else if (this.props.isLoadingGraph) { body = this.loadingState("Loading graph..."); } else if (!!this.props.graph) { - body = ; + body = this.graphState(); } return (
@@ -41,6 +41,15 @@ class AppImpl extends React.Component { this.props.fetchInstances(); } + private graphState = () => { + return ( +
+ + +
+ ) + } + 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 { } const mapStateToProps = (state: IAppState) => ({ - currentInstanceName: state.currentInstanceName, graph: state.data.graph, instances: state.data.instances, isLoadingGraph: state.data.isLoadingGraph, diff --git a/frontend/src/components/Graph.jsx b/frontend/src/components/Graph.jsx index d0c2897..ef0707e 100644 --- a/frontend/src/components/Graph.jsx +++ b/frontend/src/components/Graph.jsx @@ -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)} > @@ -40,21 +41,13 @@ class GraphImpl extends React.Component { ) } - - // 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) diff --git a/frontend/src/components/InstanceSearch.tsx b/frontend/src/components/InstanceSearch.tsx index 2acee51..34cf2ec 100644 --- a/frontend/src/components/InstanceSearch.tsx +++ b/frontend/src/components/InstanceSearch.tsx @@ -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(); @@ -75,15 +75,15 @@ class InstanceSearchImpl extends React.Component { } private onItemSelect = (item: IInstance, event?: React.SyntheticEvent) => { - 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) diff --git a/frontend/src/components/Nav.tsx b/frontend/src/components/Nav.tsx index 367b739..509664d 100644 --- a/frontend/src/components/Nav.tsx +++ b/frontend/src/components/Nav.tsx @@ -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 { +export class Nav extends React.Component { public render() { return ( @@ -38,11 +29,3 @@ class NavImpl extends React.Component { ) } } - -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) \ No newline at end of file diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..8b4227e --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -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 { + public render() { + return ( + + {this.renderSidebarContents()} + + ) + } + + private renderSidebarContents = () => { + if (this.props.isLoadingInstanceDetails) { + return this.renderLoadingState(); + } else if (!this.props.instanceDetails) { + return this.renderEmptyState(); + } + return ( +
+

{this.props.instanceName || "No instance selected"}

+ + {this.renderDescription()} + {this.renderVersion()} + {this.renderCounts()} + {this.renderPeers()} +
+ ); + } + + private renderDescription = () => { + const description = this.props.instanceDetails!.description; + if (!description) { + return; + } + return ( +
+

Description

+
+ +
+ ) + } + + private renderVersion = () => { + const version = this.props.instanceDetails!.version; + if (!version) { + return; + } + return ( +
+

Version

+ {version} + +
+ ) + } + + 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 ( +
+

Stats

+ + + + Users + {userCount || "Unknown"} + + + Statuses + {statusCount || "Unknown"} + + + Known peers + {domainCount || "Unknown"} + + + + +
+ ) + } + + private renderPeers = () => { + const peers = this.props.instanceDetails!.peers; + if (!peers) { + return; + } + const peerRows = peers.map(instance => ( + + {instance.name} + + )); + return ( +
+

Known instances

+ + + {peerRows} + + +
+ ) + } + + private renderEmptyState = () => { + return ( + + ) + } + + private renderLoadingState = () => { + return ( +
+

Description

+

+ 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. +

+

Version

+

+ Eaque rerum sequi unde omnis voluptatibus non quia fugit. +

+

Stats

+

+ 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. +

+
+ ); + } + + 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); diff --git a/frontend/src/index.css b/frontend/src/index.css index f8b906a..638be0a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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%; +} \ No newline at end of file diff --git a/frontend/src/redux/actions.ts b/frontend/src/redux/actions.ts index f0dd7df..920b323 100644 --- a/frontend/src/redux/actions.ts +++ b/frontend/src/redux/actions.ts @@ -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))); } } diff --git a/frontend/src/redux/reducers.ts b/frontend/src/redux/reducers.ts index 6b2d86b..87a7da0 100644 --- a/frontend/src/redux/reducers.ts +++ b/frontend/src/redux/reducers.ts @@ -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, }) \ No newline at end of file diff --git a/frontend/src/redux/types.ts b/frontend/src/redux/types.ts index b7cf3a9..eecd2ce 100644 --- a/frontend/src/redux/types.ts +++ b/frontend/src/redux/types.ts @@ -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, -} \ No newline at end of file +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6d933e1..2a00b44 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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" diff --git a/scraper/migrations/0001_initial.py b/scraper/migrations/0001_initial.py index 40db480..d1daaac 100644 --- a/scraper/migrations/0001_initial.py +++ b/scraper/migrations/0001_initial.py @@ -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'), ), ] diff --git a/scraper/models.py b/scraper/models.py index 8a88198..9c1649a 100644 --- a/scraper/models.py +++ b/scraper/models.py @@ -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)