moved to bootstrap-vue, there is a bug in vee-validate and bootstrap-vue >rc15

This commit is contained in:
2019-04-23 16:13:18 +02:00
parent c374ae1796
commit 4a97efce6f
26 changed files with 4310 additions and 743 deletions

3102
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,14 +9,20 @@
"test:unit": "vue-cli-service test:unit" "test:unit": "vue-cli-service test:unit"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.17",
"@fortawesome/free-solid-svg-icons": "^5.8.1",
"@fortawesome/vue-fontawesome": "^0.1.6",
"@vue/cli": "^3.6.3", "@vue/cli": "^3.6.3",
"axios": "^0.18.0", "axios": "^0.18.0",
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"bootstrap-vue": "^2.0.0-rc.13", "bootstrap-vue": "^2.0.0-rc.15",
"i": "^0.3.6",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"npm": "^6.9.0",
"popper.js": "^1.15.0", "popper.js": "^1.15.0",
"vue": "^2.6.6", "vee-validate": "^2.2.4",
"vue": "^2.6.10",
"vue-axios": "^2.1.4", "vue-axios": "^2.1.4",
"vue-class-component": "^6.0.0", "vue-class-component": "^6.0.0",
"vue-cookies": "^1.5.13", "vue-cookies": "^1.5.13",
@@ -24,6 +30,7 @@
"vue-i18n": "^8.9.0", "vue-i18n": "^8.9.0",
"vue-property-decorator": "^7.0.0", "vue-property-decorator": "^7.0.0",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-spinner": "^1.0.3",
"vue-sweetalert2": "^1.6.4", "vue-sweetalert2": "^1.6.4",
"vuejs-logger": "^1.5.3", "vuejs-logger": "^1.5.3",
"vuex": "^3.0.1", "vuex": "^3.0.1",
@@ -43,6 +50,6 @@
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"typescript": "^3.0.0", "typescript": "^3.0.0",
"vue-template-compiler": "^2.5.21" "vue-template-compiler": "^2.6.10"
} }
} }

View File

@@ -5,7 +5,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!--<link href="//netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" />-->
<style> <style>
a { cursor: pointer; } a { cursor: pointer; }
</style> </style>

View File

