diff --git a/.gitignore b/.gitignore index d780e02..9cc1da3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .idea/ config.json backend/static/ +*.gexf # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index e0e45f5..6e24e78 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ fediverse.space is a tool to explore instances in the fediverse. ## Running it -* `cp config.json.template config.json` and enter your configuration details. +* `cp config.json.template config.json` and enter your configuration details. I've used a postgres database for development. * Set the environment variable `FEDIVERSE_CONFIG` to point to the path of this file. +* `pip install -r requirements.txt` +* `yarn install` +* Make sure you have the Java 8 JRE (to run) or JDK (to develop) installed, and gradle * For development, run `python manage.py runserver --settings=backend.settings.dev` * In production, set the environment variable `DJANGO_SETTINGS_MODULE=backend.settings.production` + diff --git a/apiv1/_util.py b/apiv1/_util.py new file mode 100644 index 0000000..329199c --- /dev/null +++ b/apiv1/_util.py @@ -0,0 +1,8 @@ +def to_representation(self, instance): + """ + Object instance -> Dict of primitive datatypes. + We use a custom to_representation function to exclude empty fields in the serialized JSON. + """ + ret = super(InstanceListSerializer, self).to_representation(instance) + ret = OrderedDict(list(filter(lambda x: x[1], ret.items()))) + return ret diff --git a/apiv1/serializers.py b/apiv1/serializers.py index 5986250..4b87d58 100644 --- a/apiv1/serializers.py +++ b/apiv1/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from collections import OrderedDict -from scraper.models import Instance +from scraper.models import Instance, PeerRelationship class InstanceListSerializer(serializers.ModelSerializer): @@ -11,6 +11,7 @@ class InstanceListSerializer(serializers.ModelSerializer): def to_representation(self, instance): """ Object instance -> Dict of primitive datatypes. + We use a custom to_representation function to exclude empty fields in the serialized JSON. """ ret = super(InstanceListSerializer, self).to_representation(instance) ret = OrderedDict(list(filter(lambda x: x[1], ret.items()))) @@ -23,3 +24,39 @@ class InstanceDetailSerializer(serializers.ModelSerializer): class Meta: model = Instance fields = '__all__' + + +class EdgeSerializer(serializers.ModelSerializer): + id = serializers.SerializerMethodField('get_pk') + + class Meta: + model = PeerRelationship + fields = ('source', 'target', 'id') + + def get_pk(self, obj): + return obj.pk + + +class NodeSerializer(serializers.ModelSerializer): + id = serializers.SerializerMethodField('get_name') + label = serializers.SerializerMethodField('get_name') + size = serializers.SerializerMethodField() + + class Meta: + model = Instance + fields = ('id', 'label', 'size') + + def get_name(self, obj): + return obj.name + + def get_size(self, obj): + return obj.user_count or 1 + + def to_representation(self, instance): + """ + Object instance -> Dict of primitive datatypes. + We use a custom to_representation function to exclude empty fields in the serialized JSON. + """ + ret = super(NodeSerializer, self).to_representation(instance) + ret = OrderedDict(list(filter(lambda x: x[1], ret.items()))) + return ret diff --git a/apiv1/views.py b/apiv1/views.py index 67776fa..914198c 100644 --- a/apiv1/views.py +++ b/apiv1/views.py @@ -1,6 +1,6 @@ from rest_framework import viewsets -from scraper.models import Instance -from apiv1.serializers import InstanceListSerializer, InstanceDetailSerializer +from scraper.models import Instance, PeerRelationship +from apiv1.serializers import InstanceListSerializer, InstanceDetailSerializer, NodeSerializer, EdgeSerializer class InstanceViewSet(viewsets.ReadOnlyModelViewSet): @@ -18,3 +18,20 @@ class InstanceViewSet(viewsets.ReadOnlyModelViewSet): if hasattr(self, 'detail_serializer_class'): return self.detail_serializer_class return self.serializer_class + + +class EdgeView(viewsets.ReadOnlyModelViewSet): + """ + Endpoint to get a list of the graph's edges in a SigmaJS-friendly format. + """ + queryset = PeerRelationship.objects.all()[:1000] + serializer_class = EdgeSerializer + + +class NodeView(viewsets.ReadOnlyModelViewSet): + """ + Endpoint to get a list of the graph's nodes in a SigmaJS-friendly format. + """ + # queryset = Instance.objects.filter(status='success') + queryset = Instance.objects.all() + serializer_class = NodeSerializer diff --git a/backend/settings/base.py b/backend/settings/base.py index 86c6350..87b1456 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -135,7 +135,7 @@ USE_I18N = True USE_L10N = True -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) diff --git a/backend/urls.py b/backend/urls.py index 7b3505a..665f2dc 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -28,10 +28,11 @@ class OptionalTrailingSlashRouter(routers.DefaultRouter): router = OptionalTrailingSlashRouter() router.register(r'instances', views.InstanceViewSet) +router.register(r'graph/nodes', views.NodeView) +router.register(r'graph/edges', views.EdgeView) urlpatterns = [ path('api/v1/', include(router.urls)), path('silk/', include('silk.urls', namespace='silk')), path('', TemplateView.as_view(template_name='index.html')), ] - diff --git a/frontend/package.json b/frontend/package.json index af80a9f..1f7fd0f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "react-dom": "^16.4.2", "react-redux": "^5.0.7", "react-scripts-ts": "2.17.0", + "react-sigma": "^1.2.30", "react-virtualized": "^9.20.1", "redux": "^4.0.0", "redux-thunk": "^2.3.0" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8770303..03c8ad7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,25 +5,30 @@ import { Dispatch } from 'redux'; import { Button, Intent, NonIdealState, Spinner } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; +import { Graph } from './components/Graph'; import { Nav } from './components/Nav'; -import { fetchInstances } from './redux/actions'; -import { IAppState, IInstance } from './redux/types'; +import { fetchGraph, fetchInstances } from './redux/actions'; +import { IAppState, IGraph, IInstance } from './redux/types'; interface IAppProps { currentInstanceName?: string | null; + graph?: IGraph; instances?: IInstance[], + isLoadingGraph: boolean; isLoadingInstances: boolean, fetchInstances: () => void; + fetchGraph: () => void; } class AppImpl extends React.Component { public render() { let body = this.welcomeState(); if (this.props.isLoadingInstances) { - body = this.loadingState(); - } else if (!!this.props.instances) { - body = this.renderGraph() + body = this.loadingState("Loading instances..."); + } else if (this.props.isLoadingGraph) { + body = this.loadingState("Loading graph..."); + } else if (!!this.props.graph) { + body = ; } - // TODO: show the number of instances up front return (