Remove not used property from parameter object Show bubble menu item only to admins on nodes Merge branch 'master' into kris/add_restore_ui # Conflicts: # src/account/NetworkPage.vue Reference method properly within this Merge branch 'master' into kris/add_restore_ui # Conflicts: # src/account/NetworkPage.vue Show link properly for network in restoring state Load backups only when needed for netwrok in restoring state Link for restoring node Use latest stats for network Merge branch 'master' into kris/add_restore_ui Fix loading image for backups label Call backups API only once while starting network Show backup info in proper places only Show latest backup and allow queuing new one Use proper name for waiting restoring flag Merge branch 'master' into kris/add_restore_ui Add support for stopping metworks in restoring state Merge branch 'master' into kris/add_restore_ui # Conflicts: # src/_store/system.module.js # src/account/NetworkPage.vue Use dinamyc labels-messages Report restore is started on the page Redirect to restore page in restoring mode Fix indent Split page for restore from login page Merge branch 'master' into kris/add_restore_ui Merge branch 'master' into kris/add_restore_ui Merge branch 'master' into kris/add_restore_ui Merge branch 'master' into kris/add_restore_ui Merge branch 'master' into kris/add_restore_ui # Conflicts: # src/_store/system.module.js # src/account/NetworksPage.vue # src/auth/LoginPage.vue Merge branch 'master' into kris/add_restore_ui Merge branch 'master' into kris/add_restore_ui Merge branch 'master' into kris/add_restore_ui # Conflicts: # src/auth/LoginPage.vue Merge branch 'master' into kris/add_restore_ui # Conflicts: # src/_services/user.service.js Reference configs properly through this object Add support for restoring bubble on running instance Show restore short key to user Enhance Danger Zone on UI Add restore network UI elements Redirect to single bubble page on non-sage networks Remove setup help message on the node itself Add bubble info page on end bubble server's WebUI Co-authored-by: Kristijan Mitrovic <kmitrovic@itekako.com> Reviewed-on: https://git.bubblev.org/bubblev/bubble-web/pulls/8pull/13/head
@@ -15,6 +15,7 @@ import LogoutPage from '../auth/LogoutPage' | |||
import ForgotPasswordPage from '../auth/ForgotPasswordPage' | |||
import MultifactorAuthPage from '../auth/MultifactorAuthPage' | |||
import AppLoginPage from '../auth/AppLoginPage' | |||
import RestorePage from "../auth/RestorePage" | |||
import DashboardPage from '../account/DashboardPage' | |||
import ProfilePage from '../account/profile/ProfilePage' | |||
import ActionPage from '../account/profile/ActionPage' | |||
@@ -111,6 +112,7 @@ export const router = new Router({ | |||
{ path: '/logout', component: LogoutPage }, | |||
{ path: '/forgotPassword', component: ForgotPasswordPage }, | |||
{ path: '/appLogin', component: AppLoginPage }, | |||
{ path: '/restore', component: RestorePage }, | |||
{ path: '/admin/accounts', component: AccountsPage }, | |||
{ path: '/admin/new_account', component: ProfilePage }, | |||
@@ -128,7 +130,7 @@ export const router = new Router({ | |||
}); | |||
const publicPages = [ | |||
'/login', '/logout', '/register', '/appLogin', | |||
'/login', '/logout', '/register', '/appLogin', '/restore', | |||
'/forgotPassword', '/resetPassword', | |||
'/action', '/auth', | |||
'/activate', '/legal' | |||
@@ -10,13 +10,16 @@ export const networkService = { | |||
getNetworkById, | |||
getNearestRegions, | |||
startNetwork, | |||
queueBackup, | |||
forkNetwork, | |||
getStatusesByNetworkId, | |||
getNodesByNetworkId, | |||
stopNetwork, | |||
restoreNetwork, | |||
deleteNetwork, | |||
requestNetworkKeys, | |||
retrieveNetworkKeys | |||
retrieveNetworkKeys, | |||
getNetworkBackups | |||
}; | |||
function getAllNetworks(userId, messages, errors) { | |||
@@ -58,6 +61,16 @@ function stopNetwork(userId, networkId, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/networks/${networkId}/actions/stop`, util.postWithAuth()).then(util.handleCrudResponse(messages, errors)); | |||
} | |||
function queueBackup(userId, networkId, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/networks/${networkId}/backups/user_requested`, util.putWithAuth()) | |||
.then(util.handleCrudResponse(messages, errors)); | |||
} | |||
function restoreNetwork(userId, networkId, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/networks/${networkId}/actions/restore`, | |||
util.postWithAuth()).then(util.handleCrudResponse(messages, errors)); | |||
} | |||
// deleting network is not allowed via API, instead we delete the AccountPlan, which in turn deletes the network | |||
function deleteNetwork(userId, networkId, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/plans/${networkId}`, util.deleteWithAuth()).then(util.handleCrudResponse(messages, errors)); | |||
@@ -69,4 +82,9 @@ function requestNetworkKeys(userId, networkId, messages, errors) { | |||
function retrieveNetworkKeys(userId, networkId, code, password, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/networks/${networkId}/actions/keys/${code}`, util.postWithAuth({name: 'password', value: password})).then(util.handleCrudResponse(messages, errors)); | |||
} | |||
} | |||
function getNetworkBackups(userId, networkId, messages, errors) { | |||
return fetch(`${config.apiUrl}/users/${userId}/networks/${networkId}/backups`, util.getWithAuth()) | |||
.then(util.handleCrudResponse(messages, errors)); | |||
} |
@@ -9,6 +9,7 @@ export const userService = { | |||
login, | |||
appLogin, | |||
logout, | |||
restore, | |||
forgotPassword, | |||
register, | |||
searchAccounts, | |||
@@ -66,6 +67,16 @@ function appLogin(session, messages, errors) { | |||
.then(setSessionUser); | |||
} | |||
function restore(shortKey, longKey, password, messages, errors) { | |||
const requestOptions = { | |||
method: 'PUT', | |||
headers: { 'Content-Type': 'application/json' }, | |||
body: JSON.stringify({ 'data': longKey, 'password': password }) | |||
}; | |||
return fetch(`${config.apiUrl}/auth/restore/${shortKey}`, requestOptions) | |||
.then(handleAuthResponse(messages, errors)); | |||
} | |||
function logout(messages, errors) { | |||
if (util.currentUser() === null) { | |||
console.log('userService.logout: already logged out'); | |||
@@ -10,6 +10,7 @@ const user = util.currentUser(); | |||
const defaultStatus = { | |||
loggingIn: false, | |||
loggedIn: false, | |||
restoring: false, | |||
registering: false, | |||
updating: false, | |||
settingLocale: false, | |||
@@ -111,6 +112,17 @@ const actions = { | |||
error => commit('logoutFailure', error) | |||
); | |||
}, | |||
restore({ dispatch, commit }, { shortKey, longKey, password, systemConfigs, messages, errors }) { | |||
commit('restoreRequest'); | |||
userService.restore(shortKey, longKey, password, messages, errors) | |||
.then( | |||
ok => { | |||
commit('restoreSuccess'); | |||
systemConfigs.isWaitingRestoring = false; | |||
}, | |||
error => commit('restoreFailure', error) | |||
); | |||
}, | |||
forgotPassword({ commit }, {username, messages, errors}) { | |||
commit('forgotPasswordRequest'); | |||
userService.forgotPassword(username, messages, errors) | |||
@@ -293,6 +305,17 @@ const mutations = { | |||
console.log('logout failed: '+JSON.stringify(error)); | |||
}, | |||
restoreRequest(state) { | |||
state.status = Object.assign({}, state.status, {restoring: true}); | |||
}, | |||
restoreSuccess(state) { | |||
state.status = Object.assign({}, state.status, {restoring: false}); | |||
}, | |||
restoreFailure(state, error) { | |||
state.status = Object.assign({}, state.status, {restoring: false}); | |||
console.log('restore failed: ' + JSON.stringify(error)); | |||
}, | |||
forgotPasswordRequest(state) { | |||
state.status = Object.assign({}, {sendingResetPasswordMessage: true}); | |||
state.resetPasswordMessageSent = false; | |||
@@ -8,9 +8,9 @@ import { util } from '../_helpers'; | |||
const state = { | |||
loading: { | |||
networks: false, network: false, deleting: false, | |||
networks: false, network: false, stopping: false, restoring: false, deleting: false, | |||
nearestRegions: false, startingNetwork: false, networkStatuses: false, networkNodes: false, | |||
requestNetworkKeys: false, retrieveNetworkKeys: false | |||
requestNetworkKeys: false, retrieveNetworkKeys: false, queueBackup: false | |||
}, | |||
creating: null, | |||
error: null, | |||
@@ -22,7 +22,9 @@ const state = { | |||
networkNodes: null, | |||
deletedNetworkUuid: null, | |||
networkKeysRequested: null, | |||
networkKeys: null | |||
networkKeys: null, | |||
restoreKey: null, | |||
backups: null, | |||
}; | |||
const actions = { | |||
@@ -35,13 +37,18 @@ const actions = { | |||
); | |||
}, | |||
getBackups({ commit }, { userId, networkId, messages, errors }) { | |||
commit('getNetworkBackupsRequest'); | |||
networkService.getNetworkBackups(userId, networkId, messages, errors) | |||
.then(backups => commit('getNetworkBackupsSuccess', backups), | |||
error => commit('getNetworkBackupsFailure', error)); | |||
}, | |||
getNetworkById({ commit }, {userId, networkId, messages, errors}) { | |||
commit('getNetworkByIdRequest'); | |||
networkService.getNetworkById(userId, networkId, messages, errors) | |||
.then( | |||
network => commit('getNetworkByIdSuccess', network), | |||
error => commit('getNetworkByIdFailure', error) | |||
); | |||
.then(network => commit('getNetworkByIdSuccess', network), | |||
error => commit('getNetworkByIdFailure', error)); | |||
}, | |||
addPlanAndStartNetwork({ commit }, {userId, accountPlan, cloud, region, messages, errors}) { | |||
@@ -95,6 +102,25 @@ const actions = { | |||
); | |||
}, | |||
queueBackup({ commit, dispatch }, { userId, networkId, messages, errors }) { | |||
commit('queueBackupRequest', networkId); | |||
networkService.queueBackup(userId, networkId, messages, errors) | |||
.then(backup => commit('queueBackupSuccess', backup), | |||
error => commit('queueBackupFailure', { networkId, error: error.toString() })) | |||
.then(r => dispatch('getBackups', | |||
{ userId: userId, networkId: networkId, | |||
messages: messages, errors: errors })); | |||
}, | |||
restoreNetwork({ commit }, { userId, networkId, messages, errors }) { | |||
commit('restoreNetworkRequest', networkId); | |||
networkService.restoreNetwork(userId, networkId, messages, errors) | |||
.then( | |||
network => commit('restoreNetworkSuccess', network), | |||
error => commit('restoreNetworkFailure', { networkId, error: error.toString() }) | |||
); | |||
}, | |||
deleteNetwork({ commit }, {userId, networkId, messages, errors}) { | |||
commit('deleteNetworkRequest', networkId); | |||
networkService.deleteNetwork(userId, networkId, messages, errors) | |||
@@ -215,6 +241,31 @@ const mutations = { | |||
state.error = error; | |||
}, | |||
queueBackupRequest(state, id) { | |||
state.loading.queueBackup = true; | |||
}, | |||
queueBackupSuccess(state, id) { | |||
// noop - state.loading.queueBackup will be set to false only after backup info is loaded to prevent allowing | |||
// another backup in queue before this one i really processed. | |||
}, | |||
queueBackupFailure(state, { id, error }) { | |||
state.loading.queueBackup = false; | |||
state.error = error; | |||
}, | |||
restoreNetworkRequest(state, networkId) { | |||
state.loading.restoring = true; | |||
state.restoreKey = null; | |||
}, | |||
restoreNetworkSuccess(state, restoreNodeNotification) { | |||
state.restoreKey = restoreNodeNotification.restoreKey; | |||
state.loading.restoring = false; | |||
}, | |||
restoreNetworkFailure(state, { restoreNodeNotification, error }) { | |||
state.loading.restoring = false; | |||
state.error = error; | |||
}, | |||
deleteNetworkRequest(state, id) { | |||
state.loading.deleting = true; | |||
}, | |||
@@ -271,6 +322,18 @@ const mutations = { | |||
retrieveNetworkKeysFailure(state, error) { | |||
state.loading.retrieveNetworkKeys = false; | |||
state.error = { error }; | |||
}, | |||
getNetworkBackupsRequest(state, backups) { | |||
state.backups = null; | |||
}, | |||
getNetworkBackupsSuccess(state, backups) { | |||
state.backups = backups; | |||
state.loading.queueBackup = false; | |||
}, | |||
getNetworkBackupsFailure(state, error) { | |||
state.backups = null; | |||
state.loading.queueBackup = false; | |||
state.error = { error }; | |||
} | |||
}; | |||
@@ -21,6 +21,7 @@ const state = { | |||
launchLock: null, | |||
promoCodePolicy: null, | |||
requireSendMetrics: null, | |||
isWaitingRestoring: false, | |||
support: {}, | |||
securityLevels: null | |||
}, | |||
@@ -177,6 +178,14 @@ const getters = { | |||
index: 1 | |||
}); | |||
} else { | |||
if (isAdmin) { | |||
dashApps.push({ | |||
href: '/bubble/' + configs.networkUuid, | |||
title: messages.label_menu_network, | |||
icon: messages.label_menu_networks_icon, | |||
index: 1 | |||
}); | |||
} | |||
dashApps.push({ | |||
href: '/devices', | |||
title: messages.label_menu_devices, | |||
@@ -1,11 +1,17 @@ | |||
<!-- Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ --> | |||
<template> | |||
<div v-if="network"> | |||
<h4 v-if="network.state === 'running' && configs && configs.networkUuid && network.uuid !== configs.networkUuid"> | |||
{{network.nickname}} - <i>{{messages['msg_network_state_'+network.state]}}</i><br/> | |||
<h6><button :title="messages.message_network_connect" :onclick="'window.open(\''+networkAppLoginUrl+'\', \'_blank\')'">{{messages.message_network_connect}}</button></h6> | |||
</h4> | |||
<h4 v-else>{{network.nickname}} - <i>{{messages['msg_network_state_'+network.state]}}</i></h4> | |||
<h4>{{ network.nickname }} - <i>{{ messages['msg_network_state_'+network.state] }}</i></h4> | |||
<h6 v-if="this.isNotSelfNet && (isInReadyToRestoreState || this.network.state === 'running')"> | |||
<button v-if="this.network.state === 'running'" :title="messages.message_network_connect" | |||
:onclick="'window.open(\'' + networkAppLoginUrl + '\', \'_blank\')'"> | |||
{{messages.message_network_connect}} | |||
</button> | |||
<button v-else :title="messages.message_network_restore" | |||
:onclick="'window.open(\'' + nodeRestoreUrl + '\', \'_blank\')'"> | |||
{{ messages.message_network_restore }} | |||
</button> | |||
</h6> | |||
<div v-if="stats && (network.state === 'starting' || network.state === 'restoring')"> | |||
<!-- adapted from: https://code-boxx.com/simple-vanilla-javascript-progress-bar/ --> | |||
@@ -40,7 +46,7 @@ | |||
<span v-html="messages.message_launch_support.parseMessage(this)"></span> | |||
</div> | |||
<div v-if="network.state === 'running' && configs.networkUuid && network.uuid === configs.networkUuid"> | |||
<div v-if="isSelfNetAndRunning"> | |||
<button class="btn btn-secondary" @click="requestRestoreKey()" | |||
:disabled="loading && loading.requestNetworkKeys"> | |||
{{messages.link_network_action_request_keys}} | |||
@@ -75,25 +81,62 @@ | |||
readonly="true" /> | |||
{{ messages.message_network_keys_description }} | |||
</div> | |||
</div> | |||
<hr/> | |||
<span v-html="latestBackupInfoHtml"></span> | |||
<span v-if="allowQueueBackup"> | |||
<br/> | |||
<button @click="queueBckup()" class="btn btn-secondary" :disabled="loading && loading.queueBackup"> | |||
{{ messages.link_backup_network }} | |||
</button> | |||
</span> | |||
</div> | |||
<div v-if="network.state === 'stopped'"> | |||
<!-- todo: add button to restart network in restore mode --> | |||
<hr/> | |||
<div v-if="errors.has('networkRestore')" class="invalid-feedback d-block"> | |||
{{ errors.first('networkRestore') }} | |||
</div> | |||
<div v-if="networkNodes && networkNodes.length === 0"> | |||
<div v-if="restoreKey" class="alert alert-success"> | |||
{{ messages.restore_key_label }} {{ restoreKey }} | |||
</div> | |||
<div v-else> | |||
<button @click="restoreNet()" class="btn btn-primary" :disabled="loading && loading.restoring"> | |||
{{ messages.button_label_restore }} | |||
</button> | |||
<img v-show="loading && loading.restoring" :src="loadingImgSrc" /> | |||
</div> | |||
{{ messages.button_description_restore }} | |||
</div> | |||
<div v-else v-html="messages.restore_not_possible_nodes_exist_html" /> | |||
</div> | |||
<div v-if="network.state === 'running' || network.state === 'starting' || network.state === 'stopped' || network.state === 'error_stopping'"> | |||
<div v-if="configs.sageLauncher && (network.state === 'running' || network.state === 'starting' || network.state === 'restoring' || network.state === 'stopped' || network.state === 'error_stopping')"> | |||
<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> | |||
<div v-if="network.state === 'running' || network.state === 'starting'" style="border: 2px solid #000;"> | |||
<button @click="stopNet()" class="btn btn-danger">{{messages.link_network_action_stop}}</button> | |||
<div v-if="network.state === 'running' || network.state === 'starting' || network.state === 'restoring'" | |||
style="border: 2px solid #000;"> | |||
<button @click="stopNet()" class="btn btn-danger" :disabled="loading && loading.stopping"> | |||
{{messages.link_network_action_stop}} | |||
</button> | |||
<img v-show="loading && loading.stopping" :src="loadingImgSrc" /> | |||
{{messages.link_network_action_stop_description}} | |||
<!-- the next condition is to prevent this info shown twice on this page --> | |||
<span v-if="!isSelfNetAndRunning" v-html="latestBackupInfoHtml"></span> | |||
</div> | |||
<hr/> | |||
<div style="border: 2px solid #000;"> | |||
<button @click="deleteNet()" class="btn btn-danger">{{messages.link_network_action_delete}}</button> | |||
<div v-else-if="network.state === 'stopped' || network.state === 'error_stopping'" | |||
style="border: 2px solid #000;"> | |||
<button @click="deleteNet()" class="btn btn-danger" :disabled="loading && loading.deleting"> | |||
{{messages.link_network_action_delete}} | |||
</button> | |||
<img v-show="loading && loading.deleting" :src="loadingImgSrc" /> | |||
{{messages.link_network_action_delete_description}} | |||
</div> | |||
</div> | |||
@@ -123,11 +166,13 @@ | |||
computed: { | |||
...mapState('networks', [ | |||
'network', 'newNodeNotification', 'networkStatuses', 'networkNodes', 'networkKeysRequested', | |||
'deletedNetworkUuid', 'networkKeys', 'loading' | |||
'deletedNetworkUuid', 'networkKeys', 'loading', 'restoreKey', 'backups' | |||
]), | |||
...mapState('system', ['messages', 'configs', 'appLinks']), | |||
showSetupHelp () { | |||
return (this.network !== null && (this.network.state === 'running' || this.network.state === 'starting' || this.network.state === 'restoring')); | |||
return (this.network !== null && this.network.uuid !== this.configs.networkUuid | |||
&& (this.network.state === 'running' || this.network.state === 'starting' | |||
|| this.network.state === 'restoring')); | |||
}, | |||
addableDeviceTypes: function () { | |||
if (this.messages && this.messages['!addable_device_types']) { | |||
@@ -140,12 +185,48 @@ | |||
}, | |||
networkAppLoginUrl: function () { | |||
return 'https://'+this.network.name+'.'+this.network.domainName+'/appLogin?session='+util.currentUser().token+'&uri=/devices'; | |||
}, | |||
nodeRestoreUrl: function () { | |||
return 'https://' + this.networkNodes[0].fqdn + ':' + this.networkNodes[0].sslPort + '/restore'; | |||
}, | |||
allowQueueBackup: function () { | |||
if (this.backups === null) return false; | |||
if (this.backups.length === 0) return true; | |||
let lastBackupStatus = this.backups[0].status; | |||
return lastBackupStatus !== 'queued' && lastBackupStatus !== 'backup_in_progress'; | |||
}, | |||
latestBackupInfoHtml: function() { | |||
if (this.backups === null) { | |||
return '<hr/>' + this.messages.label_latest_backup + '<img src="' + loadingImgSrc + '" />'; | |||
} else if (this.backups.length === 0) { | |||
return '<hr/>' + this.messages.label_no_latest_backup; | |||
} else { | |||
let lastBackup = this.backups[0]; | |||
return '<hr/>' + this.messages.label_latest_backup | |||
+ " " + lastBackup.label + " <i>" + lastBackup.status + "</i> " | |||
+ this.messages.date_format_app_data_epoch_time.parseDateMessage(lastBackup.creationTime, | |||
this.messages); | |||
} | |||
}, | |||
isSelfNetAndRunning: function() { | |||
return this.network && this.network.state === 'running' | |||
&& this.configs && this.configs.networkUuid && this.network.uuid === this.configs.networkUuid; | |||
}, | |||
isNotSelfNet: function() { | |||
return this.configs && this.configs.networkUuid | |||
&& this.network && this.network.uuid !== this.configs.networkUuid | |||
}, | |||
isInReadyToRestoreState: function() { | |||
return this.network && this.network.state === 'restoring' && (!this.stats || this.stats.percent === 100) | |||
&& this.networkNodes && this.networkNodes.length === 1 | |||
} | |||
}, | |||
methods: { | |||
...mapActions('networks', [ | |||
'getNetworkById', 'deleteNetwork', 'getStatusesByNetworkId', 'getNodesByNetworkId', | |||
'stopNetwork', 'deleteNetwork', 'requestNetworkKeys', 'retrieveNetworkKeys' | |||
'stopNetwork', 'queueBackup', 'restoreNetwork', 'deleteNetwork', 'requestNetworkKeys', | |||
'retrieveNetworkKeys', 'getBackups' | |||
]), | |||
...mapActions('system', ['getAppLinks']), | |||
refreshStatus (userId) { | |||
@@ -163,12 +244,23 @@ | |||
messages: this.messages, | |||
errors: this.errors | |||
}); | |||
if (this.backups === null || this.refresher === null) { | |||
// note about the second part of the condition above: if refreshes is turned on, then fetch backups | |||
// from BE only once | |||
this.getBackups({ userId: userId, networkId: this.networkId, | |||
messages: this.messages, errors: this.errors }); | |||
} | |||
}, | |||
startStatusRefresher (user) { | |||
// todo: separate refresher for network -- after "stop" we should refresh the status to show it is stopped | |||
this.refresher = setInterval(() => this.refreshStatus(user.uuid), 5000); | |||
}, | |||
clearRefresherInterval (refresherId) { | |||
if (refresherId !== null) { | |||
clearInterval(refresherId); | |||
refresherId = null; | |||
} | |||
}, | |||
stopRefreshStatus (userId) { | |||
this.getNetworkById({userId: userId, networkId: this.networkId, messages: this.messages, errors: this.errors}); | |||
}, | |||
@@ -184,12 +276,29 @@ | |||
messages: this.messages, | |||
errors: this.errors | |||
}); | |||
clearInterval(this.refresher); | |||
this.refresher = null; | |||
clearRefresherInterval(this.refresher); | |||
this.stopRefresher = setInterval(() => this.stopRefreshStatus(this.user.uuid), 5000); | |||
} | |||
} | |||
}, | |||
queueBckup () { | |||
this.errors.clear(); | |||
this.queueBackup({ | |||
userId: this.user.uuid, | |||
networkId: this.networkId, | |||
messages: this.messages, | |||
errors: this.errors | |||
}); | |||
}, | |||
restoreNet () { | |||
this.errors.clear(); | |||
this.restoreNetwork({ | |||
userId: this.user.uuid, | |||
networkId: this.networkId, | |||
messages: this.messages, | |||
errors: this.errors | |||
}); | |||
}, | |||
deleteNet () { | |||
if (confirm(this.messages.confirmation_network_action_delete)) { | |||
this.errors.clear(); | |||
@@ -230,24 +339,19 @@ | |||
this.getAppLinks(user.locale); | |||
}, | |||
beforeDestroy () { | |||
if (this.refresher !== null) clearInterval(this.refresher); | |||
if (this.stopRefresher !== null) clearInterval(this.stopRefresher); | |||
this.clearRefresherInterval(this.refresher); | |||
this.clearRefresherInterval(this.stopRefresher); | |||
}, | |||
watch: { | |||
network (net) { | |||
if (net) { | |||
if (net.state !== 'starting' && net.state !== 'restoring' && this.refresher !== null) { | |||
clearInterval(this.refresher); | |||
this.refresher = null; | |||
} | |||
if (net.state === 'stopped' && this.stopRefresher !== null) { | |||
clearInterval(this.stopRefresher); | |||
this.stopRefresher = null; | |||
} | |||
if (net.uuid === 'Not Found') { | |||
this.$router.replace({path: '/bubbles'}); | |||
} | |||
if (net.uuid === 'Not Found') this.$router.replace({path: '/bubbles'}); | |||
this.networkUuid = net.uuid; | |||
if (net.state !== 'starting' && net.state !== 'restoring') { | |||
this.clearRefresherInterval(this.refresher); | |||
} | |||
if (net.state !== 'stopping') this.clearRefresherInterval(this.stopRefresher); | |||
} | |||
}, | |||
networkNodes (nodes) { | |||
@@ -255,19 +359,20 @@ | |||
}, | |||
networkStatuses (stats) { | |||
if (this.network && stats && stats.length && stats.length > 0) { | |||
let latestStats = null; | |||
for (let i=0; i<stats.length; i++) { | |||
if (stats[i].network === this.network.uuid) { | |||
this.stats = stats[i]; | |||
if (this.stats.percent === 100) { | |||
clearInterval(this.refresher); | |||
this.refresher = null; | |||
} | |||
return; | |||
if (stats[i].network === this.network.uuid | |||
&& (latestStats === null || stats[i].ctime > latestStats.ctime)) { | |||
latestStats = stats[i]; | |||
} | |||
} | |||
// status not found for our network | |||
clearInterval(this.refresher); | |||
this.refresher = null; | |||
if (latestStats !== null) { | |||
this.stats = latestStats; | |||
if (this.stats.percent === 100) this.clearRefresherInterval(this.refresher); | |||
} else { | |||
// status not found for our network | |||
this.clearRefresherInterval(this.refresher); | |||
} | |||
} | |||
}, | |||
deletedNetworkUuid (uuid) { | |||
@@ -46,10 +46,12 @@ | |||
}, | |||
computed: { | |||
...mapState('networks', ['networks']), | |||
...mapState('system', ['messages']), | |||
...mapState('system', ['messages', 'configs']), | |||
...mapState('users', ['policy']) | |||
}, | |||
created () { | |||
if (!this.configs.sageLauncher) { this.$router.replace({ path: '/bubble/' + this.configs.networkUuid }); } | |||
const user = util.currentUser(); | |||
const selectedLocale = (user !== null && typeof user.locale !== 'undefined' && user.locale !== null ? user.locale : 'detect'); | |||
this.getAllNetworks({userId: user.uuid, messages: this.messages, errors: this.errors}); | |||
@@ -88,4 +90,4 @@ | |||
} | |||
} | |||
}; | |||
</script> | |||
</script> |
@@ -1,6 +1,8 @@ | |||
<!-- Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ --> | |||
<template> | |||
<div class="jumbotron"> | |||
<div v-if="!configs"><img :src="loadingImgSrc" /></div> | |||
<div v-else class="jumbotron"> | |||
<totp-modal/> | |||
<table v-if="this.user !== null && status.loggedIn && activated && path && path !== '' && path !== '/'" class="dash-icon-panel"> | |||
@@ -49,13 +51,15 @@ | |||
<script> | |||
import {mapState, mapActions, mapGetters} from 'vuex' | |||
import { util } from '../_helpers' | |||
import { loadingImgSrc } from '../_store'; | |||
export default { | |||
name: 'app', | |||
data() { | |||
return { | |||
showLocaleSelector: false, | |||
selectedLocale: 'detect' | |||
selectedLocale: 'detect', | |||
loadingImgSrc: loadingImgSrc | |||
} | |||
}, | |||
computed: { | |||
@@ -87,7 +91,8 @@ export default { | |||
return appView; | |||
}, | |||
locales () { return this.configs.locales; }, | |||
path () { return this.$route.path; } | |||
path () { return this.$route.path; }, | |||
isInRestoringMode () { return this.configs ? this.configs.isWaitingRestoring : undefined; } | |||
}, | |||
methods: { | |||
...mapActions({ clearAlert: 'alert/clear' }), | |||
@@ -136,6 +141,9 @@ export default { | |||
locale (loc) { | |||
this.selectedLocale = loc; | |||
this.reloadMessages() | |||
}, | |||
isInRestoringMode (restoringMode) { | |||
if (restoringMode === true && this.path !== '/restore') this.$router.replace('/restore'); | |||
} | |||
}, | |||
created() { | |||
@@ -0,0 +1,97 @@ | |||
<!-- Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ --> | |||
<template> | |||
<div> | |||
<h2>{{ messages.form_title_restore }}</h2> | |||
<div v-if="!configs"><img :src="loadingImgSrc" /></div> | |||
<div v-else-if="!configs.isWaitingRestoring" class="alert alert-info"> | |||
{{ messages.message_restore_not_applicable }}<hr/> | |||
<a href="/">{{ messages.message_back_to_root }}</a> | |||
</div> | |||
<div v-else> | |||
<form @submit.prevent="handleSubmit"> | |||
<div class="form-group"> | |||
<label htmlFor="restoreShortKey">{{messages.field_label_restore_short_key}}</label> | |||
<input type="text" v-model="restoreShortKey" name="restoreShortKey" class="form-control" | |||
:class="{ 'is-invalid': submitted && !restoreShortKey }" /> | |||
<div v-show="submitted && !restoreShortKey" class="invalid-feedback"> | |||
{{ messages.err_restoreShortKey_required }} | |||
</div> | |||
<div v-if="submitted && errors.has('restoreShortKey')" class="invalid-feedback d-block"> | |||
{{ errors.first('restoreShortKey') }} | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="restoreLongNetworkKey">{{messages.field_label_restore_long_key}}</label> | |||
<textarea v-model="restoreLongNetworkKey" name="restoreLongNetworkKey" class="form-control" | |||
:class="{ 'is-invalid': submitted && !restoreLongNetworkKey }" /> | |||
<div v-show="submitted && !restoreLongNetworkKey" class="invalid-feedback"> | |||
{{ messages.err_restoreLongNetworkKey_required }} | |||
</div> | |||
<div v-if="submitted && errors.has('restoreLongNetworkKey')" class="invalid-feedback d-block"> | |||
{{ errors.first('restoreLongNetworkKey') }} | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<label htmlFor="password">{{messages.field_label_password}}</label> | |||
<input type="password" v-model="password" name="password" class="form-control" | |||
:class="{ 'is-invalid': submitted && !password }" /> | |||
<div v-show="submitted && !password" class="invalid-feedback"> | |||
{{ messages.err_password_required }} | |||
</div> | |||
<div v-if="submitted && errors.has('password')" class="invalid-feedback d-block"> | |||
{{ errors.first('password') }} | |||
</div> | |||
</div> | |||
<div class="form-group"> | |||
<button class="btn btn-primary" :disabled="status.restoring"> | |||
{{ messages.button_label_restore }} | |||
</button> | |||
<img v-show="status.restoring" :src="loadingImgSrc" /> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { mapState, mapActions } from 'vuex'; | |||
import { loadingImgSrc } from '../_store'; | |||
export default { | |||
data () { | |||
return { | |||
restoreShortKey: (this.$route.query && this.$route.query.k) ? this.$route.query.k : null, | |||
restoreLongNetworkKey: null, | |||
password: '', | |||
submitted: false, | |||
loadingImgSrc: loadingImgSrc | |||
} | |||
}, | |||
created () { | |||
this.loadSystemConfigs(); | |||
}, | |||
computed: { | |||
...mapState('account', [ 'status' ]), | |||
...mapState('system', [ 'configs', 'messages' ]) | |||
}, | |||
methods: { | |||
...mapActions('account', [ 'restore' ]), | |||
...mapActions('system', [ 'loadSystemConfigs' ]), | |||
handleSubmit (e) { | |||
this.errors.clear(); | |||
this.submitted = true; | |||
const { restoreShortKey, restoreLongNetworkKey, password } = this; | |||
this.restore({ | |||
shortKey: restoreShortKey, longKey: restoreLongNetworkKey, password: password, | |||
systemConfigs: this.configs, messages: this.messages, errors: this.errors | |||
}); | |||
} | |||
} | |||
}; | |||
</script> |