@@ -1,14 +1,26 @@
<template> <template>
<div id="app"> <div id="app">
<sync-loader :loading="isLoading"></sync-loader>
<div id="nav"> <div id="nav">
<router-link to="/">Home</router-link> | <router-link to="/">Home</router-link>
<router-link to="/about">About</router-link> | |
<router-link to="/login">Login</router-link> | <router-link to="/about">About</router-link>
<router-link to="/profile">Profile</router-link> | |
<router-link to="/login">Login</router-link>
|
<router-link to="/profile">Profile</router-link>
|
<router-link :to="{ name: 'rooms' }">{{ $t('rooms') }}</router-link>
|
<router-link :to="{ name: 'recorders' }">{{ $t('recorders') }}</router-link>
|
<router-link :to="{ name: 'commands' }">{{ $t('commands') }}</router-link>
|
<!-- Example split danger button --> <!-- Example split danger button -->
<div class="btn-group"> <div class="btn-group">
<button type="button" onclick="location.href='/admin'" class="btn btn-danger">Admin</button> <button type="button" onclick="location.href='/admin'" class="btn btn-danger">Admin</button>
<button type="button" class="btn btn-danger dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button type="button" class="btn btn-danger dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span> <span class="sr-only">Toggle Dropdown</span>
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
@@ -18,7 +30,8 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Separated link</a> <a class="dropdown-item" href="#">Separated link</a>
</div> </div>
</div> | </div>
|
<span>({{tokenValidity}})</span> | <span>({{tokenValidity}})</span> |
<span>({{refreshTokenValidity}})</span> <span>({{refreshTokenValidity}})</span>
</div> </div>
@@ -28,20 +41,25 @@
</template> </template>
<script> <script>
import {getRemainingJwtValiditySeconds} from "@/utils"; import {getRemainingJwtValiditySeconds} from '@/utils';
import SyncLoader from 'vue-spinner/src/SyncLoader.vue';
export default { export default {
data () { components: {
SyncLoader,
},
data() {
return { return {
isLoading: false,
tokenValidity: -1, tokenValidity: -1,
refreshTokenValidity: -1, refreshTokenValidity: -1,
}; };
}, },
methods: { methods: {
// todo ...
}, },
mounted () { mounted() {
this.$nextTick(() => {
this.$nextTick(() =>{
window.setInterval(() => { window.setInterval(() => {
// this.$log.debug(getRemainingJwtValiditySeconds(this.$store.state.access_token)); // this.$log.debug(getRemainingJwtValiditySeconds(this.$store.state.access_token));
this.tokenValidity = getRemainingJwtValiditySeconds(this.$store.state.access_token); this.tokenValidity = getRemainingJwtValiditySeconds(this.$store.state.access_token);
@@ -52,26 +70,55 @@
}, },
} };
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../node_modules/bootstrap/scss/bootstrap.scss'; @import '../node_modules/bootstrap/scss/bootstrap.scss';
#app {
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif; font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50; color: #2c3e50;
} }
#nav {
#nav {
padding: 30px; padding: 30px;
text-align: center;
a { a {
font-weight: bold; font-weight: bold;
color: #2c3e50; color: #2c3e50;
&.router-link-exact-active { &.router-link-exact-active {
color: #42b983; color: #42b983;
} }
} }
} }
/* Absolute Center Spinner */
.v-spinner {
position: fixed;
z-index: 999;
// height: 2em;
// width: 2em;
overflow: visible;
margin: auto;
top: 50%;
left: 50%;
bottom: 0;
right: 0;
}
/* Transparent Overlay */
.v-spinner:before {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.2);
}
</style> </style>

View File

@@ -1,13 +0,0 @@
// RepositoryFactory.js
import GroupRepository from "./groupRepository";
import UserRepository from "./userRepository";
const repositories = {
group: GroupRepository,
user: UserRepository,
};
export const RepositoryFactory = {
get: name => repositories[name],
};

View File

@@ -0,0 +1,28 @@
// RepositoryFactory.js
import GroupRepository from './groupRepository';
import UserRepository from './userRepository';
import RoomRepository from './roomRepository';
import RecorderRepository from './recorderRepository';
export default function get(name: string) {
switch (name) {
case 'group': {
return GroupRepository;
}
case 'recorder': {
return RecorderRepository;
}
case 'room': {
return RoomRepository;
}
case 'user': {
return UserRepository;
}
default: {
// statements;
break;
}
}
}

View File

@@ -1,19 +1,20 @@
// groupRepository.js // groupRepository.js
import Repository from "./Repository"; // @ts-ignore
import Repository from './Repository';
const resource = "/group"; const resource = '/group';
export default { export default {
getGroups() { getGroups() {
return Repository.get(`${resource}`); return Repository.get(`${resource}`);
}, },
getGroup(groupId) { getGroup(groupId: number) {
return Repository.get(`${resource}/${groupId}`); return Repository.get(`${resource}/${groupId}`);
}, },
createGroup(groupData) { createGroup(groupData: any) {
return Repository.post(`${resource}`, groupData); return Repository.post(`${resource}`, groupData);
}, },
}; };

View File

@@ -58,6 +58,6 @@ export function fetchUserGroup(jwt: any, groupId: any) {
} }
export function fetchProfile(jwt: any) { export function fetchProfile(jwt: any) {
Vue.$log.debug("JWT: "+ jwt); Vue.$log.debug('JWT: ' + jwt);
return axios.get(`${API_URL}/v1/user/profile`, { headers: { Authorization: `Bearer ${jwt}` } }); return axios.get(`${API_URL}/v1/user/profile`, { headers: { Authorization: `Bearer ${jwt}` } });
} }

View File

@@ -0,0 +1,24 @@
// groupRepository.ts
// @ts-ignore
import Repository from './Repository';
const resource = '/recorder';
export default {
getRecorders() {
return Repository.get(`${resource}`);
},
getRecorder(recorderId: number) {
return Repository.get(`${resource}/${recorderId}`);
},
createRecorder(recorderData: any) {
return Repository.post(`${resource}`, recorderData);
},
updateRecorder(recorderId: number, recorderData: any) {
return Repository.put(`${resource}/${recorderId}`, recorderData);
},
};

24
src/api/roomRepository.ts Normal file
View File

@@ -0,0 +1,24 @@
// groupRepository.ts
// @ts-ignore
import Repository from './Repository';
const resource = '/room';
export default {
getRooms() {
return Repository.get(`${resource}`);
},
getRoom(roomId: number) {
return Repository.get(`${resource}/${roomId}`);
},
createRoom(roomData: any) {
return Repository.post(`${resource}`, roomData);
},
updateRoom(roomId: number, roomData: any) {
return Repository.put(`${resource}/${roomId}`, roomData);
},
};

View File

@@ -1,19 +1,20 @@
// groupRepository.js // groupRepository.ts
import Repository from "./Repository"; // @ts-ignore
import Repository from './Repository';
const resource = "/user"; const resource = '/user';
export default { export default {
getUsers() { getUsers() {
return Repository.get(`${resource}`); return Repository.get(`${resource}`);
}, },
getUser(userId) { getUser(userId: number) {
return Repository.get(`${resource}/${userId}`); return Repository.get(`${resource}/${userId}`);
}, },
createUser(userData) { createUser(userData: any) {
return Repository.post(`${resource}`, userData); return Repository.post(`${resource}`, userData);
}, },
}; };

View File

@@ -22,29 +22,31 @@
</template> </template>
<script> <script>
import { EventBus } from '@/utils' import {EventBus} from '@/utils';
export default { export default {
data () { data() {
return { return {
email: '', email: '',
errorMsg: '', errorMsg: '',
}; };
}, },
methods: { methods: {
authenticate () { authenticate() {
this.$store.dispatch('login', { email: this.email, password: this.password }) this.$store.dispatch('login', {email: this.email, password: this.password})
.then(() => this.$router.push('/')); .then(() => this.$router.push('/'));
}, },
}, },
mounted () { mounted() {
this.$log.debug("Admin: mounting..."); this.$log.debug('Admin: mounting...');
}, },
beforeDestroy () { beforeDestroy() {
// todo ...
}, },
computed: { computed: {
// todo ...
}, },
} };
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -12,7 +12,8 @@
<thead> <thead>
<tr> <tr>
<th v-for="col in group_columns" v-on:click="sortTable(col)">{{col}} <th v-for="col in group_columns" v-on:click="sortTable(col)">{{col}}
<div class="arrow" v-if="col == sortColumn" v-bind:class="[ascending ? 'arrow_up' : 'arrow_down']"></div> <div class="arrow" v-if="col == sortColumn"
v-bind:class="[ascending ? 'arrow_up' : 'arrow_down']"></div>
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -26,7 +27,8 @@
<div class="number" <div class="number"
v-for="i in num_group_pages()" v-for="i in num_group_pages()"
v-bind:class="[i == currentPage ? 'active' : '']" v-bind:class="[i == currentPage ? 'active' : '']"
v-on:click="change_page(i)">{{i}}</div> v-on:click="change_page(i)">{{i}}
</div>
</div> </div>
</div> </div>
@@ -50,17 +52,18 @@
</template> </template>
<script> <script>
import { EventBus } from '@/utils' import {EventBus} from '@/utils';
import { RepositoryFactory} from "@/api/RepositoryFactory"; import getRepository from '@/api/RepositoryFactory';
const GroupRepository = RepositoryFactory.get('group');
const GroupRepository = getRepository('group');
export default { export default {
data () { data() {
return { return {
isLoading: false, isLoading: false,
groups: [], groups: [],
errorMsg: '', errorMsg: '',
group: {name:'', description:''}, group: {name: '', description: ''},
sortColumn: '', sortColumn: '',
currentPage: 1, currentPage: 1,
elementsPerPage: 5, elementsPerPage: 5,
@@ -71,7 +74,7 @@
this.isLoading = true; this.isLoading = true;
// const { data } = await GroupRepository.get(); // const { data } = await GroupRepository.get();
GroupRepository.getGroups() GroupRepository.getGroups()
.then(response => { .then((response) => {
this.groups = response.data; this.groups = response.data;
this.isLoading = false; this.isLoading = false;
this.$log.debug(response); this.$log.debug(response);
@@ -79,8 +82,8 @@
}, },
get_group_rows() { get_group_rows() {
var start = (this.currentPage-1) * this.elementsPerPage; const start = (this.currentPage - 1) * this.elementsPerPage;
var end = start + this.elementsPerPage; const end = start + this.elementsPerPage;
return this.groups.slice(start, end); return this.groups.slice(start, end);
}, },
num_group_pages() { num_group_pages() {
@@ -97,19 +100,19 @@
this.sortColumn = col; this.sortColumn = col;
} }
var ascending = this.ascending; const ascending = this.ascending;
this.groups.sort(function(a, b) { this.groups.sort((a, b) => {
if (a[col] > b[col]) { if (a[col] > b[col]) {
return ascending ? 1 : -1 return ascending ? 1 : -1;
} else if (a[col] < b[col]) { } else if (a[col] < b[col]) {
return ascending ? -1 : 1 return ascending ? -1 : 1;
} }
return 0; return 0;
}) });
}, },
createGroup(){ createGroup() {
this.$log.info("Creating new group..."); this.$log.info('Creating new group...');
GroupRepository.createGroup(this.group); GroupRepository.createGroup(this.group);
this.fetch(); this.fetch();
}, },
@@ -118,6 +121,7 @@
this.fetch(); this.fetch();
}, },
beforeDestroy() { beforeDestroy() {
// todo....or not
}, },
computed: { computed: {
group_columns() { group_columns() {
@@ -127,7 +131,7 @@
return Object.keys(this.groups[0]); return Object.keys(this.groups[0]);
}, },
}, },
} };
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -135,6 +139,7 @@
color: red; color: red;
font-weight: bold; font-weight: bold;
} }
table { table {
font-family: 'Open Sans', sans-serif; font-family: 'Open Sans', sans-serif;
width: 750px; width: 750px;
@@ -152,17 +157,21 @@
padding: 8px; padding: 8px;
min-width: 30px; min-width: 30px;
} }
table th:hover { table th:hover {
background: #717699; background: #717699;
} }
table td { table td {
text-align: left; text-align: left;
padding: 8px; padding: 8px;
border-right: 2px solid #7D82A8; border-right: 2px solid #7D82A8;
} }
table td:last-child { table td:last-child {
border-right: none; border-right: none;
} }
table tbody tr:nth-child(2n) td { table tbody tr:nth-child(2n) td {
background: #D4D8F9; background: #D4D8F9;
} }
@@ -184,17 +193,21 @@
padding: 8px; padding: 8px;
min-width: 30px; min-width: 30px;
} }
table th:hover { table th:hover {
background: #717699; background: #717699;
} }
table td { table td {
text-align: left; text-align: left;
padding: 8px; padding: 8px;
border-right: 2px solid #7D82A8; border-right: 2px solid #7D82A8;
} }
table td:last-child { table td:last-child {
border-right: none; border-right: none;
} }
table tbody tr:nth-child(2n) td { table tbody tr:nth-child(2n) td {
background: #D4D8F9; background: #D4D8F9;
} }
@@ -209,9 +222,11 @@
.arrow_down { .arrow_down {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAaCAYAAABPY4eKAAAAAXNSR0IArs4c6QAAAvlJREFUSA29Vk1PGlEUHQaiiewslpUJiyYs2yb9AyRuJGm7c0VJoFXSX9A0sSZN04ULF12YEBQDhMCuSZOm1FhTiLY2Rky0QPlQBLRUsICoIN/0PCsGyox26NC3eTNn3r3n3TvnvvsE1PkwGo3yUqkkEQqFgw2Mz7lWqwng7ztN06mxsTEv8U0Aam5u7r5EInkplUol/f391wAJCc7nEAgE9Uwmkzo4OPiJMa1Wq6cFs7Ozt0H6RqlUDmJXfPIx+qrX69Ti4mIyHA5r6Wq1egND+j+IyW6QAUoul18XiUTDNHaSyGazKcZtdgk8wqhUKh9o/OMvsVgsfHJy0iWqVrcQNRUMBnd6enqc9MjISAmRP3e73T9al3XnbWNjIw2+KY1Gc3imsNHR0YV4PP5+d3e32h3K316TySQFoX2WyWR2glzIO5fLTSD6IElLNwbqnFpbWyO/96lCoai0cZjN5kfYQAYi5H34fL6cxWIZbya9iJyAhULBHAqFVlMpfsV/fHxMeb3er+Vy+VUzeduzwWC45XA4dlD/vEXvdDrj8DvURsYEWK3WF4FA4JQP9mg0WrHZbEYmnpa0NxYgPVObm5teiLABdTQT8a6vrwdRWhOcHMzMzCiXlpb2/yV6qDttMpkeshEzRk4Wo/bfoe4X9vb2amzGl+HoXNT29vZqsVi0sK1jJScG+Xx+HGkL4Tew2TPi5zUdQQt9otPpuBk3e0TaHmMDh1zS7/f780S0zX6Yni+NnBj09fUZUfvudDrNZN+GkQbl8Xi8RLRtHzsB9Hr9nfn5+SjSeWUCXC7XPq5kw53wsNogjZNohYXL2EljstvtrAL70/mVaW8Y4OidRO1/gwgbUMvcqGmcDc9aPvD1gnTeQ+0nmaInokRj0nHh+uvIiVOtVvt2a2vLv7Ky0tL3cRTXIcpPAwMDpq6R4/JXE4vFQ5FI5CN+QTaRSFCYc8vLy1l0rge4ARe5kJ/d27kYkLXoy2Jo4C7K8CZOsEBvb+9rlUp1xNXPL7v3IDwxvPD6AAAAAElFTkSuQmCC') background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAaCAYAAABPY4eKAAAAAXNSR0IArs4c6QAAAvlJREFUSA29Vk1PGlEUHQaiiewslpUJiyYs2yb9AyRuJGm7c0VJoFXSX9A0sSZN04ULF12YEBQDhMCuSZOm1FhTiLY2Rky0QPlQBLRUsICoIN/0PCsGyox26NC3eTNn3r3n3TvnvvsE1PkwGo3yUqkkEQqFgw2Mz7lWqwng7ztN06mxsTEv8U0Aam5u7r5EInkplUol/f391wAJCc7nEAgE9Uwmkzo4OPiJMa1Wq6cFs7Ozt0H6RqlUDmJXfPIx+qrX69Ti4mIyHA5r6Wq1egND+j+IyW6QAUoul18XiUTDNHaSyGazKcZtdgk8wqhUKh9o/OMvsVgsfHJy0iWqVrcQNRUMBnd6enqc9MjISAmRP3e73T9al3XnbWNjIw2+KY1Gc3imsNHR0YV4PP5+d3e32h3K316TySQFoX2WyWR2glzIO5fLTSD6IElLNwbqnFpbWyO/96lCoai0cZjN5kfYQAYi5H34fL6cxWIZbya9iJyAhULBHAqFVlMpfsV/fHxMeb3er+Vy+VUzeduzwWC45XA4dlD/vEXvdDrj8DvURsYEWK3WF4FA4JQP9mg0WrHZbEYmnpa0NxYgPVObm5teiLABdTQT8a6vrwdRWhOcHMzMzCiXlpb2/yV6qDttMpkeshEzRk4Wo/bfoe4X9vb2amzGl+HoXNT29vZqsVi0sK1jJScG+Xx+HGkL4Tew2TPi5zUdQQt9otPpuBk3e0TaHmMDh1zS7/f780S0zX6Yni+NnBj09fUZUfvudDrNZN+GkQbl8Xi8RLRtHzsB9Hr9nfn5+SjSeWUCXC7XPq5kw53wsNogjZNohYXL2EljstvtrAL70/mVaW8Y4OidRO1/gwgbUMvcqGmcDc9aPvD1gnTeQ+0nmaInokRj0nHh+uvIiVOtVvt2a2vLv7Ky0tL3cRTXIcpPAwMDpq6R4/JXE4vFQ5FI5CN+QTaRSFCYc8vLy1l0rge4ARe5kJ/d27kYkLXoy2Jo4C7K8CZOsEBvb+9rlUp1xNXPL7v3IDwxvPD6AAAAAElFTkSuQmCC')
} }
.arrow_up { .arrow_up {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAaCAYAAACgoey0AAAAAXNSR0IArs4c6QAAAwpJREFUSA21Vt1PUmEYP4dvkQ8JFMwtBRocWAkDbiqXrUWXzU1rrTt0bdVqXbb1tbW16C9IBUSmm27cODdneoXjputa6069qwuW6IIBIdLvdaF4OAcOiGeDc87zPs/vd57P96WpFq7p6enbGo1mjKZpeTabjU1MTCRagGnOZHFxcXxtbe1XKpUq7+zslJeXl//Mz8+Hy+Uy3RxSE9qTk5M3otFooVQqgef4Wl9f343FYoEmoISrxuNxFX5f9vb2jhn/PxUKhfLS0tIPfFifUESRUMV8Pv/M6XReRm5rTGQyGeXxeGxYe1ezeBpBOBx2rKysbO7v79d4Wy3Y2Nj4GQqFbgnhaugxwiuGJx99Pp9FLBbXxYTXvTqd7v3MzIy6riIWGxJnMpl7AwMD14xGYyMsSq1WUyQdUqn0eSPlusQIsbGrq+vl4OCgvhFQZd1utyv1en0gEolcqsi47nWJlUrlG5fLZVcoFFy2nDKSDpIWlUoVTCQSEk4lCHmJMZ2GTCbTiMVikfIZ88l7enoos9l8dXt7+z6fDicxSJUokqDX6xXcl2wCROoc0vQCWL3sNfLOSdzR0fHY4XC4tVotl40gmVwup9xuN4OQv+UyqCFGH9rg7SOGYVRcBs3IEG4J0nVnamrqOtvuBDGGgQg9+wHFcVEi4a0LNkbdd6TrPKo8ODc311mteIIYjT/a398/jK+s1jnVM0kXoufCFvq0GuiIGEVgQIhfoygM1QrteEa9dAL7ITiYCt4RMabOK5AyKKzKWtvupLcRciu8D5J0EuDDPyT/Snd39yh6VtY2NhYQSR9G79Ds7OxdskRjEyAufvb7/cPoO5Z6e1+xtVKrq6vfcFzyi/A3ZrPZ3GdNSlwgo5ekE4X2RIQGf2C1WlufFE0GBeGWYQ8YERWLxQtnUVB830MKLZfL9RHir8lkssCn2G751tZWEWe03zTKm15YWPiEiXXTYDB0Ig/t7yd8PRws4EicwWHxO4jHD8/C5HiTTqd1BwcHFozKU89origB+y/kmzgYpgOBQP4fGmUiZmJ+WNgAAAAASUVORK5CYII=') background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAaCAYAAACgoey0AAAAAXNSR0IArs4c6QAAAwpJREFUSA21Vt1PUmEYP4dvkQ8JFMwtBRocWAkDbiqXrUWXzU1rrTt0bdVqXbb1tbW16C9IBUSmm27cODdneoXjputa6069qwuW6IIBIdLvdaF4OAcOiGeDc87zPs/vd57P96WpFq7p6enbGo1mjKZpeTabjU1MTCRagGnOZHFxcXxtbe1XKpUq7+zslJeXl//Mz8+Hy+Uy3RxSE9qTk5M3otFooVQqgef4Wl9f343FYoEmoISrxuNxFX5f9vb2jhn/PxUKhfLS0tIPfFifUESRUMV8Pv/M6XReRm5rTGQyGeXxeGxYe1ezeBpBOBx2rKysbO7v79d4Wy3Y2Nj4GQqFbgnhaugxwiuGJx99Pp9FLBbXxYTXvTqd7v3MzIy6riIWGxJnMpl7AwMD14xGYyMsSq1WUyQdUqn0eSPlusQIsbGrq+vl4OCgvhFQZd1utyv1en0gEolcqsi47nWJlUrlG5fLZVcoFFy2nDKSDpIWlUoVTCQSEk4lCHmJMZ2GTCbTiMVikfIZ88l7enoos9l8dXt7+z6fDicxSJUokqDX6xXcl2wCROoc0vQCWL3sNfLOSdzR0fHY4XC4tVotl40gmVwup9xuN4OQv+UyqCFGH9rg7SOGYVRcBs3IEG4J0nVnamrqOtvuBDGGgQg9+wHFcVEi4a0LNkbdd6TrPKo8ODc311mteIIYjT/a398/jK+s1jnVM0kXoufCFvq0GuiIGEVgQIhfoygM1QrteEa9dAL7ITiYCt4RMabOK5AyKKzKWtvupLcRciu8D5J0EuDDPyT/Snd39yh6VtY2NhYQSR9G79Ds7OxdskRjEyAufvb7/cPoO5Z6e1+xtVKrq6vfcFzyi/A3ZrPZ3GdNSlwgo5ekE4X2RIQGf2C1WlufFE0GBeGWYQ8YERWLxQtnUVB830MKLZfL9RHir8lkssCn2G751tZWEWe03zTKm15YWPiEiXXTYDB0Ig/t7yd8PRws4EicwWHxO4jHD8/C5HiTTqd1BwcHFozKU89origB+y/kmzgYpgOBQP4fGmUiZmJ+WNgAAAAASUVORK5CYII=')
} }
.arrow { .arrow {
float: right; float: right;
width: 12px; width: 12px;
@@ -230,6 +245,7 @@
margin: 0px 5px; margin: 0px 5px;
cursor: pointer; cursor: pointer;
} }
.number:hover, .number.active { .number:hover, .number.active {
background: #717699; background: #717699;
} }

