@@ -10,6 +10,7 @@ import MultifactorAuthPage from '../auth/MultifactorAuthPage' | |||
import ProfilePage from '../account/profile/ProfilePage' | |||
import ActionPage from '../account/profile/ActionPage' | |||
import PolicyPage from '../account/profile/PolicyPage' | |||
import DevicesPage from '../account/DevicesPage' | |||
import NotificationsPage from '../account/NotificationsPage' | |||
import ChangePasswordPage from '../account/profile/ChangePasswordPage' | |||
import SshKeysPage from '../account/profile/SshKeysPage' | |||
@@ -51,6 +52,7 @@ export const router = new Router({ | |||
{ path: '/me/action', component: ActionPage }, | |||
{ path: '/me/changePassword', component: ChangePasswordPage }, | |||
{ path: '/me/keys', component: SshKeysPage }, | |||
{ path: '/devices', component: DevicesPage }, | |||
{ path: '/notifications', component: NotificationsPage }, | |||
{ | |||
path: '/bubbles', component: NetworksPage , | |||
@@ -0,0 +1,20 @@ | |||
import config from 'config'; | |||
import { util } from '../_helpers'; | |||
export const deviceService = { | |||
getDevicesByUserId, | |||
addDeviceByUserId, | |||
removeDeviceByUserId | |||
}; | |||
function getDevicesByUserId(userId, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/devices`, util.getWithAuth()).then(util.handleCrudResponse(messages, errors)); | |||
} | |||
function addDeviceByUserId(userId, device, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/devices`, util.putWithAuth(device)).then(util.handleCrudResponse(messages, errors)); | |||
} | |||
function removeDeviceByUserId(userId, deviceId, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/devices/${deviceId}`, util.deleteWithAuth()).then(util.handleCrudResponse(messages, errors)); | |||
} |
@@ -6,3 +6,4 @@ export * from './footprint.service'; | |||
export * from './paymentMethod.service'; | |||
export * from './accountPlan.service'; | |||
export * from './network.service'; | |||
export * from './device.service'; |
@@ -0,0 +1,91 @@ | |||
import { deviceService } from '../_services'; | |||
import { util } from '../_helpers'; | |||
const state = { | |||
loading: { | |||
devices: false, addDevice: false, removeDevice: false | |||
}, | |||
error: null, | |||
devices: [], | |||
device: null | |||
}; | |||
const actions = { | |||
getDevicesByUserId({ commit }, {userId, messages, errors}) { | |||
commit('getDevicesByUserIdRequest'); | |||
deviceService.getDevicesByUserId(userId, messages, errors) | |||
.then( | |||
devices => commit('getDevicesByUserIdSuccess', devices), | |||
error => commit('getDevicesByUserIdFailure', error) | |||
); | |||
}, | |||
addDeviceByUserId({ commit }, {userId, device, messages, errors}) { | |||
commit('addDeviceByUserIdRequest'); | |||
deviceService.addDeviceByUserId(userId, device, messages, errors) | |||
.then( | |||
device => commit('addDeviceByUserIdSuccess', device), | |||
error => commit('addDeviceByUserIdFailure', error) | |||
); | |||
}, | |||
removeDeviceByUserId({ commit }, {userId, deviceId, messages, errors}) { | |||
commit('removeDeviceByUserIdRequest', deviceId); | |||
deviceService.removeDeviceByUserId(userId, deviceId, messages, errors) | |||
.then( | |||
ok => commit('removeDeviceByUserIdSuccess', deviceId), | |||
error => commit('removeDeviceByUserIdFailure', error) | |||
); | |||
} | |||
}; | |||
const mutations = { | |||
getDevicesByUserIdRequest(state) { | |||
state.loading.devices = true; | |||
}, | |||
getDevicesByUserIdSuccess(state, devices) { | |||
state.loading.devices = false; | |||
state.devices = devices; | |||
}, | |||
getDevicesByUserIdFailure(state, error) { | |||
state.loading.users = false; | |||
state.error = error; | |||
}, | |||
addDeviceByUserIdRequest(state) { | |||
state.loading.addDevice = true; | |||
}, | |||
addDeviceByUserIdSuccess(state, device) { | |||
state.loading.addDevice = false; | |||
state.devices.push(device); | |||
state.device = device; | |||
}, | |||
addDeviceByUserIdFailure(state, error) { | |||
state.loading.addDevice = false; | |||
state.error = error; | |||
}, | |||
removeDeviceByUserIdRequest(state, deviceId) { | |||
state.loading.removeDevice = true; | |||
}, | |||
removeDeviceByUserIdSuccess(state, deviceId) { | |||
state.loading.removeDevice = false; | |||
state.devices = state.devices.filter(function(d) { return d.uuid !== deviceId; }); | |||
}, | |||
removeDeviceByUserIdFailure(state, error) { | |||
state.loading.removeDevice = false; | |||
state.error = error; | |||
} | |||
}; | |||
const getters = { | |||
loading: util.checkLoading(state.loading) | |||
}; | |||
export const devices = { | |||
namespaced: true, | |||
state, | |||
actions, | |||
mutations, | |||
getters | |||
}; |
@@ -11,6 +11,7 @@ import { domains } from './domains.module'; | |||
import { paymentMethods } from './paymentMethods.module'; | |||
import { accountPlans } from './accountPlans.module'; | |||
import { networks } from './networks.module'; | |||
import { devices } from './devices.module'; | |||
Vue.use(Vuex); | |||
@@ -25,7 +26,8 @@ export const store = new Vuex.Store({ | |||
domains, | |||
paymentMethods, | |||
accountPlans, | |||
networks | |||
networks, | |||
devices | |||
} | |||
}); | |||
@@ -53,6 +55,7 @@ String.prototype.parseMessage = function (vue, ctx) { | |||
}; | |||
String.prototype.parseDateMessage = function (millis, messages) { | |||
if (typeof millis === 'undefined' || millis === null || millis === 0) return messages.label_date_undefined; | |||
const date = new Date(millis); | |||
const context = { | |||
YYYY: date.getFullYear(), | |||
@@ -60,7 +63,12 @@ String.prototype.parseDateMessage = function (millis, messages) { | |||
M: messages['label_date_month_short_'+date.getMonth()], | |||
EEE: messages['label_date_day_'+date.getDay()], | |||
E: messages['label_date_day_short_'+date.getDay()], | |||
d: date.getDate() | |||
d: date.getDate(), | |||
H: date.getHours(), | |||
h: (date.getHours() > 12 ? date.getHours() - 12 : date.getHours()+1), | |||
a: (date.getHours() > 12 ? messages['label_date_day_half_pm'] : messages['label_date_day_half_am']), | |||
m: date.getMinutes(), | |||
s: date.getSeconds() | |||
}; | |||
return this ? ''+this.replace(/{{[\w]+?}}/g, match => { | |||
const expression = match.slice(2, -2); | |||
@@ -1,9 +1,10 @@ | |||
import { systemService, userService } from '../_services'; | |||
import { systemService } from '../_services'; | |||
import { account } from "./account.module"; | |||
import { router } from "../_helpers"; | |||
const state = { | |||
configs: { | |||
networkUuid: null, | |||
allowRegistration: false, | |||
paymentsEnabled: false, | |||
sageLauncher: false, | |||
@@ -150,6 +151,10 @@ const getters = { | |||
href: '/me', | |||
title: messages.label_menu_account, | |||
icon: messages.label_menu_account_icon | |||
}, { | |||
href: '/devices', | |||
title: messages.label_menu_devices, | |||
icon: messages.label_menu_devices_icon | |||
}, { | |||
href: '/notifications', | |||
title: messages.label_menu_notifications, | |||
@@ -0,0 +1,115 @@ | |||
<template> | |||
<div> | |||
<em v-if="loading && loading.devices">{{messages.loading_devices}}</em> | |||
<div v-if="devices && devices.length > 0"> | |||
<h2>{{messages.table_title_devices}}</h2> | |||
<table border="1"> | |||
<thead> | |||
<tr> | |||
<th nowrap="nowrap">{{messages.label_field_device_name}}</th> | |||
<!-- <th nowrap="nowrap">{{messages.label_field_device_enabled}}</th>--> | |||
<th>{{messages.label_field_device_ctime}}</th> | |||
<th><!-- delete --></th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<tr v-for="device in devices"> | |||
<td nowrap="nowrap">{{device.name}}</td> | |||
<!-- <td>{{messages['message_'+device.enabled]}}</td>--> | |||
<td nowrap="nowrap">{{messages.label_device_ctime_format.parseDateMessage(device.ctime, messages)}}</td> | |||
<td> | |||
<i @click="removeDevice(device.uuid)" aria-hidden="true" :class="messages.button_label_remove_device_icon" :title="messages.button_label_remove_device"></i> | |||
<span class="sr-only">{{messages.button_label_remove_device}}</span> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
<hr/> | |||
<router-link to="/new_bubble">{{messages.button_label_new_network}}</router-link> | |||
</div> | |||
<div v-if="!devices || devices.length === 0"> | |||
{{messages.message_no_devices}} | |||
</div> | |||
<hr/> | |||
<form @submit.prevent="addDevice()"> | |||
<h4>{{messages.form_title_add_device}}</h4> | |||
<div class="form-group"> | |||
<label htmlFor="name">{{messages.label_field_device_name}}</label> | |||
<input v-model="deviceName" name="name" class="form-control"/> | |||
<div v-if="submitted && errors.has('name')" class="invalid-feedback d-block">{{ errors.first('name') }}</div> | |||
<div v-if="submitted && errors.has('deviceName')" class="invalid-feedback d-block">{{ errors.first('deviceName') }}</div> | |||
</div> | |||
<hr/> | |||
<div class="form-group"> | |||
<button class="btn btn-primary" :disabled="loading()">{{messages.button_label_add_device}}</button> | |||
<img v-show="loading()" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" /> | |||
</div> | |||
</form> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions, mapGetters } from 'vuex'; | |||
import { util } from '../_helpers'; | |||
export default { | |||
data () { | |||
return { | |||
userId: util.currentUser().uuid, | |||
submitted: false, | |||
deviceName: null | |||
}; | |||
}, | |||
computed: { | |||
...mapState('devices', ['devices', 'device']), | |||
...mapState('system', ['messages']) | |||
}, | |||
created () { | |||
this.getDevicesByUserId({ | |||
userId: this.userId, | |||
messages: this.messages, | |||
errors: this.errors | |||
}); | |||
}, | |||
methods: { | |||
...mapActions('devices', ['getDevicesByUserId', 'addDeviceByUserId', 'removeDeviceByUserId']), | |||
...mapGetters('devices', ['loading']), | |||
addDevice () { | |||
this.errors.clear(); | |||
this.submitted = true; | |||
this.addDeviceByUserId({ | |||
userId: this.userId, | |||
device: { | |||
name: this.deviceName | |||
}, | |||
messages: this.messages, | |||
errors: this.errors | |||
}); | |||
}, | |||
removeDevice (id) { | |||
this.errors.clear(); | |||
this.submitted = true; | |||
this.removeDeviceByUserId({ | |||
userId: this.userId, | |||
deviceId: id, | |||
messages: this.messages, | |||
errors: this.errors | |||
}); | |||
} | |||
}, | |||
watch: { | |||
device(dev) { | |||
if (dev) { | |||
// after device added, clear device name field | |||
this.deviceName = null; | |||
} | |||
} | |||
} | |||
}; | |||
</script> |
@@ -1,6 +1,10 @@ | |||
<template> | |||
<div v-if="network"> | |||
<h4>{{network.name}}.{{network.domainName}} - <i>{{messages['msg_network_state_'+network.state]}}</i></h4> | |||
<h4 v-if="network.state === 'running' && configs.networkUuid && network.uuid !== configs.networkUuid"> | |||
<a :href="'https://'+network.name+'.'+network.domainName+':1443/'">{{network.name}}.{{network.domainName}}</a> - <i>{{messages['msg_network_state_'+network.state]}}</i> | |||
</h4> | |||
<h4 v-else>{{network.name}}.{{network.domainName}} - <i>{{messages['msg_network_state_'+network.state]}}</i></h4> | |||
<div v-if="stats && network.state !== 'stopped'"> | |||
<!-- adapted from: https://code-boxx.com/simple-vanilla-javascript-progress-bar/ --> | |||
<div class="progress-wrap"> | |||
@@ -20,7 +24,7 @@ | |||
<table border="1"> | |||
<thead> | |||
<tr> | |||
<th nowrap="nowrap">{{messages.label_field_nodes_fqdn}}</th> | |||
<th nowrap="nowrap">{{messages.label_field_nodes_name}}</th> | |||
<th nowrap="nowrap">{{messages.label_field_nodes_region}}</th> | |||
<th nowrap="nowrap">{{messages.label_field_nodes_ip4}}</th> | |||
<th nowrap="nowrap">{{messages.label_field_nodes_ip6}}</th> | |||
@@ -29,7 +33,7 @@ | |||
</thead> | |||
<tbody> | |||
<tr v-for="node in networkNodes"> | |||
<td>{{node.fqdn}}</td> | |||
<td>{{node.name}}</td> | |||
<td nowrap="nowrap">{{node.region}}</td> | |||
<td>{{node.ip4}}</td> | |||
<td>{{node.ip6}}</td> | |||
@@ -39,7 +43,7 @@ | |||
</table> | |||
</div> | |||
<div v-if="network.state === 'running'"> | |||
<div v-if="network.state === 'running' && configs.networkUuid && network.uuid === configs.networkUuid"> | |||
<button @click="requestRestoreKey()">{{messages.link_network_action_request_keys}}</button> | |||
<div v-if="errors.has('networkKeys')" class="invalid-feedback d-block">{{ errors.first('networkKeys') }}</div> | |||
<div v-if="networkKeysRequested && networkKeysRequested === networkId">{{messages.message_network_action_keys_requested}}</div> | |||
@@ -66,12 +70,13 @@ | |||
</form> | |||
</div> | |||
<hr/> | |||
<div v-if="network.state === 'stopped'"> | |||
<!-- todo: add button to restart network in restore mode --> | |||
</div> | |||
<div> | |||
<hr/> | |||
<div class="text-danger"><h4>{{messages.title_network_danger_zone}}</h4></div> | |||
<div v-if="errors.has('node')" class="invalid-feedback d-block">{{ errors.first('node') }}</div> | |||
<div v-if="errors.has('accountPlan')" class="invalid-feedback d-block">{{ errors.first('accountPlan') }}</div> | |||
@@ -111,7 +116,7 @@ | |||
'network', 'newNodeNotification', 'networkStatuses', 'networkNodes', 'networkKeysRequested', | |||
'deletedNetwork', 'networkKeys' | |||
]), | |||
...mapState('system', ['messages']) | |||
...mapState('system', ['messages', 'configs']) | |||
}, | |||
methods: { | |||
...mapActions('networks', [ | |||
@@ -34,8 +34,8 @@ | |||
</template> | |||
<script> | |||
import { mapState, mapActions, mapGetters } from 'vuex' | |||
import { util } from '../_helpers' | |||
import { mapState, mapActions, mapGetters } from 'vuex'; | |||
import { util } from '../_helpers'; | |||
export default { | |||
computed: { | |||
@@ -36,7 +36,7 @@ export default { | |||
return { | |||
name: '', | |||
password: '', | |||
unlockKey: null, | |||
unlockKey: (this.$route.params && this.$route.params.k) ? this.$route.params.k : null, | |||
submitted: false | |||
} | |||
}, | |||