wip
This commit is contained in:
parent
d52575aa42
commit
7e5e4fd29b
19 changed files with 352 additions and 68 deletions
29
core/core/globals.py
Normal file
29
core/core/globals.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import signal
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
def create_task(coro):
|
||||
global loop
|
||||
loop.create_task(coro)
|
||||
|
||||
|
||||
async def shutdown(sig, loop):
|
||||
log = logging.getLogger()
|
||||
log.info(f"Received exit signal {sig.name}...")
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not
|
||||
asyncio.current_task()]
|
||||
[task.cancel() for task in tasks]
|
||||
log.info(f"Cancelling {len(tasks)} outstanding tasks")
|
||||
await asyncio.wait_for(loop.shutdown_asyncgens(), timeout=10)
|
||||
loop.stop()
|
||||
log.info("Shutdown complete.")
|
||||
|
||||
|
||||
def init_loop():
|
||||
global loop
|
||||
loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(shutdown(signal.SIGTERM, loop)))
|
||||
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(signal.SIGINT, loop)))
|
||||
return loop
|
|
@ -51,6 +51,7 @@ INSTALLED_APPS = [
|
|||
'channels',
|
||||
'files',
|
||||
'inventory',
|
||||
'mail',
|
||||
'notify_sessions',
|
||||
]
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ from django.urls import path, include
|
|||
from .version import get_info
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('djangoadmin/', admin.site.urls),
|
||||
path('api/1/', include('inventory.api_v1')),
|
||||
path('api/1/', include('files.api_v1')),
|
||||
path('api/1/', include('files.media_v1')),
|
||||
|
|
0
core/mail/__init__.py
Normal file
0
core/mail/__init__.py
Normal file
0
core/mail/migrations/__init__.py
Normal file
0
core/mail/migrations/__init__.py
Normal file
15
core/mail/models.py
Normal file
15
core/mail/models.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from django.db import models
|
||||
from django_softdelete.models import SoftDeleteModel
|
||||
from inventory.models import Event
|
||||
from issues.models import IssueThread
|
||||
|
||||
|
||||
class Email(SoftDeleteModel):
|
||||
id = models.AutoField(primary_key=True)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
body = models.TextField()
|
||||
subject = models.CharField(max_length=255)
|
||||
sender = models.CharField(max_length=255)
|
||||
recipient = models.CharField(max_length=255)
|
||||
event = models.ForeignKey(Event, models.SET_DEFAULT, default=1)
|
||||
issue = models.ForeignKey(IssueThread, models.SET_NULL, null=True)
|
62
core/mail/protocol.py
Normal file
62
core/mail/protocol.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
import asyncio
|
||||
import logging
|
||||
|
||||
from core.globals import create_task
|
||||
from mail.models import Email
|
||||
from notify_sessions.models import trigger_event
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
|
||||
def make_reply(message, to, subject):
|
||||
from email.message import EmailMessage
|
||||
from core.settings import MAIL_DOMAIN
|
||||
|
||||
reply = EmailMessage()
|
||||
reply["From"] = "noreply@" + MAIL_DOMAIN
|
||||
reply["To"] = to
|
||||
reply["Subject"] = subject
|
||||
reply.set_content(message)
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
async def send_smtp(message):
|
||||
import aiosmtplib
|
||||
log = logging.getLogger('mail.log')
|
||||
log.info('Sending message to %s' % message['To'])
|
||||
|
||||
await aiosmtplib.send(message, hostname="127.0.0.1", port=25, use_tls=False, start_tls=False)
|
||||
|
||||
|
||||
async def deleyed_actions(from_email, reference):
|
||||
await asyncio.sleep(1)
|
||||
await trigger_event(None, "email received", reference)
|
||||
await send_smtp(make_reply("Thank you for your message.", from_email, 'Message received'))
|
||||
|
||||
|
||||
class LMTPHandler:
|
||||
async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
|
||||
from core.settings import MAIL_DOMAIN
|
||||
if not address.endswith('@' + MAIL_DOMAIN):
|
||||
return '550 not relaying to that domain'
|
||||
envelope.rcpt_tos.append(address)
|
||||
return '250 OK'
|
||||
|
||||
async def handle_DATA(self, server, session, envelope):
|
||||
log = logging.getLogger('mail.log')
|
||||
log.info('Message from %s' % envelope.mail_from)
|
||||
log.info('Message for %s' % envelope.rcpt_tos)
|
||||
log.info('Message data:\n')
|
||||
for ln in envelope.content.decode('utf8', errors='replace').splitlines():
|
||||
log.info(f'> {ln}'.strip())
|
||||
log.info('End of message')
|
||||
|
||||
mail = await sync_to_async(Email.objects.create, thread_sensitive=True)(
|
||||
body=envelope.content.decode('utf8', errors='replace'),
|
||||
subject=envelope.subject,
|
||||
sender=envelope.mail_from,
|
||||
recipient=envelope.rcpt_tos[0],
|
||||
)
|
||||
create_task(deleyed_actions(envelope.mail_from, mail.id))
|
||||
|
||||
return '250 Message accepted for delivery'
|
31
core/mail/socket.py
Normal file
31
core/mail/socket.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from abc import ABCMeta
|
||||
|
||||
from aiosmtpd.controller import BaseController, UnixSocketMixin
|
||||
from aiosmtpd.lmtp import LMTP
|
||||
|
||||
|
||||
class BaseAsyncController(BaseController, metaclass=ABCMeta):
|
||||
def __init__(
|
||||
self,
|
||||
handler,
|
||||
loop,
|
||||
**SMTP_parameters,
|
||||
):
|
||||
super().__init__(
|
||||
handler,
|
||||
loop,
|
||||
**SMTP_parameters,
|
||||
)
|
||||
|
||||
def serve(self):
|
||||
return self._create_server()
|
||||
|
||||
|
||||
class UnixSocketLMTPController(UnixSocketMixin, BaseAsyncController):
|
||||
def factory(self):
|
||||
return LMTP(self.handler)
|
||||
|
||||
def _trigger_server(self): # pragma: no-unixsock
|
||||
# Prevent confusion on which _trigger_server() to invoke.
|
||||
# Or so LGTM.com claimed
|
||||
UnixSocketMixin._trigger_server(self)
|
0
core/mail/tests/__init__.py
Normal file
0
core/mail/tests/__init__.py
Normal file
25
core/notify_sessions/models.py
Normal file
25
core/notify_sessions/models.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from asgiref.sync import sync_to_async
|
||||
from channels.layers import get_channel_layer
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
user = models.ForeignKey(User, models.SET_NULL, null=True)
|
||||
type = models.CharField(max_length=255)
|
||||
reference = models.IntegerField(blank=True, null=True)
|
||||
|
||||
|
||||
async def trigger_event(user, type, reference=None):
|
||||
event = await sync_to_async(Event.objects.create, thread_sensitive=True)(user=user, type=type, reference=reference)
|
||||
channel_layer = get_channel_layer()
|
||||
await channel_layer.group_send(
|
||||
'generals',
|
||||
{
|
||||
'name': 'send_message_to_frontend',
|
||||
'message': "event_trigered_from_views",
|
||||
'event_id': event.id,
|
||||
}
|
||||
)
|
0
core/notify_sessions/tests/__init__.py
Normal file
0
core/notify_sessions/tests/__init__.py
Normal file
57
core/notify_sessions/tests/test_notify_socket.py
Normal file
57
core/notify_sessions/tests/test_notify_socket.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
from django.test import TestCase
|
||||
from channels.testing import ApplicationCommunicator, WebsocketCommunicator
|
||||
|
||||
from notify_sessions.consumers import NotifyConsumer
|
||||
|
||||
|
||||
# class NotifyConsumerTestCase(TestCase):
|
||||
#
|
||||
# async def test_connect(self):
|
||||
# communicator = ApplicationCommunicator(NotifyConsumer.as_asgi(), {
|
||||
# "type": "websocket.connect",
|
||||
# "path": "/ws/2/notify/",
|
||||
# "headers": [
|
||||
# (b'host', b'localhost:8000'),
|
||||
# (b'connection', b'upgrade'),
|
||||
# (b'pragma', b'no-cache'),
|
||||
# (b'cache-control', b'no-cache'),
|
||||
# (b'upgrade', b'websocket'),
|
||||
# (b'origin', b'http://localhost:8000'),
|
||||
# (b'sec-websocket-version', b'13'),
|
||||
# (b'user-agent', b'Mozilla/5.0 (X11; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0'),
|
||||
# (b'accept', b'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'),
|
||||
# (b'accept-language', b'en-US,en;q=0.5'),
|
||||
# (b'sec-websocket-key', b''),
|
||||
# (b'sec-websocket-extensions', b'permessage-deflate'),
|
||||
# ],
|
||||
# "query_string": b"",
|
||||
# "client": [],
|
||||
# "server": [],
|
||||
# })
|
||||
# await communicator.send_input({
|
||||
# "type": "websocket.receive",
|
||||
# "text": '{"name":"foo","message":"bar"}',
|
||||
# })
|
||||
|
||||
|
||||
class NotifyWebsocketTestCase(TestCase):
|
||||
|
||||
async def test_connect(self):
|
||||
communicator = WebsocketCommunicator(NotifyConsumer.as_asgi(), "/ws/2/notify/")
|
||||
connected, subprotocol = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_send_message(self):
|
||||
communicator = WebsocketCommunicator(NotifyConsumer.as_asgi(), "/ws/2/notify/")
|
||||
connected, subprotocol = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
await communicator.send_json_to({
|
||||
"name": "foo",
|
||||
"message": "bar",
|
||||
})
|
||||
response = await communicator.receive_json_from()
|
||||
self.assertEqual(response, {
|
||||
"message": "bar",
|
||||
})
|
||||
await communicator.disconnect()
|
|
@ -1,30 +1,18 @@
|
|||
aiosmtpd==1.4.4.post2
|
||||
aiosmtplib==3.0.1
|
||||
anyio==4.0.0
|
||||
asgiref==3.7.2
|
||||
async-timeout==4.0.3
|
||||
atpublic==4.0
|
||||
attrs==23.1.0
|
||||
certifi==2023.11.17
|
||||
channels==4.0.0
|
||||
channels-redis==4.1.0
|
||||
charset-normalizer==3.3.2
|
||||
click==8.1.7
|
||||
coreapi==2.3.3
|
||||
coreschema==0.0.4
|
||||
coverage==7.3.2
|
||||
Django==4.2.7
|
||||
django-extensions==3.2.3
|
||||
django-mysql==4.12.0
|
||||
django-soft-delete==0.9.21
|
||||
djangorestframework==3.14.0
|
||||
drf-yasg==1.21.7
|
||||
h11==0.14.0
|
||||
httptools==0.6.1
|
||||
idna==3.4
|
||||
inflection==0.5.1
|
||||
itypes==1.2.0
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.3
|
||||
msgpack==1.0.7
|
||||
msgpack-python==0.5.6
|
||||
|
@ -35,11 +23,9 @@ Pillow==10.1.0
|
|||
python-dotenv==1.0.0
|
||||
pytz==2023.3.post1
|
||||
PyYAML==6.0.1
|
||||
redis==5.0.1
|
||||
requests==2.31.0
|
||||
sdnotify==0.3.2
|
||||
setproctitle==1.3.3
|
||||
six==1.16.0
|
||||
sniffio==1.3.0
|
||||
sqlparse==0.4.4
|
||||
typing_extensions==4.8.0
|
||||
|
|
|
@ -43,7 +43,7 @@ server {
|
|||
proxy_pass http://c3lf-sys3;
|
||||
}
|
||||
|
||||
location /admin {
|
||||
location /djangoadmin {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
|
|
@ -23,7 +23,9 @@ export default {
|
|||
components: {Toast, Navbar, AddItemModal},
|
||||
computed: mapState(['loadedItems', 'layout', 'toasts']),
|
||||
data: () => ({
|
||||
addModalOpen: false
|
||||
addModalOpen: false,
|
||||
notify_socket: null,
|
||||
socket_toast: null,
|
||||
}),
|
||||
methods: {
|
||||
...mapMutations(['removeToast', 'createToast']),
|
||||
|
@ -32,35 +34,64 @@ export default {
|
|||
},
|
||||
closeAddModal() {
|
||||
this.addModalOpen = false;
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.notify_socket = new WebSocket('wss://' + window.location.host + '/ws/notify/');
|
||||
this.notify_socket.onmessage = (e) => {
|
||||
let data = JSON.parse(e.data);
|
||||
console.log(data, e.data);
|
||||
};
|
||||
tryConnect() {
|
||||
if (!this.notify_socket || this.notify_socket.readyState !== WebSocket.OPEN) {
|
||||
if (this.socket_toast) {
|
||||
this.removeToast(this.socket_toast.key);
|
||||
this.socket_toast = null;
|
||||
}
|
||||
this.socket_toast = this.createToast({
|
||||
title: "Connecting...",
|
||||
message: "Connecting to websocket...",
|
||||
color: "warning"
|
||||
});
|
||||
this.notify_socket = new WebSocket('wss://' + window.location.host + '/ws/2/notify/');
|
||||
this.notify_socket.onopen = (e) => {
|
||||
this.createToast({
|
||||
if (this.socket_toast) {
|
||||
this.removeToast(this.socket_toast.key);
|
||||
this.socket_toast = null;
|
||||
}
|
||||
this.socket_toast = this.createToast({
|
||||
title: "Connection established",
|
||||
message: JSON.stringify(e),
|
||||
color: "success"
|
||||
});
|
||||
};
|
||||
this.notify_socket.onclose = (e) => {
|
||||
this.createToast({
|
||||
if (this.socket_toast) {
|
||||
this.removeToast(this.socket_toast.key);
|
||||
this.socket_toast = null;
|
||||
}
|
||||
this.socket_toast = this.createToast({
|
||||
title: "Connection closed",
|
||||
message: JSON.stringify(e),
|
||||
color: "danger"
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.tryConnect();
|
||||
}, 1000);
|
||||
};
|
||||
this.notify_socket.onerror = (e) => {
|
||||
this.createToast({
|
||||
if (this.socket_toast) {
|
||||
this.removeToast(this.socket_toast.key);
|
||||
this.socket_toast = null;
|
||||
}
|
||||
this.socket_toast = this.createToast({
|
||||
title: "Connection error",
|
||||
message: JSON.stringify(e),
|
||||
color: "danger"
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.tryConnect();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.tryConnect();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -77,7 +77,11 @@ export default {
|
|||
//{'title':'mass-edit','path':'massedit'},
|
||||
],
|
||||
links: [
|
||||
{'title':'howto engel','path':'/howto/'}
|
||||
{'title':'howto engel','path':'/howto/'},
|
||||
{'title':'events','path':'/admin/events/'},
|
||||
{'title':'files','path':'/admin/files/'},
|
||||
{'title':'users','path':'/admin/users/'},
|
||||
{'title':'debug','path':'/admin/debug/'},
|
||||
]
|
||||
}),
|
||||
computed: {
|
||||
|
|
|
@ -6,6 +6,7 @@ import Error from './views/Error';
|
|||
import HowTo from './views/HowTo';
|
||||
import VueRouter from 'vue-router';
|
||||
import Vue from 'vue';
|
||||
import Debug from "@/views/Debug.vue";
|
||||
|
||||
|
||||
|
||||
|
@ -16,6 +17,7 @@ const routes = [
|
|||
{ path: '/howto', name: 'howto', component: HowTo},
|
||||
{ path: '/admin/files', name: 'files', component: Files},
|
||||
{ path: '/admin/events', name: 'events', component: Events},
|
||||
{ path: '/admin/debug', name: 'debug', component: Debug},
|
||||
{ path: '/:event/boxes', name: 'boxes', component: Boxes},
|
||||
{ path: '/:event/items', name: 'items', component: Items},
|
||||
{ path: '/:event/box/:uid', name: 'boxes', component: Boxes},
|
||||
|
|
|
@ -48,7 +48,7 @@ const store = new Vuex.Store({
|
|||
lastUsed: localStorage.getItem('lf_lastUsed') || {},
|
||||
},
|
||||
getters: {
|
||||
getEventSlug: state => state.route && state.route.params.event? state.route.params.event : state.events.length ? state.events[0].slug : '36C3',
|
||||
getEventSlug: state => state.route && state.route.params.event ? state.route.params.event : state.events.length ? state.events[0].slug : '36C3',
|
||||
getActiveView: state => state.route.name || 'items',
|
||||
getFilters: state => state.route.query,
|
||||
getBoxes: state => state.loadedBoxes
|
||||
|
@ -74,68 +74,70 @@ const store = new Vuex.Store({
|
|||
state.loadedBoxes = loadedBoxes;
|
||||
},
|
||||
updateItem(state, updatedItem) {
|
||||
const item = state.loadedItems.filter(({ uid }) => uid === updatedItem.uid)[0];
|
||||
const item = state.loadedItems.filter(({uid}) => uid === updatedItem.uid)[0];
|
||||
Object.assign(item, updatedItem);
|
||||
},
|
||||
removeItem(state, item) {
|
||||
state.loadedItems = state.loadedItems.filter(it => it !== item );
|
||||
state.loadedItems = state.loadedItems.filter(it => it !== item);
|
||||
},
|
||||
appendItem(state, item) {
|
||||
state.loadedItems.push(item);
|
||||
},
|
||||
createToast(state, { title, message, color }) {
|
||||
state.toasts.push({ title, message, color, key: state.keyIncrement });
|
||||
createToast(state, {title, message, color}) {
|
||||
var toast = {title, message, color, key: state.keyIncrement}
|
||||
state.toasts.push(toast);
|
||||
state.keyIncrement += 1;
|
||||
return toast;
|
||||
},
|
||||
removeToast(state, key) {
|
||||
state.toasts = state.toasts.filter(toast => toast.key !== key);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async loadEvents({ commit }) {
|
||||
const { data } = await axios.get('/1/events');
|
||||
async loadEvents({commit}) {
|
||||
const {data} = await axios.get('/1/events');
|
||||
commit('replaceEvents', data);
|
||||
},
|
||||
changeEvent({ dispatch, getters}, eventName) {
|
||||
changeEvent({dispatch, getters}, eventName) {
|
||||
router.push({path: `/${eventName.slug}/${getters.getActiveView}`});
|
||||
dispatch('loadEventItems');
|
||||
},
|
||||
changeView({ getters }, link) {
|
||||
changeView({getters}, link) {
|
||||
router.push({path: `/${getters.getEventSlug}/${link.path}`});
|
||||
},
|
||||
showBoxContent({ getters }, box) {
|
||||
showBoxContent({getters}, box) {
|
||||
router.push({path: `/${getters.getEventSlug}/items`, query: {box}});
|
||||
},
|
||||
async loadEventItems({ commit, getters }) {
|
||||
const { data } = await axios.get(`/1/${getters.getEventSlug}/items`);
|
||||
async loadEventItems({commit, getters}) {
|
||||
const {data} = await axios.get(`/1/${getters.getEventSlug}/items`);
|
||||
commit('replaceLoadedItems', data);
|
||||
},
|
||||
async searchEventItems({ commit, getters }, query) {
|
||||
async searchEventItems({commit, getters}, query) {
|
||||
const foo = utf8.encode(query);
|
||||
const bar = base64.encode(foo);
|
||||
|
||||
const {data} = await axios.get(`/1/${getters.getEventSlug}/items/${bar}`);
|
||||
commit('replaceLoadedItems', data);
|
||||
},
|
||||
async loadBoxes({ commit }) {
|
||||
const { data } = await axios.get('/1/boxes');
|
||||
async loadBoxes({commit}) {
|
||||
const {data} = await axios.get('/1/boxes');
|
||||
commit('replaceBoxes', data);
|
||||
},
|
||||
async updateItem({ commit, getters }, item) {
|
||||
const { data } = await axios.put(`/1/${getters.getEventSlug}/item/${item.uid}`, item);
|
||||
async updateItem({commit, getters}, item) {
|
||||
const {data} = await axios.put(`/1/${getters.getEventSlug}/item/${item.uid}`, item);
|
||||
commit('updateItem', data);
|
||||
},
|
||||
async markItemReturned({ commit, getters }, item) {
|
||||
async markItemReturned({commit, getters}, item) {
|
||||
await axios.put(`/1/${getters.getEventSlug}/item/${item.uid}`, {returned: true});
|
||||
commit('removeItem', item);
|
||||
},
|
||||
async deleteItem({ commit, getters }, item) {
|
||||
async deleteItem({commit, getters}, item) {
|
||||
await axios.delete(`/1/${getters.getEventSlug}/item/${item.uid}`, item);
|
||||
commit('removeItem',item);
|
||||
commit('removeItem', item);
|
||||
},
|
||||
async postItem({ commit, getters }, item) {
|
||||
commit('updateLastUsed',{box: item.box, cid: item.cid});
|
||||
const { data } = await axios.post(`/1/${getters.getEventSlug}/item`, item);
|
||||
async postItem({commit, getters}, item) {
|
||||
commit('updateLastUsed', {box: item.box, cid: item.cid});
|
||||
const {data} = await axios.post(`/1/${getters.getEventSlug}/item`, item);
|
||||
commit('appendItem', data);
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +145,7 @@ const store = new Vuex.Store({
|
|||
|
||||
export default store;
|
||||
|
||||
store.dispatch('loadEvents').then(() =>{
|
||||
store.dispatch('loadEvents').then(() => {
|
||||
store.dispatch('loadEventItems');
|
||||
store.dispatch('loadBoxes');
|
||||
});
|
||||
|
|
39
web/src/views/Debug.vue
Normal file
39
web/src/views/Debug.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="container-fluid px-xl-5 mt-3">
|
||||
<div class="row">
|
||||
<div class="col-xl-8 offset-xl-2">
|
||||
<Table
|
||||
:columns="['slug', 'name']"
|
||||
:items="events"
|
||||
:keyName="'slug'"
|
||||
v-slot="{ item }"
|
||||
>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary" @click.stop="changeEvent(item)" >
|
||||
<font-awesome-icon icon="archive"/> use
|
||||
</button>
|
||||
<button class="btn btn-danger" @click.stop="" >
|
||||
<font-awesome-icon icon="trash"/> delete
|
||||
</button>
|
||||
</div>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapState} from 'vuex';
|
||||
import Table from '@/components/Table';
|
||||
|
||||
export default {
|
||||
name: 'Debug',
|
||||
components: {Table},
|
||||
computed: mapState(['events']),
|
||||
methods: mapActions(['changeEvent']),
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
Loading…
Reference in a new issue