View File

@@ -16,7 +16,7 @@
<h3>Users</h3> <h3>Users</h3>
<ul> <ul>
<li v-for="(user) in users" > <li v-for="(user) in users">
<a href="#">({{user.id}}) {{user.first_name}}</a> <a href="#">({{user.id}}) {{user.first_name}}</a>
</li> </li>
</ul> </ul>
@@ -50,10 +50,11 @@
</template> </template>
<script> <script>
import { EventBus } from '@/utils' import {EventBus} from '@/utils';
export default { export default {
data () {
data() {
return { return {
email: '', email: '',
password: '', password: '',
@@ -62,19 +63,20 @@
}; };
}, },
methods: { methods: {
authenticate () { authenticate() {
this.$store.dispatch('login', { email: this.email, password: this.password }) this.$store.dispatch('login', {email: this.email, password: this.password})
.then(() => this.$router.push('/')); .then(() => this.$router.push('/'));
}, },
register () { register() {
this.$store.dispatch('register', { email: this.email, password: this.password }) this.$store.dispatch('register', {email: this.email, password: this.password})
.then(() => this.$router.push('/')); .then(() => this.$router.push('/'));
}, },
oidc_login () { oidc_login() {
this.$store.dispatch('oidc_login', "\\oidc_login_redirection"); this.$store.dispatch('oidc_login', '\\oidc_login_redirection');
}, },
}, },
mounted () { mounted() {
// this.$parent.$data.isLoading = true;
EventBus.$on('failedRegistering', (msg) => { EventBus.$on('failedRegistering', (msg) => {
this.errorMsg = msg; this.errorMsg = msg;
}); });
@@ -91,22 +93,22 @@
this.$store.dispatch('loadLoginProviders'); this.$store.dispatch('loadLoginProviders');
// get tokens // get tokens
if(this.$cookies.isKey("tokens")){ if (this.$cookies.isKey('tokens')) {
this.$store.dispatch('storeTokens', JSON.parse(atob(this.$cookies.get("tokens")))); this.$store.dispatch('storeTokens', JSON.parse(atob(this.$cookies.get('tokens'))));
this.$cookies.remove("tokens"); this.$cookies.remove('tokens');
} }
console.log(this.$cookies.keys()); this.$log.debug(this.$cookies.keys());
}, },
beforeDestroy () { beforeDestroy() {
EventBus.$off('failedRegistering'); EventBus.$off('failedRegistering');
EventBus.$off('failedAuthentication'); EventBus.$off('failedAuthentication');
}, },
computed: { computed: {
users () { users() {
return this.$store.state.users; return this.$store.state.users;
}, },
}, },
} };
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -1,75 +0,0 @@
<template>
<div>
<div class="field">
<label class="label is-large">Question</label>
<div class="control">
<input type="text" class="input is-large" v-model="question">
</div>
</div>
<div class="field">
<div class="control">
<a class="button is-large is-info" @click="addChoice">
<span class="icon is-small">
<i class="fa fa-plus-square-o fa-align-left" aria-hidden="true"></i>
</span>
<span>Add choice</span>
</a>
<a class="button is-large is-primary" @click="saveQuestion">
<span class="icon is-small">
<i class="fa fa-check"></i>
</span>
<span>Save</span>
</a>
</div>
</div>
<h2 class="label is-large" v-show="choices.length > 0">Question Choices</h2>
<div class="field has-addons" v-for="(choice, idx) in choices" v-bind:key="idx">
<div class="control choice">
<input type="text" class="input is-large" v-model="choices[idx]">
</div>
<div class="control">
<a class="button is-large">
<span class="icon is-small" @click.stop="removeChoice(choice)">
<i class="fa fa-times" aria-hidden="true"></i>
</span>
</a>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
question: '',
choices: []
}
},
methods: {
removeChoice (choice) {
const idx = this.choices.findIndex(c => c === choice)
this.choices.splice(idx, 1)
},
saveQuestion () {
this.$emit('questionComplete', {
question: this.question,
choices: this.choices.filter(c => !!c)
})
this.question = ''
this.choices = ['']
},
addChoice () {
this.choices.push('')
}
}
}
</script>
<style>
.choice {
width: 90%;
}
</style>

