stash
This commit is contained in:
parent
a83083d0eb
commit
86003a8582
10 changed files with 20860 additions and 100 deletions
20695
frontend/src/assets/css/toolshed.css
Normal file
20695
frontend/src/assets/css/toolshed.css
Normal file
File diff suppressed because it is too large
Load diff
BIN
frontend/src/assets/icons/toolshed-48x48.png
Normal file
BIN
frontend/src/assets/icons/toolshed-48x48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -47,80 +47,7 @@
|
||||||
</form>
|
</form>
|
||||||
<div class="navbar-collapse collapse">
|
<div class="navbar-collapse collapse">
|
||||||
<ul class="navbar-nav navbar-align">
|
<ul class="navbar-nav navbar-align">
|
||||||
<li class="nav-item dropdown">
|
<Notifications :notifications="notifications"/>
|
||||||
<a class="nav-icon dropdown-toggle" href="#" id="alertsDropdown" data-toggle="dropdown">
|
|
||||||
<div class="position-relative">
|
|
||||||
<b-icon-bell class="bi-valign-middle"></b-icon-bell>
|
|
||||||
<span class="indicator">4</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right py-0"
|
|
||||||
aria-labelledby="alertsDropdown">
|
|
||||||
<div class="dropdown-menu-header">
|
|
||||||
4 New Notifications
|
|
||||||
</div>
|
|
||||||
<div class="list-group">
|
|
||||||
<a href="#" class="list-group-item">
|
|
||||||
<div class="row g-0 align-items-center">
|
|
||||||
<div class="col-2">
|
|
||||||
<b-icon-exclamation-circle
|
|
||||||
class="bi-valign-middle text-danger"></b-icon-exclamation-circle>
|
|
||||||
</div>
|
|
||||||
<div class="col-10">
|
|
||||||
<div class="text-dark">Update completed</div>
|
|
||||||
<div class="text-muted small mt-1">Restart server 12 to complete the
|
|
||||||
update.
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small mt-1">30m ago</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="list-group-item">
|
|
||||||
<div class="row g-0 align-items-center">
|
|
||||||
<div class="col-2">
|
|
||||||
<b-icon-bell class="bi-valign-middle text-warning"></b-icon-bell>
|
|
||||||
</div>
|
|
||||||
<div class="col-10">
|
|
||||||
<div class="text-dark">Lorem ipsum</div>
|
|
||||||
<div class="text-muted small mt-1">Aliquam ex eros, imperdiet vulputate
|
|
||||||
hendrerit
|
|
||||||
et.
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small mt-1">2h ago</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="list-group-item">
|
|
||||||
<div class="row g-0 align-items-center">
|
|
||||||
<div class="col-2">
|
|
||||||
<b-icon-house class="bi-valign-middle text-primary"></b-icon-house>
|
|
||||||
</div>
|
|
||||||
<div class="col-10">
|
|
||||||
<div class="text-dark">Login from 192.186.1.8</div>
|
|
||||||
<div class="text-muted small mt-1">5h ago</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<!--{% for notification in top_notifications %}
|
|
||||||
<a href="#" class="list-group-item">
|
|
||||||
<div class="row g-0 align-items-center">
|
|
||||||
<div class="col-2">
|
|
||||||
{% bs_icon 'person-add' extra_classes="bi-valign-middle text-success" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-10">
|
|
||||||
<div class="text-dark">New connection</div>
|
|
||||||
<div class="text-muted small mt-1">Christina accepted your request.</div>
|
|
||||||
<div class="text-muted small mt-1">14h ago</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endfor %}-->
|
|
||||||
</div>
|
|
||||||
<div class="dropdown-menu-footer">
|
|
||||||
<a href="#" class="text-muted">Show all notifications</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-icon dropdown-toggle" href="#" id="messagesDropdown" data-toggle="dropdown">
|
<a class="nav-icon dropdown-toggle" href="#" id="messagesDropdown" data-toggle="dropdown">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
|
@ -268,18 +195,25 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapGetters, mapMutations} from 'vuex';
|
import {mapGetters, mapMutations, mapState} from 'vuex';
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
import Notifications from "@/components/Notifications.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BaseLayout',
|
name: 'BaseLayout',
|
||||||
components: {
|
components: {
|
||||||
|
Notifications,
|
||||||
...BIcons
|
...BIcons
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
//...mapState(['notifications']),
|
||||||
|
...mapGetters(['notifications']),
|
||||||
username() {
|
username() {
|
||||||
return this.$route.params.username
|
return this.$route.params.username
|
||||||
}
|
},
|
||||||
|
top_notifications() {
|
||||||
|
return this.notifications.slice(0, 5)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations(['logout'])
|
...mapMutations(['logout'])
|
||||||
|
|
90
frontend/src/components/Notifications.vue
Normal file
90
frontend/src/components/Notifications.vue
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-icon dropdown-toggle" href="#" id="alertsDropdown" data-toggle="dropdown">
|
||||||
|
<div class="position-relative">
|
||||||
|
<b-icon-bell class="bi-valign-middle"></b-icon-bell>
|
||||||
|
<span :class="['indicator', notificationsColor(top_notifications)]">{{
|
||||||
|
top_notifications.length
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right py-0"
|
||||||
|
aria-labelledby="alertsDropdown">
|
||||||
|
<div class="dropdown-menu-header">
|
||||||
|
{{ top_notifications.length }} New Notifications
|
||||||
|
</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="#" class="list-group-item" v-for="notification in top_notifications" :key="notification.id">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-2">
|
||||||
|
<b-icon-exclamation-circle class="bi-valign-middle text-danger"
|
||||||
|
v-if="notification.type === 'error'"></b-icon-exclamation-circle>
|
||||||
|
<b-icon-info-circle class="bi-valign-middle text-info"
|
||||||
|
v-else-if="notification.type === 'info'"></b-icon-info-circle>
|
||||||
|
<b-icon-check-circle class="bi-valign-middle text-success"
|
||||||
|
v-else-if="notification.type === 'success'"></b-icon-check-circle>
|
||||||
|
<b-icon-bell class="bi-valign-middle text-warning"
|
||||||
|
v-else-if="notification.type === 'warning'"></b-icon-bell>
|
||||||
|
<b-icon-house class="bi-valign-middle text-primary"
|
||||||
|
v-else-if="notification.type === 'login'"></b-icon-house>
|
||||||
|
<b-icon-person-plus class="bi-valign-middle text-success"
|
||||||
|
v-else-if="notification.type === 'friend'"></b-icon-person-plus>
|
||||||
|
</div>
|
||||||
|
<div class="col-10">
|
||||||
|
<div class="text-dark">{{ notification.title }}</div>
|
||||||
|
<div class="text-muted small mt-1" v-if="notification.msg">{{ notification.msg }}</div>
|
||||||
|
<div class="text-muted small mt-1">{{ humanizeTime(notification.time) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu-footer">
|
||||||
|
<a href="#" class="text-muted">Show all notifications</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, mapMutations} from 'vuex';
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Notifications',
|
||||||
|
components: {
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
props: ['notifications'],
|
||||||
|
computed: {
|
||||||
|
top_notifications() {
|
||||||
|
return this.notifications.sort((a, b) => {
|
||||||
|
return new Date(b.time) - new Date(a.time);
|
||||||
|
}).slice(0, 8);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapMutations([]),
|
||||||
|
humanizeTime(time) {
|
||||||
|
return moment(time).fromNow();
|
||||||
|
},
|
||||||
|
notificationsColor(notifications) {
|
||||||
|
if (notifications.length === 0) {
|
||||||
|
return 'invisible';
|
||||||
|
}
|
||||||
|
if (notifications.filter(n => n.type === 'error').length > 0) {
|
||||||
|
return 'bg-danger';
|
||||||
|
}
|
||||||
|
if (notifications.filter(n => n.type === 'warning').length > 0) {
|
||||||
|
return 'bg-warning';
|
||||||
|
}
|
||||||
|
return 'bg-primary';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -33,6 +33,15 @@ class NeighborsCache {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list() {
|
||||||
|
return Object.entries(this._cache).map(([domain, elem]) => {
|
||||||
|
return {
|
||||||
|
domain: domain,
|
||||||
|
time: elem.time
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NeighborsCache;
|
export default NeighborsCache;
|
|
@ -12,16 +12,9 @@ export default createStore({
|
||||||
remember: false,
|
remember: false,
|
||||||
friends: [],
|
friends: [],
|
||||||
item_map: {},
|
item_map: {},
|
||||||
|
//notifications: [],
|
||||||
resolver: new FallBackResolver(),
|
resolver: new FallBackResolver(),
|
||||||
unreachable_neighbors: new NeighborsCache(),
|
unreachable_neighbors: new NeighborsCache(),
|
||||||
/*wk: new Wellknown({
|
|
||||||
update: true, // Will load latest definitions from updateURL.
|
|
||||||
updateURL: new URL(), // URL to load the latest definitions. (default: project URL)
|
|
||||||
persist: false, // True to persist the loaded definitions (nodejs: in filesystem, browser: localStorage)
|
|
||||||
localStoragePrefix: 'dnsquery_', // Prefix for files persisted.
|
|
||||||
maxAge: 300000, // Max age of persisted data to be used in ms.
|
|
||||||
timeout: 5000 // Timeout when loading updates.
|
|
||||||
})*/
|
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setUser(state, user) {
|
setUser(state, user) {
|
||||||
|
@ -105,7 +98,6 @@ export default createStore({
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
async getFriends(state) {
|
async getFriends(state) {
|
||||||
return ['jedi@j3d1.de', 'foobar@example.com', 'foobaz@example.eleon'];
|
return ['jedi@j3d1.de', 'foobar@example.com', 'foobaz@example.eleon'];
|
||||||
|
@ -123,10 +115,11 @@ export default createStore({
|
||||||
(result) => result.map(
|
(result) => result.map(
|
||||||
(answer) => answer.target + ':' + answer.port))
|
(answer) => answer.target + ':' + answer.port))
|
||||||
},
|
},
|
||||||
async apiFederatedGet({state}, url) {
|
async apiFederatedGet({state}, {host, target}) {
|
||||||
if (state.unreachable_neighbors.queryUnreachable(url)) {
|
if (state.unreachable_neighbors.queryUnreachable(host)) {
|
||||||
throw new Error('unreachable neighbor')
|
throw new Error('unreachable neighbor')
|
||||||
}
|
}
|
||||||
|
const url = host + target
|
||||||
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url), state.keypair.signSk)
|
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url), state.keypair.signSk)
|
||||||
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
|
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
|
||||||
return await fetch(url, {
|
return await fetch(url, {
|
||||||
|
@ -134,13 +127,14 @@ export default createStore({
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': auth
|
'Authorization': auth
|
||||||
}
|
}
|
||||||
}).catch( err => state.unreachable_neighbors.unreachable(url)
|
}).catch( err => state.unreachable_neighbors.unreachable(host)
|
||||||
).then(response => response.json())
|
).then(response => response.json())
|
||||||
},
|
},
|
||||||
async apiFederatedPost({state}, {url, data}) {
|
async apiFederatedPost({state}, {host, target, data}) {
|
||||||
if (state.unreachable_neighbors.queryUnreachable(url)) {
|
if (state.unreachable_neighbors.queryUnreachable(host)) {
|
||||||
throw new Error('unreachable neighbor')
|
throw new Error('unreachable neighbor')
|
||||||
}
|
}
|
||||||
|
const url = host + target
|
||||||
const json = JSON.stringify(data)
|
const json = JSON.stringify(data)
|
||||||
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + json), state.keypair.signSk)
|
const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + json), state.keypair.signSk)
|
||||||
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
|
const auth = 'Signature ' + state.user + ':' + nacl.to_hex(signature)
|
||||||
|
@ -151,7 +145,7 @@ export default createStore({
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: json
|
body: json
|
||||||
}).catch( err => state.unreachable_neighbors.unreachable(url)
|
}).catch( err => state.unreachable_neighbors.unreachable(host)
|
||||||
).then(response => response.json())
|
).then(response => response.json())
|
||||||
},
|
},
|
||||||
async apiLocalGet({state}, {target}) {
|
async apiLocalGet({state}, {target}) {
|
||||||
|
@ -181,6 +175,42 @@ export default createStore({
|
||||||
return Object.entries(state.item_map).reduce((acc, [url, items]) => {
|
return Object.entries(state.item_map).reduce((acc, [url, items]) => {
|
||||||
return acc.concat(items)
|
return acc.concat(items)
|
||||||
}, [])
|
}, [])
|
||||||
}
|
},
|
||||||
|
notifications(state) {
|
||||||
|
// supported types: error, warning, info, login, success, friend
|
||||||
|
const u = state.unreachable_neighbors.list().map(elem => {
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
title: elem.domain + ' unreachable',
|
||||||
|
msg: 'The neighbor ' + elem.domain + ' is currently unreachable. Please try again later.',
|
||||||
|
time: elem.time
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return [...u, {
|
||||||
|
type: 'info',
|
||||||
|
title: 'Welcome to the Toolshed',
|
||||||
|
msg: 'This is a federated social network. You can add friends from other servers and share items with them.',
|
||||||
|
time: Date.now()
|
||||||
|
}, {
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Lorem ipsum',
|
||||||
|
msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.',
|
||||||
|
time: Date.now() - 1000 * 60 * 60 * 2
|
||||||
|
}, {
|
||||||
|
type: 'login',
|
||||||
|
title: 'Login from 192.186.1.8',
|
||||||
|
time: Date.now() - 1000 * 60 * 60 * 5
|
||||||
|
}, {
|
||||||
|
type: 'friend',
|
||||||
|
title: 'New connection',
|
||||||
|
msg: 'Christina accepted your request.',
|
||||||
|
time: Date.now() - 1000 * 60 * 60 * 14
|
||||||
|
}, {
|
||||||
|
type: 'success',
|
||||||
|
title: 'Lorem ipsum',
|
||||||
|
msg: 'Aliquam ex eros, imperdiet vulputate hendrerit et.',
|
||||||
|
time: Date.now() - 1000 * 60 * 60 * 24
|
||||||
|
}]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -72,9 +72,11 @@ export default {
|
||||||
async getInventoryItems() {
|
async getInventoryItems() {
|
||||||
try {
|
try {
|
||||||
const servers = await this.getFriends().then(friends => friends.map(friend => this.getFriendServer({username: friend})))
|
const servers = await this.getFriends().then(friends => friends.map(friend => this.getFriendServer({username: friend})))
|
||||||
const urls = servers.map(server => server.then(s => `http://${s}/api/inventory_items/`))
|
const urls = servers.map(server => server.then(s => {
|
||||||
|
return {host: `http://${s}`, target: "/api/inventory_items/"}
|
||||||
|
}))
|
||||||
urls.map(url => url.then(u => this.apiFederatedGet(u).then(items => {
|
urls.map(url => url.then(u => this.apiFederatedGet(u).then(items => {
|
||||||
this.setInventoryItems({url: u, items})
|
this.setInventoryItems({url: u.domain, items})
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
}))) // TODO: handle error
|
}))) // TODO: handle error
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BaseLayout :username="username">
|
<BaseLayout>
|
||||||
<main class="container">
|
<main class="content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BaseLayout :username="username">
|
<BaseLayout>
|
||||||
<main class="container">
|
<main class="content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BaseLayout :username="username">
|
<BaseLayout>
|
||||||
<main class="container">
|
<main class="content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
Loading…
Reference in a new issue