make stream and restreamconfig listviews reactive with vuejs against restapi

This commit is contained in:
Jan Koppe 2020-05-31 20:25:39 +02:00
parent 2f5c5b20c5
commit 9b231f6859
Signed by: thunfisch
GPG Key ID: BE935B0735A2129B
10 changed files with 204 additions and 80 deletions

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
static/css/font-awesome.min.css static/css/font-awesome.min.css
static/js/bootstrap.bundle.min.js static/js/bootstrap.bundle.min.js
static/js/jquery.min.js static/js/jquery.min.js
static/js/vue.min.js
static/js/axios.min.js
static/fonts static/fonts
staticfiles staticfiles

View File

@ -5,11 +5,6 @@ WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
# https://github.com/twbs/bootstrap/issues/30553 don't upgrade jquery to 3.5.0 yet
ENV JQUERY_VERSION=3.4.1
ENV BOOTSTRAP_VERSION=4.4.1
ENV INTER_VERSION=3.13
# install required packages # install required packages
RUN apk add --no-cache postgresql-dev gcc python3-dev musl-dev gettext postgresql-client nginx supervisor RUN apk add --no-cache postgresql-dev gcc python3-dev musl-dev gettext postgresql-client nginx supervisor
@ -28,14 +23,8 @@ RUN addgroup -S portier && adduser -S portier -G portier
ADD --chown=portier:portier . /app ADD --chown=portier:portier . /app
# add static external libraries for frontend # add static external libraries for frontend
RUN wget http://code.jquery.com/jquery-${JQUERY_VERSION}.min.js -O /app/static/js/jquery.min.js \ RUN ./fetch_frontend_libs.sh \
&& wget https://stackpath.bootstrapcdn.com/bootstrap/${BOOTSTRAP_VERSION}/js/bootstrap.bundle.min.js -O /app/static/js/bootstrap.bundle.min.js \ && chown -R portier:portier static/
&& mkdir -p /tmp/inter /app/static/fonts \
&& cd /tmp/inter && wget https://github.com/rsms/inter/releases/download/v${INTER_VERSION}/Inter-${INTER_VERSION}.zip \
&& unzip Inter-${INTER_VERSION}.zip && mv /tmp/inter/Inter\ Web/* /app/static/fonts/ \
&& cd - \
&& rm -rf /tmp/inter \
&& chown -R portier:portier /app/static/fonts/
# collect static files and compile localized strings # collect static files and compile localized strings
RUN ./manage.py collectstatic --noinput --link RUN ./manage.py collectstatic --noinput --link

20
fetch_frontend_libs.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/sh
set -ex
JQUERY_VERSION=3.5.1
BOOTSTRAP_VERSION=4.4.1
INTER_VERSION=3.13
VUE_VERSION=2.6.11
AXIOS_VERSION=0.19.2
wget https://cdn.jsdelivr.net/npm/jquery@${JQUERY_VERSION}/dist/jquery.min.js -O static/js/jquery.min.js
wget https://cdn.jsdelivr.net/npm/vue@${VUE_VERSION}/dist/vue.min.js -O static/js/vue.min.js
wget https://cdn.jsdelivr.net/npm/axios@${AXIOS_VERSION}/dist/axios.min.js -O static/js/axios.min.js
wget https://cdn.jsdelivr.net/npm/bootstrap@${BOOTSTRAP_VERSION}/dist/js/bootstrap.bundle.min.js -O static/js/bootstrap.bundle.min.js
mkdir -p /tmp/inter static/fonts
cd /tmp/inter && wget https://github.com/rsms/inter/releases/download/v${INTER_VERSION}/Inter-${INTER_VERSION}.zip
unzip Inter-${INTER_VERSION}.zip
cd -
mv /tmp/inter/Inter\ Web/* static/fonts/
rm -rf /tmp/inter

View File

@ -1,11 +1,37 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load fontawesome_5 %}
{% block 'content' %} {% block 'content' %}
<div class="jumbotron"> <div class="jumbotron">
<h1 class="display-4">Hello, world!</h1> <h1 class="display-4">Streaming Magic!</h1>
<p class="leader">Nothing to see here yet. We're working on it, though!</p>
<hr class="my-4"> <hr class="my-4">
<p>Portier will be a pretty cool streaming platform, built by and for nerds. You are welcome to contribute, be it code, ideas, documentation, design or something else entirely.</p> <p>Portier is a pretty cool streaming platform, built by and for nerds. You are welcome to contribute, be it code, ideas, documentation, design or something else entirely.</p>
<a class="btn btn-primary btn-lg" href="https://github.com/chaoswest-tv/portier" role="button">Code on GitHub</a> <a class="btn btn-primary btn" href="https://github.com/chaoswest-tv/portier" role="button">Code on GitHub</a>
</div> </div>
<div class="card-deck">
<div class="card text-center">
<div class="card-body">
<h1 class="card-title text-center">{% fa5_icon 'check-double' %}</h1>
<h4 class="card-subtitle text-center mb-2">Redundancy</h4>
<hr class="mb-3">
<p class="card-text">Send us multiple copies of your stream over different network connections. If one of your connections fails, we'll automatically fail over to the next available - within seconds!</p>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h1 class="card-title">{% fa5_icon 'code-branch' %}</h1>
<h4 class="card-subtitle mb-2">Restreaming</h4>
<hr class="mb-3">
<p class="card-text">Any stream that's coming into our system can be restreamed to a new target. This works with most of the popular streaming platforms - even multiple at the same time.</p>
</div>
</div>
<div class="card text-center">
<div class="card-body">
<h1 class="card-title text-center">{% fa5_icon 'hands-helping' %}</h1>
<h4 class="card-subtitle text-center mb-2">Awesome Support</h4>
<hr class="mb-3">
<p class="card-text">Streaming is hard. Especially if you're not a techie. We have streaming-ninjas in our team that will happily support you every step of the way.</p>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -20,7 +20,8 @@ class ApplicationViewSet(viewsets.ReadOnlyModelViewSet):
class StreamSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer): class StreamSerializer(ObjectPermissionsAssignmentMixin, serializers.ModelSerializer):
class Meta: class Meta:
model = Stream model = Stream
fields = ['id', 'stream', 'name', 'application'] fields = '__all__'
read_only_fields = ['publish_counter']
def get_permissions_map(self, created): def get_permissions_map(self, created):
current_user = self.context['request'].user current_user = self.context['request'].user

View File

@ -1,8 +1,8 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load static %}
{% load bootstrap4 %} {% load bootstrap4 %}
{% load fontawesome_5 %} {% load fontawesome_5 %}
{% load guardian_tags %}
{% block 'content' %} {% block 'content' %}
@ -19,36 +19,45 @@
</div> </div>
</div> </div>
<hr class="my-4"> <hr class="my-4">
<table class="table"> <div id="app">
<thead class="thead-light"> <div v-if="isLoading">
<tr> <div class="my-4 text-center">
<th scope="col">{% trans "name" %}</th> <div class="spinner-border" role="status">
<th scope="col">{% trans "active" %}</th> <span class="sr-only">{% trans "loading..." %}</span>
<th scope="col" class="text-right">{% trans "actions" %}</th> </div>
</tr> </div>
</thead> </div>
<tbody> <table v-else class="table">
{% for object in object_list %} <thead class="thead-light">
{% get_obj_perms user for object as "obj_perms" %} <tr>
{% if "view_restreamconfig" in obj_perms %} <th scope="col">{% trans "name" %}</th>
<tr> <th scope="col">{% trans "active" %}</th>
<th scope="row">{{ object.name }}</th> <th scope="col" class="text-right">{% trans "actions" %}</th>
<td> </tr>
{% if object.active %} </thead>
<span class="text-success">{% fa5_icon 'check-circle' %}</span> <tbody>
{% else %} <tr v-for="cfg in cfgs">
<span class="text-danger">{% fa5_icon 'times-circle' %}</span> <th scope="row">{% verbatim %}{{cfg.name}}{% endverbatim %}</th>
{% endif %} <td v-if="cfg.active">
<button v-on:click="toggleActive(cfg)" type="button" class="btn btn-outline-success">
{% fa5_icon 'check-circle' %}
</button>
</td> </td>
<td align="right"> <td v-else>
<a href="{% url 'restream:restreamconfig_detail' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-primary">{% trans "details" %}</a> <button v-on:click="toggleActive(cfg)" type="button" class="btn btn-outline-danger">
{% if "delete_restreamconfig" in obj_perms %} {% fa5_icon 'times-circle' %}
<a href="{% url 'restream:restreamconfig_delete' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %}</a> </button>
{% endif %}
</td> </td>
<td align="right">
<a v-bind:href="detailLink(cfg.id)" type="button" class="btn btn-sm btn-outline-primary">{% trans "details" %}</a>
<a v-bind:href="deleteLink(cfg.id)" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %}</a>
</td>
</tr> </tr>
{% endif %} </tbody>
{% endfor %} </table>
</tbody> </div>
</table>
<script src="{% static 'js/vue.min.js' %}"></script>
<script src="{% static 'js/axios.min.js' %}"></script>
<script src="{% static 'js/restreamconfig-list.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,6 @@
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import ensure_csrf_cookie
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
from guardian.decorators import permission_required_or_403 from guardian.decorators import permission_required_or_403
@ -12,6 +13,7 @@ from . import forms
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')
@method_decorator(permission_required_or_403('restream.add_restreamconfig'), @method_decorator(permission_required_or_403('restream.add_restreamconfig'),
name='dispatch') name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class RestreamConfigList(ListView): class RestreamConfigList(ListView):
model = models.RestreamConfig model = models.RestreamConfig

View File

@ -1,5 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load static %}
{% load bootstrap4 %} {% load bootstrap4 %}
{% load fontawesome_5 %} {% load fontawesome_5 %}
{% load guardian_tags %} {% load guardian_tags %}
@ -18,36 +19,39 @@
</div> </div>
</div> </div>
<hr class="my-4"> <hr class="my-4">
<table class="table"> <div id="app">
<thead class="thead-light"> <div v-if="isLoading">
<tr> <div class="my-4 text-center">
<th scope="col">{% trans "name" %}</th> <div class="spinner-border" role="status">
<th scope="col">{% trans "receiving" %}</th> <span class="sr-only">{% trans "loading..." %}</span>
<th scope="col" class="text-right">{% trans "actions" %}</th> </div>
</tr> </div>
</thead> </div>
<tbody> <table v-else class="table">
{% for object in object_list %} <thead class="thead-light">
{% get_obj_perms user for object as "obj_perms" %} <tr>
{% if "view_stream" in obj_perms %} <th scope="col">{% trans "name" %}</th>
<tr> <th scope="col">{% trans "receiving" %}</th>
<th scope="ro">{{ object.name }}</th> <th scope="col" class="text-right">{% trans "actions" %}</th>
<td> </tr>
{% if object.publish_counter > 0 %} </thead>
<span class="text-success">{% fa5_icon 'check-circle' %}</span> <tbody>
{% else %} <tr v-for="stream in streams">
<span class="text-danger">{% fa5_icon 'times-circle' %}</span> <th scope="row">{% verbatim %}{{ stream.name }}{% endverbatim %}</th>
{% endif %} <td>
</td> <span v-if="isPublishing(stream)" class="text-success">{% fa5_icon 'check-circle' %} <span class="badge badge-success">{% verbatim %}{{ stream.publish_counter}}{% endverbatim %}</span></span>
<td align="right"> <span v-else class="text-danger">{% fa5_icon 'times-circle' %}</span>
<a href="{% url 'rtmp:stream_detail' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-primary ">{% trans "details" %}</a> </td>
{% if "delete_stream" in obj_perms %} <td align="right">
<a href="{% url 'rtmp:stream_delete' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %}</a> <a v-bind:href="detailLink(stream.id)" type="button" class="btn btn-sm btn-outline-primary">{% trans "details" %}</a>
{% endif %} <a v-bind:href="deleteLink(stream.id)" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %}</a>
</td> </td>
</tr> </tr>
{% endif %} </tbody>
{% endfor %} </table>
</tbody> </div>
</table>
<script src="{% static 'js/vue.min.js' %}"></script>
<script src="{% static 'js/axios.min.js' %}"></script>
<script src="{% static 'js/stream-list.js' %}"></script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,37 @@
var app = new Vue({
el: '#app',
data: {
cfgs: [],
isLoading: true
},
methods: {
detailLink(id) {
return `${id}/`
},
deleteLink(id) {
return `${id}/delete`
},
toggleActive(cfg) {
axios
.patch('/api/v1/restreamconfigs/' + cfg.id + '/', { active: !cfg.active })
.then(response => {
i = this.cfgs.findIndex((obj => obj.id == cfg.id))
Vue.set(this.cfgs, i, response.data)
}
)
},
fetchData() {
axios
.get('/api/v1/restreamconfigs/')
.then(response => {
this.cfgs = response.data
this.isLoading = false
})
}
},
mounted () {
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'
this.fetchData()
}
})

34
static/js/stream-list.js Normal file
View File

@ -0,0 +1,34 @@
var app = new Vue({
el: '#app',
data: {
streams: [],
isLoading: true
},
methods: {
isPublishing(stream) {
return stream.publish_counter > 0
},
detailLink(id) {
return `${id}/`
},
deleteLink(id) {
return `${id}/delete`
},
fetchData() {
axios
.get('/api/v1/streams/')
.then(response => {
this.streams = response.data
this.isLoading = false
})
}
},
mounted () {
axios.defaults.xsrfCookieName = 'csrftoken'
axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'
this.fetchData()
setInterval(function () {
this.fetchData();
}.bind(this), 5000);
}
})