View File

@@ -1,122 +0,0 @@
<template>
<div>
<section class="hero is-primary">
<div class="hero-body">
<div class="container has-text-centered">
<h2 class="title">{{ name }}</h2>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="tabs is-centered is-fullwidth is-large">
<ul>
<li :class="{'is-active': step == 'name'}" @click="step = 'name'">
<a>Name</a>
</li>
<li :class="{'is-active': step == 'questions'}" @click="step = 'questions'">
<a>Questions</a>
</li>
<li :class="{'is-active': step == 'review'}" @click="step = 'review'">
<a>Review</a>
</li>
</ul>
</div>
<div class="columns">
<div class="column is-half is-offset-one-quarter">
<div class="name" v-show="step === 'name'">
<div class="field">
<label class="label is-large" for="name">Survey name:</label>
<div class="control">
<input type="text" class="input is-large" id="name" v-model="name">
</div>
</div>
</div>
<div class="questions" v-show="step === 'questions'">
<new-question v-on:questionComplete="appendQuestion"/>
</div>
<div class="review" v-show="step === 'review'">
<ul>
<li class="question" v-for="(question, qIdx) in questions" :key="`question-${qIdx}`">
<div class="title">
{{ question.question }}
<span class="icon is-medium is-pulled-right delete-question"
@click.stop="removeQuestion(question)">
<i class="fa fa-times" aria-hidden="true"></i>
</span>
</div>
<ul>
<li v-for="(choice , cIdx) in question.choices" :key="`choice-${cIdx}`">
{{ cIdx + 1 }}. {{ choice }}
</li>
</ul>
</li>
</ul>
<div class="control">
<a class="button is-large is-primary" @click="submitSurvey">Submit</a>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
import NewQuestion from '@/components/NewQuestion'
export default {
components: { NewQuestion },
data () {
return {
step: 'name',
name: '',
questions: []
}
},
methods: {
appendQuestion (newQuestion) {
this.questions.push(newQuestion)
},
removeQuestion (question) {
const idx = this.questions.findIndex(q => q.question === question.question)
this.questions.splice(idx, 1)
},
submitSurvey () {
this.$store.dispatch('submitNewSurvey', {
name: this.name,
questions: this.questions
})
.then(() => this.$router.push('/'))
.catch((error) => {
console.log('Error creating survey', error)
this.$router.push('/')
})
}
}
}
</script>
<style>
.question {
margin: 10px 20px 25px 10px;
}
.delete-question {
cursor: pointer;
padding: 10px;
}
.delete-question:hover {
background-color: lightgray;
border-radius: 50%;
}
</style>

View File

@@ -17,11 +17,11 @@
</template> </template>
<script> <script>
import { EventBus } from '@/utils' import {EventBus} from '@/utils';
import {getRemainingJwtValiditySeconds} from "../utils"; import {getRemainingJwtValiditySeconds} from '../utils';
export default { export default {
data () { data() {
return { return {
email: '', email: '',
errorMsg: '', errorMsg: '',
@@ -30,19 +30,19 @@
}; };
}, },
methods: { methods: {
authenticate () { authenticate() {
this.$store.dispatch('login', { email: this.email, password: this.password }) this.$store.dispatch('login', {email: this.email, password: this.password})
.then(() => this.$router.push('/')); .then(() => this.$router.push('/'));
}, },
}, },
mounted () { mounted() {
this.$log.debug("Profile: mounting..."); this.$log.debug('Profile: mounting...');
EventBus.$on('failedLoadingProfile', (msg) => { EventBus.$on('failedLoadingProfile', (msg) => {
this.errorMsg = msg; this.errorMsg = msg;
}); });
this.$store.dispatch('loadProfile'); this.$store.dispatch('loadProfile');
this.$nextTick(() =>{ this.$nextTick( () => {
window.setInterval(() => { window.setInterval(() => {
this.$log.debug(getRemainingJwtValiditySeconds(this.$store.state.access_token)); this.$log.debug(getRemainingJwtValiditySeconds(this.$store.state.access_token));
this.tokenValidity = getRemainingJwtValiditySeconds(this.$store.state.access_token); this.tokenValidity = getRemainingJwtValiditySeconds(this.$store.state.access_token);
@@ -52,15 +52,15 @@
}, },
beforeDestroy () { beforeDestroy() {
EventBus.$off('failedLoadingProfile'); EventBus.$off('failedLoadingProfile');
}, },
computed: { computed: {
profile () { profile() {
return this.$store.state.profile; return this.$store.state.profile;
}, },
}, },
} };
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -0,0 +1,271 @@
<template>
<div>
<section class="section">
<h1 class="title">{{ $t('Manage recorders and recorder models') }}</h1>
<p class="subtitle">
{{ $t('List, create, update and delete') }} <strong>{{ $t('recorders') }}</strong> and <strong>recorder models</strong>!
</p>
<hr>
<h2 class="">{{ $t('Create recorder') }}</h2>
<!-- form starts here -->
<form v-on:submit.prevent="$log.debug(form)">
<section class="form">
<div class="field">
<label class="label">{{ $t('name') }}</label>
<div class="control">
<input name="name"
v-model="form.name"
v-validate="'required|min:3'"
v-bind:class="{'is-danger': errors.has('name')}"
class="input" type="text" placeholder="Full name">
</div>
<p class="help is-danger" v-show="errors.has('name')">
{{ errors.first('name') }}
</p>
</div>
<div class="field">
<label class="label">{{ $t('description') }}</label>
<div class="control">
<textarea class="textarea" placeholder="description of recorder"
v-model="form.description"></textarea>
</div>
</div>
<div class="field">
<label class="label">{{ $t('room') }}</label>
<div class="control">
<div class="select">
<select v-model="form.room">
<option disabled value="">No room selected</option>
<option v-for="option in options.room" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
</div>
</div>
</div>
<div class="field">
<label class="label">{{ $t('model') }}</label>
<div class="control">
<div class="select">
<select v-model="form.model">
<option disabled value="">No model selected</option>
<option v-for="option in options.model" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
</div>
</div>
</div>
<div class="field">
<label class="label">LogRocket Usecases</label>
<div class="control">
<div class="select is-multiple">
<select multiple v-model="form.logrocket_usecases">
<option>Debugging</option>
<option>Fixing Errors</option>
<option>User support</option>
</select>
</div>
</div>
</div>
<div class="field">
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="form.terms">
I agree to the <a href="#">terms and conditions</a>
</label>
</div>
</div>
<div class="field">
<label>
<strong>What dev concepts are you interested in?</strong>
</label>
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="form.concepts"
value="promises">
Promises
</label>
<label class="checkbox">
<input type="checkbox" v-model="form.concepts"
value="testing">
Testing
</label>
</div>
</div>
<div class="field">
<label><strong>Is JavaScript awesome?</strong></label>
<div class="control">
<label class="radio">
<input v-model="form.js_awesome" type="radio" value="Yes">
Yes
</label>
<label class="radio">
<input v-model="form.js_awesome" type="radio" value="Yeap!">
Yeap!
</label>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button
v-bind:disabled="errors.any()"
class="button is-primary">
Submit
</button>
</div>
</div>
</section>
</form>
<hr>
<h2 class="">{{ $t('Create recorder model') }}</h2>
<!-- form starts here -->
<form v-on:submit.prevent="$log.debug(form)">
<section class="form">
<div class="field">
<label class="label">{{ $t('model name') }}</label>
<div class="control">
<input name="name"
v-model="form.name"
v-validate="'required|min:3'"
v-bind:class="{'is-danger': errors.has('name')}"
class="input" type="text" placeholder="Full name">
</div>
<p class="help is-danger" v-show="errors.has('name')">
{{ errors.first('name') }}
</p>
</div>
<div class="field">
<label class="label">{{ $t('notes') }}</label>
<div class="control">
<textarea class="textarea" placeholder="Notes"
v-model="form.notes"></textarea>
</div>
</div>
<div class="field">
<label class="label">Recorder commands</label>
<div class="control">
<div class="select is-multiple">
<select multiple v-model="form.commands">
<option v-for="option in recorderCommands.options">
{{ option }}
</option>
</select>
</div>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button
v-bind:disabled="errors.any()"
class="button is-primary">
Create
</button>
</div>
</div>
</section>
</form>
</section>
<div class="column">
<section class="section" id="results">
<div class="box">
<ul>
<!-- loop through all the `form` properties and show their values -->
<li v-for="(item, k) in form">
<strong>{{ k }}:</strong> {{ item }}
</li>
</ul>
</div>
</section>
</div>
</div>
</template>
<script>
export default {
data() {
return {
step: 'name',
name: '',
questions: [],
form: {
name: '',
message: '',
inquiry_type: '',
logrocket_usecases: [],
terms: false,
concepts: [],
js_awesome: '',
},
};
},
methods: {
appendQuestion(newQuestion) {
this.questions.push(newQuestion);
},
removeQuestion(question) {
const idx = this.questions.findIndex((q) => q.question === question.question);
this.questions.splice(idx, 1);
},
submitSurvey() {
this.$store.dispatch('submitNewSurvey', {
name: this.name,
questions: this.questions,
})
.then(() => this.$router.push('/'))
.catch((error) => {
this.$log.error('Error creating survey', error);
this.$router.push('/');
});
},
},
computed: {
options() {
return {
room: [
{value: 'audimax', text: 'AudiMax'},
{value: 'hmu', text: 'Unterer Hörsaal'},
{value: 'hmo', text: 'Oberer Hörsaal'},
],
model: [
{value: 'extron', text: 'extron'},
{value: 'smp', text: 'SMP'},
{value: 'smp2', text: 'SMP2'},
],
};
},
recorderCommands() {
return {
options: [
'reboot',
'reset',
'setStreamingProfile',
],
};
},
},
};
</script>
<style>
</style>

353
src/components/Rooms.vue Normal file
View File

@@ -0,0 +1,353 @@
<template>
<div class="container">
<section class="section">
<h1 class="title">{{ $t('Manage rooms') }}</h1>
<p class="lead">
{{ $t('List, create and delete') }} <strong>{{ $t('rooms') }}</strong>
</p>
<br/>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="room-list-tab" data-toggle="tab" href="#room-list" role="tab"
aria-controls="room-list" aria-selected="true">Room list</a>
</li>
<li class="nav-item">
<a class="nav-link" id="room-create-tab" data-toggle="tab" href="#room-create" role="tab"
aria-controls="room-create" aria-selected="false">Create room</a>
</li>
</ul>
<b-tabs card>
<b-tab title="Room list" active>
<p>{{ $t('There are')}}&nbsp;{{rooms.length}}&nbsp;{{ $t('rooms defined')}}:</p>
<b-card-group deck>
<b-card v-for="(room) in rooms" :title="room.name">
<b-card-text>
<h5 class="card-title">{{ $t('name') }}:&nbsp;{{room.name}}&nbsp;
<a class="float-right badge badge-pill badge-primary">
<font-awesome-icon icon="pencil-alt"/>
</a>
</h5>
<p class="card-text"><strong>{{ $t('alternate_name') }}:</strong>&nbsp;{{room.alternate_name}}&nbsp;
<a class="float-right badge badge-pill badge-info">
<font-awesome-icon icon="pencil-alt"/>
</a>
</p>
<p class="card-text"><strong>{{ $t('room_number') }}:</strong>&nbsp;{{room.number}}&nbsp;
<a class="float-right badge badge-pill badge-info">
<font-awesome-icon icon="pencil-alt"/>
</a></p>
<p class="card-text">
<small><strong>{{ $t('comment') }}:</strong>&nbsp;{{room.comment}}&nbsp;
<a class="float-right badge badge-pill badge-info">
<font-awesome-icon icon="pencil-alt"/>
</a></small>
</p>
<hr/>
<div v-if="room.recorder">
<p class="card-text"><strong>{{ $t('Recorder') }}:</strong>&nbsp;{{room.recorder.name}}&nbsp;
<a class="float-right badge badge-pill badge-info">
<font-awesome-icon icon="pencil-alt"/>
</a></p>
</div>
<div v-else>
<p class="card-text"><strong>{{ $t('Recorder') }}:</strong>&nbsp;{{
$t('no_recorder_defined')}}</p>
<div class="form-group row">
<div class="col-sm-8">
<select class="form-control" v-model="form.recorder">
<option disabled value="">No recorder selected</option>
<option v-for="option in options.recorders" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
</div>
<label class="label col-sm-4 col-form-label">{{ $t('recorder') }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox"
v-model="show_assigned_recorders" id="defaultCheck1">
<label class="form-check-label" for="defaultCheck1">
Show already assigned recorders
</label>
</div>
</div>
</b-card-text>
<div slot="footer">
<small class="text-muted">Last updated 3 mins ago</small>
</div>
</b-card>
<b-card title="Title">
<b-card-text>
This card has supporting text below as a natural lead-in to additional content.
</b-card-text>
<div slot="footer">
<small class="text-muted">Last updated 3 mins ago</small>
</div>
</b-card>
<b-card title="Title">
<b-card-text>
This is a wider card with supporting text below as a natural lead-in to additional
content.
This card has even longer content than the first to show that equal height action.
</b-card-text>
<div slot="footer">
<small class="text-muted">Last updated 3 mins ago</small>
</div>
</b-card>
</b-card-group>
</b-tab>
<b-tab title="Create room">
<b-card-text>
<p>{{ $t('Create a new room')}}:</p>
</b-card-text>
</b-tab>
</b-tabs>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="room-list" role="tabpanel" aria-labelledby="home-tab">
<p>{{ $t('There are')}}&nbsp;{{rooms.length}}&nbsp;{{ $t('rooms defined')}}:</p>
<div class="card-deck">
<div class="card" v-for="(room) in rooms">
<h5 class="card-header">{{room.name}} - {{room.number}}</h5>
<div class="card-body">
<h5 class="card-title">{{ $t('name') }}:&nbsp;{{room.name}}&nbsp;
<a class="float-right badge badge-pill badge-primary">
<font-awesome-icon icon="pencil-alt"/>
</a>
</h5>
<p class="card-text"><strong>{{ $t('alternate_name') }}:</strong>&nbsp;{{room.alternate_name}}&nbsp;
<a class="float-right badge badge-pill badge-info">
<font-awesome-icon icon="pencil-alt"/>
</a>
</p>
<p class="card-text"><strong>{{ $t('room_number') }}:</strong>&nbsp;{{room.number}}&nbsp;
<a class="float-right badge badge-pill badge-info">
<font-awesome-icon icon="pencil-alt"/>
</a></p>
<p class="card-text">
<small><strong>{{ $t('comment') }}:</strong>&nbsp;{{room.comment}}&nbsp;
<a class="float-right badge badge-pill badge-info">
<font-awesome-icon icon="pencil-alt"/>
</a></small>
</p>
<hr/>
<div v-if="room.recorder">
<p class="card-text"><strong>{{ $t('Recorder') }}:</strong>&nbsp;{{room.recorder.name}}&nbsp;
<a class="float-right badge badge-pill badge-info">
<font-awesome-icon icon="pencil-alt"/>
</a></p>
</div>
<div v-else>
<p class="card-text"><strong>{{ $t('Recorder') }}:</strong>&nbsp;{{
$t('no_recorder_defined')}}</p>
<div class="form-group row">
<div class="col-sm-8">
<select class="form-control" v-model="form.recorder">
<option disabled value="">No recorder selected</option>
<option v-for="option in options.recorders" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
</div>
<label class="label col-sm-4 col-form-label">{{ $t('recorder') }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox"
v-model="show_assigned_recorders" id="defaultCheck1">
<label class="form-check-label" for="defaultCheck1">
Show already assigned recorders
</label>
</div>
</div>
</div>
<div class="card-footer text-muted">
<button type="button" class="btn btn-danger">{{ $t('delete') }}&nbsp;<font-awesome-icon
icon="trash"/>
</button>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="room-create" role="tabpanel" aria-labelledby="profile-tab">
<p>{{ $t('Create a new room')}}:</p>
<!-- form starts here -->
<form v-on:submit.prevent="saveRoom()">
<section class="form">
<div class="form-group row">
<label class="label required col-sm-2 col-form-label">{{ $t('name') }}</label>
<div class="col-sm-6">
<input name="name"
v-model="form.name"
v-validate="'required|min:3'"
v-bind:class="{'is-danger': errors.has('name'), 'is-invalid': errors.has('name')}"
class="form-control" type="text" placeholder="Room name" required>
</div>
<p class="col-sm-4" v-show="errors.has('name')">
{{ errors.first('name') }}
</p>
</div>
<div class="form-group row">
<label class="label col-sm-2 col-form-label">{{ $t('alternate_name') }}</label>
<div class="col-sm-6">
<input name="alternate_name"
v-model="form.alternate_name"
class="form-control" type="text" placeholder="Alternate name">
</div>
</div>
<div class="form-group row">
<label class="label required col-sm-2 col-form-label">{{ $t('number') }}</label>
<div class="col-sm-6">
<input name="number"
v-model="form.number"
v-validate="'required|min:3'"
v-bind:class="{'is-danger': errors.has('number'), 'is-invalid': errors.has('number')}"
class="form-control" type="text" placeholder="Room number" required>
</div>
<p class="col-sm-4" v-show="errors.has('number')">
{{ errors.first('number') }}
</p>
</div>
<div class="form-group row">
<label class="label col-sm-2 col-form-label">{{ $t('comment') }}</label>
<div class="col-sm-6">
<textarea class="textarea form-control" placeholder="Comments, remarks, notes, etc."
v-model="form.comment"></textarea>
</div>
</div>
<div class="form-group row">
<label class="label col-sm-2 col-form-label">{{ $t('recorder') }}</label>
<div class="col-sm-6">
<select class="form-control" v-model="form.recorder">
<option disabled value="">No recorder selected</option>
<option v-for="option in options.recorders" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button
v-bind:disabled="errors.any()"
type="submit"
class="btn btn-primary">
Create room
</button>
</div>
</div>
</section>
</form>
</div>
</div>
<hr>
</section>
<div class="column">
<section class="section" id="results">
<div class="box">
<ul>
<!-- loop through all the `form` properties and show their values -->
<li v-for="(item, k) in form">
<strong>{{ k }}:</strong> {{ item }}
</li>
</ul>
</div>
</section>
</div>
</div>
</template>
<script>
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
import getRepository from '@/api/RepositoryFactory';
const RoomRepository = getRepository('room');
export default {
components: {
PulseLoader,
},
props: [],
data() {
return {
show_assigned_recorders: false,
form: {
name: '',
alternate_name: '',
number: '',
comment: '',
recorder: null,
},
};
},
methods: {
saveRoom() {
this.$parent.$data.isLoading = true;
RoomRepository.createRoom(this.form)
.then(() => {
this.$store.dispatch('loadRooms')
.then(() => {
this.$parent.$data.isLoading = false;
});
});
},
},
mounted() {
this.$parent.$data.isLoading = true;
this.$store.dispatch('loadRooms')
.then(() => {
this.$parent.$data.isLoading = false;
});
},
computed: {
options() {
return {
recorders: [
{value: 8, text: 'SMP 351 AudiMax'},
{value: 13, text: 'SMP 351 HMU'},
],
};
},
rooms() {
return this.$store.state.rooms;
},
},
};
</script>
<style>
.comment {
white-space: pre-wrap;
}
.card:hover {
background-color: whitesmoke;
}
.required:after {
content: " *";
}
</style>

View File

@@ -1,115 +0,0 @@
<template>
<div>
<section class="hero is-primary">
<div class="hero-body">
<div class="container has-text-centered">
<h2 class="title">{{ survey.name }}</h2>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="columns">
<div class="column is-10 is-offset-1">
<div
v-for="(question, idx) in survey.questions"
v-bind:key="question.id"
v-show="currentQuestion === idx">
<div class="column is-offset-3 is-6">
<h4 class='title has-text-centered'>{{ question.text }}</h4>
</div>
<div class="column is-offset-4 is-4">
<div class="control">
<div v-for="choice in question.choices" v-bind:key="choice.id">
<label class="radio">
<input type="radio" v-model="question.choice" name="choice" :value="choice.id">
{{ choice.text }}
</label>
</div>
</div>
</div>
</div>
<div class="column is-offset-one-quarter is-half">
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
<a class="pagination-previous" @click.stop="goToPreviousQuestion"><i class="fa fa-chevron-left" aria-hidden="true"></i> &nbsp;&nbsp; Back</a>
<a class="pagination-next" @click.stop="goToNextQuestion">Next &nbsp;&nbsp; <i class="fa fa-chevron-right" aria-hidden="true"></i></a>
</nav>
</div>
<div class="has-text-centered">
<a v-show="surveyComplete" class='button is-large is-focused is-primary' @click="handleSubmit">Submit</a>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
data () {
return {
currentQuestion: 0
}
},
beforeMount () {
this.$store.dispatch('loadSurvey', { id: parseInt(this.$route.params.id) })
},
methods: {
goToNextQuestion () {
if (this.currentQuestion === this.survey.questions.length - 1) {
this.currentQuestion = 0
} else {
this.currentQuestion++
}
},
goToPreviousQuestion () {
if (this.currentQuestion === 0) {
this.currentQuestion = this.survey.questions.lenth - 1
} else {
this.currentQuestion--
}
},
handleSubmit () {
this.$store.dispatch('addSurveyResponse')
.then(() => this.$router.push('/'))
}
},
computed: {
surveyComplete () {
if (this.survey.questions) {
const numQuestions = this.survey.questions.length
const numCompleted = this.survey.questions.filter(q => q.choice).length
return numQuestions === numCompleted
}
return false
},
survey () {
return this.$store.state.currentSurvey
},
selectedChoice: {
get () {
const question = this.survey.questions[this.currentQuestion]
return question.choice
},
set (value) {
const question = this.survey.questions[this.currentQuestion]
this.$store.commit('setChoice', { questionId: question.id, choice: value })
}
}
}
}
</script>
<style>
</style>

View File

@@ -12,7 +12,8 @@
<thead> <thead>
<tr> <tr>
<th v-for="col in columns" v-on:click="sortTable(col)">{{col}} <th v-for="col in columns" v-on:click="sortTable(col)">{{col}}
<div class="arrow" v-if="col == sortColumn" v-bind:class="[ascending ? 'arrow_up' : 'arrow_down']"></div> <div class="arrow" v-if="col == sortColumn"
v-bind:class="[ascending ? 'arrow_up' : 'arrow_down']"></div>
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -26,7 +27,8 @@
<div class="number" <div class="number"
v-for="i in num_pages()" v-for="i in num_pages()"
v-bind:class="[i === currentPage ? 'active' : '']" v-bind:class="[i === currentPage ? 'active' : '']"
v-on:click="change_page(i)">{{i}}</div> v-on:click="change_page(i)">{{i}}
</div>
</div> </div>
</div> </div>
@@ -47,14 +49,16 @@
</template> </template>
<script> <script>
import { EventBus } from '@/utils'; import {EventBus} from '@/utils';
import {getRemainingJwtValiditySeconds} from '../utils'; import {getRemainingJwtValiditySeconds} from '../utils';
import { RepositoryFactory} from '@/api/RepositoryFactory'; import getRepository from '@/api/RepositoryFactory';
const UserRepository = RepositoryFactory.get('user');
const UserRepository = getRepository('user');
export default { export default {
data() { data() {
return { return {
isLoading: false,
user: {}, user: {},
users: [], users: [],
errorMsg: '', errorMsg: '',
@@ -68,7 +72,7 @@
this.isLoading = true; this.isLoading = true;
// const { data } = await GroupRepository.get(); // const { data } = await GroupRepository.get();
UserRepository.getUsers() UserRepository.getUsers()
.then( (response) => { .then((response) => {
this.users = response.data; this.users = response.data;
this.isLoading = false; this.isLoading = false;
this.$log.debug(response); this.$log.debug(response);
@@ -76,8 +80,8 @@
}, },
get_rows() { get_rows() {
let start = (this.currentPage-1) * this.elementsPerPage; const start = (this.currentPage - 1) * this.elementsPerPage;
let end = start + this.elementsPerPage; const end = start + this.elementsPerPage;
return this.users.slice(start, end); return this.users.slice(start, end);
}, },
num_pages() { num_pages() {
@@ -94,19 +98,19 @@
this.sortColumn = col; this.sortColumn = col;
} }
let ascending = this.ascending; const ascending = this.ascending;
this.users.sort(function(a, b) { this.users.sort((a, b) => {
if (a[col] > b[col]) { if (a[col] > b[col]) {
return ascending ? 1 : -1 return ascending ? 1 : -1;
} else if (a[col] < b[col]) { } else if (a[col] < b[col]) {
return ascending ? -1 : 1 return ascending ? -1 : 1;
} }
return 0; return 0;
}) });
}, },
createUser(){ createUser() {
this.$log.info("Creating new user..."); this.$log.info('Creating new user...');
UserRepository.createUser(this.user); UserRepository.createUser(this.user);
this.fetch(); this.fetch();
}, },
@@ -115,6 +119,7 @@
this.fetch(); this.fetch();
}, },
beforeDestroy() { beforeDestroy() {
// todo
}, },
computed: { computed: {
columns() { columns() {
@@ -124,7 +129,7 @@
return Object.keys(this.users[0]); return Object.keys(this.users[0]);
}, },
}, },
} };
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
import axios from 'axios'; import axios from 'axios';
import VueAxios from 'vue-axios'; import VueAxios from 'vue-axios';
import BootstrapVue from 'bootstrap-vue';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import store from './store'; import store from './store';
@@ -8,16 +9,29 @@ import VueSweetalert2 from 'vue-sweetalert2';
import VueCookies from 'vue-cookies'; import VueCookies from 'vue-cookies';
import VueLogger from 'vuejs-logger'; import VueLogger from 'vuejs-logger';
import i18n from '@/plugins/i18n'; import i18n from '@/plugins/i18n';
import VeeValidate from 'vee-validate';
// @ts-ignore // @ts-ignore
import FlagIcon from 'vue-flag-icon'; import FlagIcon from 'vue-flag-icon';
// following is to avoid missing type definitions // following is to avoid missing type definitions
// const FlagIcon = require('vue-flag-icon'); // const FlagIcon = require('vue-flag-icon');
import 'bootstrap'; import { library } from '@fortawesome/fontawesome-svg-core';
import 'bootstrap/dist/css/bootstrap.min.css'; import { faCoffee, faTrash, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
//import 'bootstrap';
//import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
library.add(faCoffee, faTrash, faPencilAlt);
Vue.component('font-awesome-icon', FontAwesomeIcon);
const options = { const options = {
isEnabled: true, isEnabled: true,
// logLevel : isProduction ? 'error' : 'debug', // logLevel : isProduction ? 'error' : 'debug',
@@ -31,9 +45,11 @@ const options = {
Vue.use(VueLogger, options); Vue.use(VueLogger, options);
Vue.use(VueAxios, axios); Vue.use(VueAxios, axios);
Vue.use(BootstrapVue);
Vue.use(FlagIcon); Vue.use(FlagIcon);
Vue.use(VueCookies); Vue.use(VueCookies);
Vue.use(VueSweetalert2); Vue.use(VueSweetalert2);
Vue.use(VeeValidate);
// setup fake backend // setup fake backend
// import { configureFakeBackend } from './helpers'; // import { configureFakeBackend } from './helpers';

View File

@@ -3,13 +3,14 @@ import Router from 'vue-router';
import Home from './views/Home.vue'; import Home from './views/Home.vue';
import NotFound from './views/NotFound.vue'; import NotFound from './views/NotFound.vue';
import Survey from '@/components/Survey.vue'; import NewSurvey from '@/components/Rooms.vue';
import NewSurvey from '@/components/NewSurvey.vue';
import Login from '@/components/Login.vue'; import Login from '@/components/Login.vue';
import Admin from '@/components/Admin.vue'; import Admin from '@/components/Admin.vue';
import Profile from '@/components/Profile.vue'; import Profile from '@/components/Profile.vue';
import User from '@/components/User.vue'; import User from '@/components/User.vue';
import Group from '@/components/Group.vue'; import Group from '@/components/Group.vue';
import Rooms from '@/components/Rooms.vue';
import Recorders from '@/components/Recorders.vue';
import store from '@/store'; import store from '@/store';
Vue.use(Router); Vue.use(Router);
@@ -56,7 +57,19 @@ export const router = new Router({
}, { }, {
path: '/surveys/:id', path: '/surveys/:id',
name: 'Survey', name: 'Survey',
component: Survey, component: Rooms,
}, {
path: '/rooms',
name: 'rooms',
component: Rooms,
}, {
path: '/recorders',
name: 'recorders',
component: Recorders,
}, {
path: '/commands',
name: 'commands',
component: Rooms,
}, { }, {
path: '/surveys', path: '/surveys',
name: 'NewSurvey', name: 'NewSurvey',

View File

@@ -1,6 +1,8 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate'; import createPersistedState from 'vuex-persistedstate';
import getRepository from '@/api/RepositoryFactory';
import RoomRepository from '@/api/roomRepository';
// imports of AJAX functions will go here // imports of AJAX functions will go here
import { import {
@@ -14,13 +16,13 @@ import {
oidc_login, fetchUsers, getFreshToken, fetchProfile, fetchUserGroups, oidc_login, fetchUsers, getFreshToken, fetchProfile, fetchUserGroups,
} from '@/api'; } from '@/api';
import {isValidJwt, EventBus} from '@/utils'; import {isValidJwt, EventBus} from '@/utils';
import {response} from "express";
Vue.use(Vuex); Vue.use(Vuex);
const state = { const state = {
// single source of data // single source of data
surveys: [], surveys: [],
rooms: [],
loginProviders: [], loginProviders: [],
currentSurvey: {}, currentSurvey: {},
profile: {}, profile: {},
@@ -30,30 +32,31 @@ const state = {
refresh_token: '', refresh_token: '',
}; };
// const RoomRepository = getRepository('room');
const actions = { const actions = {
// asynchronous operations // asynchronous operations
loadSurveys(context: any) { loadRooms(context: any) {
return fetchSurveys() return RoomRepository.getRooms()
.then((response) => { .then((response: any) => {
context.commit('setSurveys', { surveys: response.data }); Vue.$log.debug(response);
Vue.$log.debug(response.data);
context.commit('setRooms', {rooms: response.data});
EventBus.$emit('roomsLoaded', response.data);
})
.catch((error: any) => {
Vue.$log.warn('Error loading users!', error);
EventBus.$emit('failedLoadingUsers', error);
}); });
}, },
// @ts-ignore
loadSurvey(context: any, { id }) {
return fetchSurvey(id)
.then((response) => {
context.commit('setSurvey', { survey: response.data });
});
},
addSurveyResponse(context: any) {
return saveSurveyResponse(context.state.currentSurvey);
},
loadUsers(context: any) { loadUsers(context: any) {
return fetchUsers(context.state.access_token) return fetchUsers(context.state.access_token)
.then((response) => { .then((response) => {
Vue.$log.debug(response); Vue.$log.debug(response);
Vue.$log.debug(response.data); Vue.$log.debug(response.data);
context.commit('setUsers', { users: response.data }); context.commit('setUsers', {users: response.data});
EventBus.$emit('usersLoaded', response.data); EventBus.$emit('usersLoaded', response.data);
}) })
.catch((error) => { .catch((error) => {
@@ -66,7 +69,7 @@ const actions = {
.then((response) => { .then((response) => {
Vue.$log.debug(response); Vue.$log.debug(response);
Vue.$log.debug(response.data); Vue.$log.debug(response.data);
context.commit('setUserGroups', { groups: response.data }); context.commit('setUserGroups', {groups: response.data});
EventBus.$emit('groupsLoaded', response.data); EventBus.$emit('groupsLoaded', response.data);
}) })
.catch((error) => { .catch((error) => {
@@ -74,7 +77,7 @@ const actions = {
EventBus.$emit('failedLoadingUserGroups', error); EventBus.$emit('failedLoadingUserGroups', error);
}); });
}, },
loadProfileAuthCheck(context:any){ loadProfileAuthCheck(context: any) {
if (!getters.isAuthenticated) { if (!getters.isAuthenticated) {
EventBus.$emit('accessTokenInvalid'); EventBus.$emit('accessTokenInvalid');
if (!getters.isRefreshTokenValid) { if (!getters.isRefreshTokenValid) {
@@ -82,7 +85,7 @@ const actions = {
EventBus.$emit('refreshTokenInvalid'); EventBus.$emit('refreshTokenInvalid');
EventBus.$emit('accessAndRefreshTokenInvalid'); EventBus.$emit('accessAndRefreshTokenInvalid');
} else { } else {
return context.dispatch('refreshToken').then( () => { return context.dispatch('refreshToken').then(() => {
context.commit('loadProfile'); context.commit('loadProfile');
}); });
} }
@@ -95,7 +98,7 @@ const actions = {
.then((response) => { .then((response) => {
Vue.$log.debug(response); Vue.$log.debug(response);
Vue.$log.debug(response.data); Vue.$log.debug(response.data);
context.commit('setProfile', { profile: response.data }); context.commit('setProfile', {profile: response.data});
EventBus.$emit('profileLoaded', response.data); EventBus.$emit('profileLoaded', response.data);
}) })
.catch((error) => { .catch((error) => {
@@ -111,9 +114,9 @@ const actions = {
}); });
}, },
login(context: any, userData: any) { login(context: any, userData: any) {
context.commit('setUserData', { userData }); context.commit('setUserData', {userData});
return authenticate(userData) return authenticate(userData)
.then((response) => context.commit('setJwtToken', { tokens: response.data })) .then((response) => context.commit('setJwtToken', {tokens: response.data}))
.catch((error) => { .catch((error) => {
Vue.$log.warn('Error Authenticating: ', error); Vue.$log.warn('Error Authenticating: ', error);
EventBus.$emit('failedAuthentication', error); EventBus.$emit('failedAuthentication', error);
@@ -122,7 +125,7 @@ const actions = {
oidc_login(context: any, redirectionUrl: any) { oidc_login(context: any, redirectionUrl: any) {
// context.commit('setUserData', { userData }); // context.commit('setUserData', { userData });
return oidc_login(redirectionUrl) return oidc_login(redirectionUrl)
.then((response) => context.commit('setJwtToken', { tokens: response.data })) .then((response) => context.commit('setJwtToken', {tokens: response.data}))
.catch((error) => { .catch((error) => {
Vue.$log.warn('Error Authenticating: ', error); Vue.$log.warn('Error Authenticating: ', error);
EventBus.$emit('failedAuthentication', error); EventBus.$emit('failedAuthentication', error);
@@ -133,8 +136,9 @@ const actions = {
Vue.$log.debug('Refreshing tokens!'); Vue.$log.debug('Refreshing tokens!');
return getFreshToken(context.state.refresh_token) return getFreshToken(context.state.refresh_token)
.then((response) => { .then((response) => {
context.commit('setTokens', { tokens: response.data }); context.commit('setTokens', {tokens: response.data});
Vue.$log.debug('Tokens refreshed!'); }) Vue.$log.debug('Tokens refreshed!');
})
.catch((error) => { .catch((error) => {
Vue.$log.warn('Error Refreshing token: ', error); Vue.$log.warn('Error Refreshing token: ', error);
EventBus.$emit('failedRefreshingToken', error); EventBus.$emit('failedRefreshingToken', error);
@@ -145,7 +149,7 @@ const actions = {
EventBus.$emit('storedTokens'); EventBus.$emit('storedTokens');
}, },
register(context: any, userData: any) { register(context: any, userData: any) {
context.commit('setUserData', { userData }); context.commit('setUserData', {userData});
return register(userData) return register(userData)
.then(context.dispatch('login', userData)) .then(context.dispatch('login', userData))
.catch((error) => { .catch((error) => {
@@ -170,6 +174,9 @@ const mutations = {
} }
sState.currentSurvey = payload.survey; sState.currentSurvey = payload.survey;
}, },
setRooms(sState: any, payload: any) {
sState.rooms = payload.rooms;
},
setUsers(sState: any, payload: any) { setUsers(sState: any, payload: any) {
sState.users = payload.users; sState.users = payload.users;
}, },
@@ -177,7 +184,7 @@ const mutations = {
sState.userGroups = payload.groups; sState.userGroups = payload.groups;
}, },
setChoice(sState: any, payload: any) { setChoice(sState: any, payload: any) {
const { questionId, choice } = payload; const {questionId, choice} = payload;
const nQuestions = sState.currentSurvey.questions.length; const nQuestions = sState.currentSurvey.questions.length;
for (let i = 0; i < nQuestions; i++) { for (let i = 0; i < nQuestions; i++) {
if (sState.currentSurvey.questions[i].id === questionId) { if (sState.currentSurvey.questions[i].id === questionId) {
@@ -207,10 +214,10 @@ const mutations = {
}, },
setTokens(sState: any, payload: any) { setTokens(sState: any, payload: any) {
Vue.$log.debug('setTokens payload = ', payload); Vue.$log.debug('setTokens payload = ', payload);
if(payload.tokens.access_token){ if (payload.tokens.access_token) {
sState.access_token = payload.tokens.access_token; sState.access_token = payload.tokens.access_token;
} }
if(payload.tokens.refresh_token){ if (payload.tokens.refresh_token) {
sState.refresh_token = payload.tokens.refresh_token; sState.refresh_token = payload.tokens.refresh_token;
} }
Vue.$log.debug('access_token: ' + sState.access_token); Vue.$log.debug('access_token: ' + sState.access_token);

View File

@@ -1,5 +1,6 @@
<template> <template>
<div class="home"> <div class="home">
<b-alert show>Default Alert</b-alert>
<div> <div>
<button v-for="entry in languages" :key="entry.title" @click="changeLocale(entry.language)"> <button v-for="entry in languages" :key="entry.title" @click="changeLocale(entry.language)">
<flag :iso="entry.flag" v-bind:squared="false"/> <flag :iso="entry.flag" v-bind:squared="false"/>

View File

@@ -1,4 +1,7 @@
var path = require('path');
module.exports = { module.exports = {
css: { css: {
loaderOptions: { loaderOptions: {
sass: { sass: {