@@ -0,0 +1,38 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
coverage | |||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (http://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules | |||||
jspm_packages | |||||
typings | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
.idea | |||||
dist | |||||
*~ |
@@ -0,0 +1,25 @@ | |||||
# bubble-web | |||||
Bubble frontend VueJS web application | |||||
The initial basis for the bubble-web frontend was Jason Watmore's wonderfully well-written | |||||
[Vue/Vuex Registration and Login Example](https://jasonwatmore.com/post/2018/07/14/vue-vuex-user-registration-and-login-tutorial-example). | |||||
The [LICENSE](vue-vuex-registration-login-example-docs/LICENSE) and [README.md](vue-vuex-registration-login-example-docs/README.md) | |||||
files for this code are in the [vue-vuex-registration-login-example-docs](vue-vuex-registration-login-example-docs) directory. | |||||
To use this frontend with a Bubble API: | |||||
Build it: | |||||
npm install | |||||
webpack | |||||
In your `${HOME}/.bubble-test.env` file, add this line (fix the path to point to the correct location of your `bubble-web/dist` directory): | |||||
export ASSETS_DIR=/path/to/bubble-web/dist | |||||
In the `bubble/bubble-server` directory, run the test named `bubble.test.dev.DevServerTest`, like so: | |||||
mvn -Dtest=bubble.test.dev.DevServerTest test | |||||
You can now test the frontend. If you make changes to the frontend code, you can simply run `webpack` again and reload the page in your browser. |
@@ -0,0 +1,36 @@ | |||||
{ | |||||
"name": "vue-vuex-registration-login-example", | |||||
"version": "1.0.0", | |||||
"repository": { | |||||
"type": "git", | |||||
"url": "https://github.com/cornflourblue/vue-vuex-registration-login-example.git" | |||||
}, | |||||
"license": "MIT", | |||||
"scripts": { | |||||
"start": "webpack-dev-server --open" | |||||
}, | |||||
"dependencies": { | |||||
"safe-eval": "^0.4.1", | |||||
"vee-validate": "^2.2.8", | |||||
"vue": "^2.6.10", | |||||
"vue-router": "^3.0.6", | |||||
"vue-select": "^3.4.0", | |||||
"vue-sidebar-menu": "^4.4.3", | |||||
"vuex": "^3.1.1" | |||||
}, | |||||
"devDependencies": { | |||||
"babel-core": "^6.26.3", | |||||
"babel-loader": "^7.1.5", | |||||
"babel-preset-env": "^1.7.0", | |||||
"babel-preset-stage-3": "^6.24.1", | |||||
"babel-preset-vue": "^2.0.2", | |||||
"css-loader": "^2.1.1", | |||||
"html-webpack-plugin": "^3.2.0", | |||||
"path": "^0.12.7", | |||||
"vue-loader": "^14.2.3", | |||||
"vue-template-compiler": "^2.6.10", | |||||
"webpack": "^4.39.3", | |||||
"webpack-cli": "^3.3.8", | |||||
"webpack-dev-server": "^3.8.0" | |||||
} | |||||
} |
@@ -0,0 +1,95 @@ | |||||
export function currentUser() { | |||||
let userJson = localStorage.getItem('user'); | |||||
return userJson ? JSON.parse(userJson) : null; | |||||
} | |||||
export function userLoggedIn() { return !!currentUser(); } | |||||
export function authHeader() { | |||||
// return authorization header with jwt token | |||||
let user = currentUser(); | |||||
if (user && user.token) { | |||||
return { 'X-Bubble-Session': user.token }; | |||||
} else { | |||||
return {}; | |||||
} | |||||
} | |||||
export function getWithAuth() { | |||||
return { | |||||
method: 'GET', | |||||
headers: authHeader() | |||||
}; | |||||
} | |||||
export function postWithAuth(obj) { | |||||
return { | |||||
method: 'POST', | |||||
headers: { ...authHeader(), 'Content-Type': 'application/json' }, | |||||
body: JSON.stringify(obj) | |||||
}; | |||||
} | |||||
export function putWithAuth(obj) { | |||||
return { | |||||
method: 'PUT', | |||||
headers: { ...authHeader(), 'Content-Type': 'application/json' }, | |||||
body: JSON.stringify(obj) | |||||
}; | |||||
} | |||||
export function deleteWithAuth() { | |||||
return { | |||||
method: 'DELETE', | |||||
headers: authHeader() | |||||
}; | |||||
} | |||||
export function handleBasicResponse(response) { | |||||
return response.text().then(text => { | |||||
const data = text && JSON.parse(text); | |||||
if (!response.ok) { | |||||
const error = (data && data.message) || response.statusText; | |||||
return Promise.reject(error); | |||||
} | |||||
return data; | |||||
}); | |||||
} | |||||
export function handleCrudResponse(response) { | |||||
return response.text().then(text => { | |||||
const data = text && JSON.parse(text); | |||||
if (!response.ok) { | |||||
if (response.status === 404) { | |||||
// set error from API | |||||
// todo: show nicer error message | |||||
console.log('handleResponse: received 404, user not found: '+JSON.stringify(data)); | |||||
} else if (response.status === 422) { | |||||
// set error from API | |||||
// todo: load auth error messages, find text for error message | |||||
console.log('handleResponse: received 422, error: '+JSON.stringify(data)); | |||||
} | |||||
const error = (data && data.message) || response.statusText; | |||||
return Promise.reject(error); | |||||
} | |||||
return data; | |||||
}); | |||||
} | |||||
export function setValidationErrors(data, messages, errors) { | |||||
for (let i=0; i<data.length; i++) { | |||||
if (data[i].messageTemplate) { | |||||
const parts = data[i].messageTemplate.split(/[._]+/); | |||||
if (parts.length === 3 && parts[0] === 'err') { | |||||
const field = parts[1]; | |||||
const messageTemplate = data[i].messageTemplate.replace(/\./g, '_'); | |||||
const message = messages.hasOwnProperty(messageTemplate) ? messages[messageTemplate] : '???'+messageTemplate; | |||||
errors.add({field: field, msg: message}); | |||||
console.log('>>>>> field '+field+' added error: '+message+', errors='+JSON.stringify(errors)); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,2 @@ | |||||
export * from './router'; | |||||
export * from './api-util'; |
@@ -0,0 +1,53 @@ | |||||
import Vue from 'vue'; | |||||
import Router from 'vue-router'; | |||||
import HomePage from '../account/HomePage' | |||||
import RegisterPage from '../auth/RegisterPage' | |||||
import LoginPage from '../auth/LoginPage' | |||||
import ProfilePage from '../account/ProfilePage' | |||||
import NetworksPage from '../account/NetworksPage' | |||||
import NewNetworkPage from '../account/NewNetworkPage' | |||||
import NetworkPage from '../account/NetworkPage' | |||||
import AccountsPage from '../admin/AccountsPage' | |||||
import { currentUser } from '../_helpers' | |||||
Vue.use(Router); | |||||
export const router = new Router({ | |||||
mode: 'history', | |||||
routes: [ | |||||
{ | |||||
path: '/', component: HomePage, | |||||
children: [ | |||||
{ path: '', component: NetworksPage }, | |||||
{ path: '/profile', component: ProfilePage }, | |||||
{ path: '/networks', component: NetworksPage }, | |||||
{ path: '/networks/new', component: NewNetworkPage }, | |||||
{ path: '/networks/:uuid', component: NetworkPage } | |||||
] | |||||
}, | |||||
{ path: '/register', component: RegisterPage }, | |||||
{ path: '/login', component: LoginPage }, | |||||
{ path: '/logout', component: LoginPage }, | |||||
{ path: '/admin/accounts', component: AccountsPage }, | |||||
{ path: '/admin/accounts/:uuid', component: ProfilePage }, | |||||
// otherwise redirect to home | |||||
{ path: '*', redirect: '/' } | |||||
] | |||||
}); | |||||
router.beforeEach((to, from, next) => { | |||||
const publicPages = ['/login', '/logout', '/register']; | |||||
const authRequired = !publicPages.includes(to.path); | |||||
const user = currentUser(); | |||||
if (authRequired) { | |||||
// redirect to login page if not logged in and trying to access a restricted page | |||||
if (!user) return next('/login'); | |||||
// redirect to home page if not admin and trying to access an admin page | |||||
if (to.path.startsWith('/admin') && user.admin !== true) return next('/'); | |||||
} | |||||
next(); | |||||
}); |
@@ -0,0 +1,20 @@ | |||||
import config from 'config'; | |||||
import { getWithAuth, putWithAuth, handleCrudResponse } from '../_helpers'; | |||||
export const accountPlanService = { | |||||
getAll, | |||||
getById, | |||||
newAccountPlan | |||||
}; | |||||
function getAll(userId) { | |||||
return fetch(`${config.apiUrl}/users/${userId}/accountPlans`, getWithAuth()).then(handleCrudResponse); | |||||
} | |||||
function getById(userId, accountPlanId) { | |||||
return fetch(`${config.apiUrl}/users/${userId}/accountPlans/${accountPlanId}`, getWithAuth()).then(handleCrudResponse); | |||||
} | |||||
function newAccountPlan(userId, accountPlan) { | |||||
return fetch(`${config.apiUrl}/users/${userId}/accountPlans`, putWithAuth(accountPlan)).then(handleCrudResponse); | |||||
} |
@@ -0,0 +1,15 @@ | |||||
import config from 'config'; | |||||
import { getWithAuth, handleCrudResponse } from '../_helpers'; | |||||
export const domainService = { | |||||
getAll, | |||||
getById | |||||
}; | |||||
function getAll(userId) { | |||||
return fetch(`${config.apiUrl}/users/${userId}/domains`, getWithAuth()).then(handleCrudResponse); | |||||
} | |||||
function getById(userId, domainId) { | |||||
return fetch(`${config.apiUrl}/users/${userId}/domains/${domainId}`, getWithAuth()).then(handleCrudResponse); | |||||
} |
@@ -0,0 +1,15 @@ | |||||
import config from 'config'; | |||||
import { getWithAuth, handleCrudResponse } from '../_helpers'; | |||||
export const footprintService = { | |||||
getAll, | |||||
getById | |||||
}; | |||||
function getAll() { | |||||
return fetch(`${config.apiUrl}/me/footprints`, getWithAuth()).then(handleCrudResponse); | |||||
} | |||||
function getById(footprintId) { | |||||
return fetch(`${config.apiUrl}/me/footprints/${footprintId}`, getWithAuth()).then(handleCrudResponse); | |||||
} |
@@ -0,0 +1,7 @@ | |||||
export * from './system.service'; | |||||
export * from './user.service'; | |||||
export * from './domain.service'; | |||||
export * from './plan.service'; | |||||
export * from './footprint.service'; | |||||
export * from './accountPlan.service'; | |||||
export * from './network.service'; |
@@ -0,0 +1,15 @@ | |||||
import config from 'config'; | |||||
import { getWithAuth, handleCrudResponse } from '../_helpers'; | |||||
export const networkService = { | |||||
getAll, | |||||
getById | |||||
}; | |||||
function getAll(userId) { | |||||
return fetch(`${config.apiUrl}/users/${userId}/networks`, getWithAuth()).then(handleCrudResponse); | |||||
} | |||||
function getById(userId, networkId) { | |||||
return fetch(`${config.apiUrl}/users/${userId}/networks/${networkId}`, getWithAuth()).then(handleCrudResponse); | |||||
} |
@@ -0,0 +1,15 @@ | |||||
import config from 'config'; | |||||
import { getWithAuth, handleCrudResponse } from '../_helpers'; | |||||
export const planService = { | |||||
getAll, | |||||
getById | |||||
}; | |||||
function getAll() { | |||||
return fetch(`${config.apiUrl}/plans`, getWithAuth()).then(handleCrudResponse); | |||||
} | |||||
function getById(planId) { | |||||
return fetch(`${config.apiUrl}/plans/${planId}`, getWithAuth()).then(handleCrudResponse); | |||||
} |
@@ -0,0 +1,37 @@ | |||||
import config from 'config'; | |||||
import { getWithAuth, userLoggedIn, handleBasicResponse } from '../_helpers'; | |||||
export const systemService = { | |||||
loadSystemConfigs, | |||||
loadMessages, | |||||
loadTimezones, | |||||
detectTimezone | |||||
}; | |||||
function loadSystemConfigs() { | |||||
const requestOptions = userLoggedIn() ? getWithAuth() : { method: 'GET' }; | |||||
return fetch(`${config.apiUrl}/auth/configs`, requestOptions) | |||||
.then(handleBasicResponse) | |||||
.then(configs => { return configs; }); | |||||
} | |||||
function loadMessages(group, locale) { | |||||
const requestOptions = userLoggedIn() ? getWithAuth() : { method: 'GET' }; | |||||
if (!locale || locale === '') locale = 'detect'; | |||||
return fetch(`${config.apiUrl}/messages/${locale}/${group}`, requestOptions) | |||||
.then(handleBasicResponse) | |||||
.then(messages => { return messages; }); | |||||
} | |||||
function loadTimezones() { | |||||
const requestOptions = userLoggedIn() ? getWithAuth() : { method: 'GET' }; | |||||
return fetch(`${config.apiUrl}/timezones`, requestOptions) | |||||
.then(handleBasicResponse) | |||||
.then(timezones => { return timezones; }); | |||||
} | |||||
function detectTimezone() { | |||||
return fetch(`${config.apiUrl}/me/detect/timezone`, getWithAuth()) | |||||
.then(handleBasicResponse) | |||||
.then(timezone => { return timezone; }); | |||||
} |
@@ -0,0 +1,95 @@ | |||||
import config from 'config'; | |||||
import { getWithAuth, postWithAuth, deleteWithAuth, handleCrudResponse, setValidationErrors } from '../_helpers'; | |||||
export const userService = { | |||||
login, | |||||
logout, | |||||
register, | |||||
getAll, | |||||
getById, | |||||
update, | |||||
delete: _delete | |||||
}; | |||||
function login(name, password, messages, errors) { | |||||
const requestOptions = { | |||||
method: 'POST', | |||||
headers: { 'Content-Type': 'application/json' }, | |||||
body: JSON.stringify({ 'name': name, 'password': password }) | |||||
}; | |||||
return fetch(`${config.apiUrl}/auth/login`, requestOptions) | |||||
.then(handleAuthResponse(messages, errors)) | |||||
.then(user => { | |||||
// login successful if there's a session token in the response | |||||
if (user.token) { | |||||
// store user details and session token in local storage to keep user logged in between page refreshes | |||||
localStorage.setItem('user', JSON.stringify(user)); | |||||
} | |||||
return user; | |||||
}); | |||||
} | |||||
function logout() { | |||||
// remove user from local storage to log user out | |||||
localStorage.removeItem('user'); | |||||
} | |||||
function register(user, messages, errors) { | |||||
const requestOptions = { | |||||
method: 'POST', | |||||
headers: { 'Content-Type': 'application/json' }, | |||||
body: JSON.stringify(user) | |||||
}; | |||||
return fetch(`${config.apiUrl}/auth/register`, requestOptions) | |||||
.then(handleAuthResponse(messages, errors)) | |||||
.then(user => { | |||||
if (user.token) { | |||||
// store user details and session token in local storage to keep user logged in between page refreshes | |||||
localStorage.setItem('user', JSON.stringify(user)); | |||||
} | |||||
return user; | |||||
}); | |||||
} | |||||
function getAll() { | |||||
return fetch(`${config.apiUrl}/users`, getWithAuth()).then(handleCrudResponse); | |||||
} | |||||
function getById(id) { | |||||
return fetch(`${config.apiUrl}/users/${id}`, getWithAuth()).then(handleCrudResponse); | |||||
} | |||||
function update(user) { | |||||
return fetch(`${config.apiUrl}/users/${user.uuid}`, postWithAuth(user)).then(handleCrudResponse); | |||||
} | |||||
// prefixed function name with underscore because delete is a reserved word in javascript | |||||
function _delete(id) { | |||||
return fetch(`${config.apiUrl}/users/${id}`, deleteWithAuth()).then(handleCrudResponse); | |||||
} | |||||
function handleAuthResponse(messages, errors) { | |||||
return function (response) { | |||||
return response.text().then(text => { | |||||
const data = text && JSON.parse(text); | |||||
if (!response.ok) { | |||||
if (response.status === 401) { | |||||
// auto logout if 401 response returned from api | |||||
logout(); | |||||
location.reload(true); | |||||
} else if (response.status === 404) { | |||||
// set error from API | |||||
// todo: show nicer error message | |||||
console.log('handleAuthResponse: received 404, user not found: '+JSON.stringify(data)); | |||||
} else if (response.status === 422) { | |||||
setValidationErrors(data, messages, errors); | |||||
} | |||||
const error = (data && data.message) || response.statusText; | |||||
return Promise.reject(error); | |||||
} | |||||
return data; | |||||
}); | |||||
}; | |||||
} |
@@ -0,0 +1,104 @@ | |||||
import { userService } from '../_services'; | |||||
import { router } from '../_helpers'; | |||||
// todo: why can't we import currentUser from api-util and use that here? | |||||
// when I try to do that, webpack succeeds but then an error occurs loading any page, with the | |||||
// error message "_helpers.currentUser is not defined" | |||||
const user = JSON.parse(localStorage.getItem('user')); | |||||
const state = user | |||||
? { status: { loggedIn: true }, user } | |||||
: { status: {}, user: null }; | |||||
const actions = { | |||||
login({ dispatch, commit }, { user, messages, errors }) { | |||||
commit('loginRequest', { name: user.name }); | |||||
userService.login(user.name, user.password, messages, errors) | |||||
.then( | |||||
user => { | |||||
commit('loginSuccess', user); | |||||
router.push('/'); | |||||
}, | |||||
error => { | |||||
commit('loginFailure', error); | |||||
dispatch('alert/error', error, { root: true }); | |||||
} | |||||
); | |||||
}, | |||||
logout({ commit }) { | |||||
userService.logout(); | |||||
commit('logout'); | |||||
}, | |||||
register({ dispatch, commit }, {user, messages, errors}) { | |||||
commit('registerRequest', user); | |||||
userService.register(user, messages, errors) | |||||
.then( | |||||
user => { | |||||
commit('registerSuccess', user); | |||||
router.push('/'); | |||||
setTimeout(() => { | |||||
// display success message after route change completes | |||||
dispatch('alert/success', 'Registration successful', { root: true }); | |||||
}) | |||||
}, | |||||
error => { | |||||
commit('registerFailure'); | |||||
dispatch('alert/error', error, { root: true }); | |||||
} | |||||
); | |||||
}, | |||||
update({ dispatch, commit }, user) { | |||||
commit('updateRequest', user); | |||||
userService.update(user) | |||||
.then( | |||||
user => { | |||||
commit('updateSuccess', user); | |||||
router.push('/me'); | |||||
setTimeout(() => { | |||||
// display success message after route change completes | |||||
dispatch('alert/success', 'Profile update was successful', { root: true }); | |||||
}) | |||||
}, | |||||
error => { | |||||
commit('updateFailure', error); | |||||
dispatch('alert/error', error, { root: true }); | |||||
} | |||||
); | |||||
} | |||||
}; | |||||
const mutations = { | |||||
loginRequest(state, user) { | |||||
state.status = { loggingIn: true }; | |||||
state.user = user; | |||||
}, | |||||
loginSuccess(state, user) { | |||||
state.status = { loggedIn: true }; | |||||
state.user = user; | |||||
}, | |||||
loginFailure(state) { | |||||
state.status = {}; | |||||
state.user = null; | |||||
}, | |||||
logout(state) { | |||||
state.status = {}; | |||||
state.user = null; | |||||
}, | |||||
registerRequest(state, user) { | |||||
state.status = { registering: true }; | |||||
state.user = user; | |||||
}, | |||||
registerSuccess(state, user) { | |||||
state.status = {}; | |||||
state.user = user; | |||||
}, | |||||
registerFailure(state) { | |||||
state.status = {}; | |||||
} | |||||
}; | |||||
export const account = { | |||||
namespaced: true, | |||||
state, | |||||
actions, | |||||
mutations | |||||
}; |
@@ -0,0 +1,88 @@ | |||||
import { accountPlanService } from '../_services'; | |||||
import { account } from '../_store/account.module'; | |||||
const state = { | |||||
loading: null, | |||||
error: null, | |||||
accountPlans: null, | |||||
accountPlan: null | |||||
}; | |||||
const actions = { | |||||
getAll({ commit }) { | |||||
commit('getAllRequest'); | |||||
accountPlanService.getAll(account.user.uuid) | |||||
.then( | |||||
accountPlans => commit('getAllSuccess', accountPlans), | |||||
error => commit('getAllFailure', error) | |||||
); | |||||
}, | |||||
getByUuid({ commit }, uuid) { | |||||
commit('getByUuidRequest'); | |||||
accountPlanService.getById(account.user.uuid, uuid) | |||||
.then( | |||||
accountPlan => commit('getByUuidSuccess', accountPlan), | |||||
error => commit('getByUuidFailure', error) | |||||
); | |||||
}, | |||||
delete({ commit }, id) { | |||||
commit('deleteRequest', id); | |||||
accountPlanService.delete(account.user.uuid, id) | |||||
.then( | |||||
accountPlan => commit('deleteSuccess', accountPlan), | |||||
error => commit('deleteFailure', { id, error: error.toString() }) | |||||
); | |||||
} | |||||
}; | |||||
const mutations = { | |||||
getAllRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getAllSuccess(state, accountPlans) { | |||||
state.loading = false; | |||||
state.accountPlans = accountPlans; | |||||
}, | |||||
getAllFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
}, | |||||
getByUuidRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getByUuidSuccess(state, accountPlan) { | |||||
state.loading = false; | |||||
state.accountPlan = accountPlan; | |||||
}, | |||||
getByUuidFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
}, | |||||
deleteRequest(state, id) { | |||||
state.loading = true; | |||||
}, | |||||
deleteSuccess(state, id) { | |||||
state.loading = false; | |||||
// remove deleted accountPlan from state | |||||
if (state.accountPlans) { | |||||
state.accountPlans = state.accountPlans.filter(accountPlan => accountPlan.uuid !== id) | |||||
} | |||||
}, | |||||
deleteFailure(state, { id, error }) { | |||||
state.loading = false; | |||||
// remove 'deleting:true' property and add 'deleteError:[error]' property to accountPlan | |||||
state.error = error; | |||||
} | |||||
}; | |||||
export const accountPlans = { | |||||
namespaced: true, | |||||
state, | |||||
actions, | |||||
mutations | |||||
}; |
@@ -0,0 +1,38 @@ | |||||
const state = { | |||||
type: null, | |||||
message: null | |||||
}; | |||||
const actions = { | |||||
success({ commit }, message) { | |||||
commit('success', message); | |||||
}, | |||||
error({ commit }, message) { | |||||
commit('error', message); | |||||
}, | |||||
clear({ commit }, message) { | |||||
commit('success', message); | |||||
} | |||||
}; | |||||
const mutations = { | |||||
success(state, message) { | |||||
state.type = 'alert-success'; | |||||
state.message = message; | |||||
}, | |||||
error(state, message) { | |||||
state.type = 'alert-danger'; | |||||
state.message = message; | |||||
}, | |||||
clear(state) { | |||||
state.type = null; | |||||
state.message = null; | |||||
} | |||||
}; | |||||
export const alert = { | |||||
namespaced: true, | |||||
state, | |||||
actions, | |||||
mutations | |||||
}; |
@@ -0,0 +1,60 @@ | |||||
import { domainService } from '../_services'; | |||||
const state = { | |||||
loading: null, | |||||
error: null, | |||||
domains: null, | |||||
domain: null | |||||
}; | |||||
const actions = { | |||||
getAll({ commit }, userId) { | |||||
commit('getAllRequest'); | |||||
domainService.getAll(userId) | |||||
.then( | |||||
domains => commit('getAllSuccess', domains), | |||||
error => commit('getAllFailure', error) | |||||
); | |||||
}, | |||||
getByUuid({ commit }, userId, uuid) { | |||||
commit('getByUuidRequest'); | |||||
domainService.getById(userId, uuid) | |||||
.then( | |||||
domain => commit('getByUuidSuccess', domain), | |||||
error => commit('getByUuidFailure', error) | |||||
); | |||||
} | |||||
}; | |||||
const mutations = { | |||||
getAllRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getAllSuccess(state, domains) { | |||||
state.loading = false; | |||||
state.domains = domains; | |||||
}, | |||||
getAllFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
}, | |||||
getByUuidRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getByUuidSuccess(state, domain) { | |||||
state.loading = false; | |||||
state.domain = domain; | |||||
}, | |||||
getByUuidFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
} | |||||
}; | |||||
export const domains = { | |||||
namespaced: true, | |||||
state, | |||||
actions, | |||||
mutations | |||||
}; |
@@ -0,0 +1,60 @@ | |||||
import { footprintService } from '../_services'; | |||||
const state = { | |||||
loading: null, | |||||
error: null, | |||||
footprints: null, | |||||
footprint: null | |||||
}; | |||||
const actions = { | |||||
getAll({ commit }) { | |||||
commit('getAllRequest'); | |||||
footprintService.getAll() | |||||
.then( | |||||
footprints => commit('getAllSuccess', footprints), | |||||
error => commit('getAllFailure', error) | |||||
); | |||||
}, | |||||
getByUuid({ commit }, uuid) { | |||||
commit('getByUuidRequest'); | |||||
footprintService.getById(uuid) | |||||
.then( | |||||
footprint => commit('getByUuidSuccess', footprint), | |||||
error => commit('getByUuidFailure', error) | |||||
); | |||||
} | |||||
}; | |||||
const mutations = { | |||||
getAllRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getAllSuccess(state, footprints) { | |||||
state.loading = false; | |||||
state.footprints = footprints; | |||||
}, | |||||
getAllFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
}, | |||||
getByUuidRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getByUuidSuccess(state, footprint) { | |||||
state.loading = false; | |||||
state.footprint = footprint; | |||||
}, | |||||
getByUuidFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
} | |||||
}; | |||||
export const footprints = { | |||||
namespaced: true, | |||||
state, | |||||
actions, | |||||
mutations | |||||
}; |
@@ -0,0 +1,50 @@ | |||||
import Vue from 'vue'; | |||||
import Vuex from 'vuex'; | |||||
import { alert } from './alert.module'; | |||||
import { system } from './system.module'; | |||||
import { account } from './account.module'; | |||||
import { users } from './users.module'; | |||||
import { plans } from './plans.module'; | |||||
import { footprints } from './footprints.module'; | |||||
import { domains } from './domains.module'; | |||||
import { accountPlans } from './accountPlans.module'; | |||||
import { networks } from './networks.module'; | |||||
Vue.use(Vuex); | |||||
export const store = new Vuex.Store({ | |||||
modules: { | |||||
alert, | |||||
system, | |||||
account, | |||||
users, | |||||
domains, | |||||
plans, | |||||
footprints, | |||||
accountPlans, | |||||
networks | |||||
} | |||||
}); | |||||
const safeEval = require('safe-eval'); | |||||
function evalInContext(vue, string) { | |||||
try { | |||||
return safeEval('vue.'+string, {vue}); | |||||
} catch (error) { | |||||
try { | |||||
return safeEval(string); | |||||
} catch (errorWithoutThis) { | |||||
console.warn('evalInContext: Error evaluating "' + string + '"', errorWithoutThis); | |||||
return ''; | |||||
} | |||||
} | |||||
} | |||||
String.prototype.parseMessage = function (vue) { | |||||
return this ? ''+this.replace(/{{.*?}}/g, match => { | |||||
const expression = match.slice(2, -2); | |||||
return evalInContext(vue, expression) | |||||
}) : ''; | |||||
}; |
@@ -0,0 +1,90 @@ | |||||
import { networkService } from '../_services'; | |||||
import { account } from '../_store/account.module'; | |||||
const state = { | |||||
loading: null, | |||||
creating: null, | |||||
error: null, | |||||
networks: null, | |||||
network: null | |||||
}; | |||||
const actions = { | |||||
getAll({ commit }) { | |||||
commit('getAllRequest'); | |||||
networkService.getAll(account.state.user.uuid) | |||||
.then( | |||||
networks => commit('getAllSuccess', networks), | |||||
error => commit('getAllFailure', error) | |||||
); | |||||
}, | |||||
getByUuid({ commit }, uuid) { | |||||
commit('getByUuidRequest'); | |||||
networkService.getById(account.state.user.uuid, uuid) | |||||
.then( | |||||
network => commit('getByUuidSuccess', network), | |||||
error => commit('getByUuidFailure', error) | |||||
); | |||||
}, | |||||
create({ commit }, accountPlan) { | |||||
// todo | |||||
}, | |||||
delete({ commit }, id) { | |||||
commit('deleteRequest', id); | |||||
networkService.delete(account.state.user.uuid, id) | |||||
.then( | |||||
network => commit('deleteSuccess', network), | |||||
error => commit('deleteFailure', { id, error: error.toString() }) | |||||
); | |||||
} | |||||
}; | |||||
const mutations = { | |||||
getAllRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getAllSuccess(state, networks) { | |||||
state.loading = false; | |||||
state.networks = networks; | |||||
}, | |||||
getAllFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
}, | |||||
getByUuidRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getByUuidSuccess(state, network) { | |||||
state.loading = false; | |||||
state.network = network; | |||||
}, | |||||
getByUuidFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
}, | |||||
deleteRequest(state, id) { | |||||
state.loading = true; | |||||
}, | |||||
deleteSuccess(state, id) { | |||||
state.loading = false; | |||||
// remove deleted network from state | |||||
if (state.networks) { | |||||
state.networks = state.networks.filter(network => network.uuid !== id) | |||||
} | |||||
}, | |||||
deleteFailure(state, { id, error }) { | |||||
state.loading = false; | |||||
// remove 'deleting:true' property and add 'deleteError:[error]' property to network | |||||
state.error = error; | |||||
} | |||||
}; | |||||
export const networks = { | |||||
namespaced: true, | |||||
state, | |||||
actions, | |||||
mutations | |||||
}; |
@@ -0,0 +1,84 @@ | |||||
import { planService } from '../_services'; | |||||
const state = { | |||||
loading: null, | |||||
error: null, | |||||
plans: null, | |||||
plan: null | |||||
}; | |||||
const actions = { | |||||
getAll({ commit }) { | |||||
commit('getAllRequest'); | |||||
planService.getAll() | |||||
.then( | |||||
plans => commit('getAllSuccess', plans), | |||||
error => commit('getAllFailure', error) | |||||
); | |||||
}, | |||||
getByUuid({ commit }, uuid) { | |||||
commit('getByUuidRequest'); | |||||
planService.getById(uuid) | |||||
.then( | |||||
plan => commit('getByUuidSuccess', plan), | |||||
error => commit('getByUuidFailure', error) | |||||
); | |||||
}, | |||||
delete({ commit }, id) { | |||||
commit('deleteRequest', id); | |||||
planService.delete(id) | |||||
.then( | |||||
plan => commit('deleteSuccess', plan), | |||||
error => commit('deleteFailure', { id, error: error.toString() }) | |||||
); | |||||
} | |||||
}; | |||||
const mutations = { | |||||
getAllRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getAllSuccess(state, plans) { | |||||
state.loading = false; | |||||
state.plans = plans; | |||||
}, | |||||
getAllFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
}, | |||||
getByUuidRequest(state) { | |||||
state.loading = true; | |||||
}, | |||||
getByUuidSuccess(state, plan) { | |||||
state.loading = false; | |||||
state.plan = plan; | |||||
}, | |||||
getByUuidFailure(state, error) { | |||||
state.loading = false; | |||||
state.error = { error }; | |||||
}, | |||||
deleteRequest(state, id) { | |||||
state.loading = true; | |||||
}, | |||||
deleteSuccess(state, id) { | |||||
state.loading = false; | |||||
// remove deleted plan from state | |||||
if (state.plans) { | |||||
state.plans = state.plans.filter(plan => plan.uuid !== id) | |||||
} | |||||
}, | |||||
deleteFailure(state, { id, error }) { | |||||
state.loading = false; | |||||
// remove 'deleting:true' property and add 'deleteError:[error]' property to user | |||||
state.error = error; | |||||
} | |||||
}; | |||||
export const plans = { | |||||
namespaced: true, | |||||
state, | |||||
actions, | |||||
mutations | |||||
}; |
@@ -0,0 +1,128 @@ | |||||
import { systemService } from '../_services'; | |||||
const state = { | |||||
configs: { | |||||
allowRegistration: null | |||||
}, | |||||
messages: {}, | |||||
countries: [], | |||||
locales: [], | |||||
timezones: [], | |||||
detectedTimezone: null, | |||||
menu: [], | |||||
error: null | |||||
}; | |||||
const actions = { | |||||
loadSystemConfigs({ commit }) { | |||||
commit('loadSystemConfigsRequest'); | |||||
systemService.loadSystemConfigs() | |||||
.then( | |||||
configs => commit('loadSystemConfigsSuccess', configs), | |||||
error => commit('loadSystemConfigsFailure', error) | |||||
); | |||||
}, | |||||
loadMessages({ commit }, group, locale) { | |||||
commit('loadMessagesRequest'); | |||||
systemService.loadMessages(group, locale) | |||||
.then( | |||||
messages => commit('loadMessagesSuccess', {group, messages}), | |||||
error => commit('loadMessagesFailure', error) | |||||
); | |||||
}, | |||||
loadTimezones({ commit }) { | |||||
commit('loadTimezonesRequest'); | |||||
systemService.loadTimezones().then( | |||||
timezones => commit('loadTimezonesSuccess', timezones), | |||||
error => commit('loadTimezonesFailure', error) | |||||
) | |||||
}, | |||||
detectTimezone({ commit }) { | |||||
commit('detectTimezoneRequest'); | |||||
systemService.detectTimezone().then( | |||||
timezones => commit('detectTimezoneSuccess', timezones), | |||||
error => commit('detectTimezoneFailure', error) | |||||
) | |||||
} | |||||
}; | |||||
const mutations = { | |||||
loadSystemConfigsRequest(state) {}, | |||||
loadSystemConfigsSuccess(state, configs) { | |||||
state.configs = configs; | |||||
}, | |||||
loadSystemConfigsFailure(state, error) { | |||||
state.error = error; | |||||
}, | |||||
loadMessagesRequest(state) {}, | |||||
loadMessagesSuccess(state, {group, messages}) { | |||||
// console.log('loadMessages (group='+group+'), messages='+JSON.stringify(messages)); | |||||
state.messages = Object.assign({}, state.messages, messages); | |||||
state.menu = [{ | |||||
href: '/', | |||||
title: messages.label_menu_dashboard, | |||||
icon: messages.label_menu_dashboard_icon | |||||
}, { | |||||
href: '/profile', | |||||
title: messages.label_menu_account, | |||||
icon: messages.label_menu_account_icon, | |||||
child: [{ | |||||
href: '/profile/policy', | |||||
title: messages.label_menu_account_policy, | |||||
icon: messages.label_menu_account_policy_icon | |||||
}, { | |||||
href: '/profile/contacts', | |||||
title: messages.label_menu_account_contacts, | |||||
icon: messages.label_menu_account_contacts_icon | |||||
}] | |||||
}, { | |||||
href: '/me/networks', | |||||
title: messages.label_menu_networks, | |||||
icon: messages.label_menu_networks_icon | |||||
}, { | |||||
href: '/logout', | |||||
title: messages.label_menu_logout, | |||||
icon: messages.label_menu_logout_icon | |||||
}]; | |||||
if (messages.country_codes) { | |||||
const countries = []; | |||||
const codes = messages.country_codes.split(','); | |||||
for (let i=0; i<codes.length; i++) { | |||||
countries.push({code: codes[i], countryName: messages['country_'+codes[i]]}); | |||||
} | |||||
state.countries = countries; | |||||
} | |||||
if (messages.locale_codes) { | |||||
const locales = []; | |||||
const codes = messages.locale_codes.split(','); | |||||
for (let i=0; i<codes.length; i++) { | |||||
locales.push({localeCode: codes[i], localeName: messages['locale_'+codes[i]]}); | |||||
} | |||||
state.locales = locales; | |||||
} | |||||
}, | |||||
loadMessagesFailure(state, error) { | |||||
state.error = error; | |||||
}, | |||||
loadTimezonesRequest(state) {}, | |||||
loadTimezonesSuccess(state, timezones) { | |||||
state.timezones = timezones; | |||||
}, | |||||
loadTimezonesFailure(state, error) { | |||||
state.error = error; | |||||
}, | |||||
detectTimezoneRequest(state) {}, | |||||
detectTimezoneSuccess(state, detectedTimezone) { | |||||
state.detectedTimezone = detectedTimezone; | |||||
}, | |||||
detectTimezoneFailure(state, error) { | |||||
state.error = error; | |||||
} | |||||
}; | |||||
export const system = { | |||||
namespaced: true, | |||||
state, | |||||
actions, | |||||
mutations | |||||
}; |
@@ -0,0 +1,114 @@ | |||||
import { userService } from '../_services'; | |||||
import { account } from '../_store/account.module'; | |||||
const state = { | |||||
all: {}, | |||||
user: null | |||||
}; | |||||
const actions = { | |||||
getAll({ commit }) { | |||||
commit('getAllRequest'); | |||||
userService.getAll() | |||||
.then( | |||||
users => commit('getAllSuccess', users), | |||||
error => commit('getAllFailure', error) | |||||
); | |||||
}, | |||||
getByUuid({ commit }, uuid) { | |||||
commit('getByUuidRequest'); | |||||
userService.getById(uuid) | |||||
.then( | |||||
users => commit('getByUuidSuccess', users), | |||||
error => commit('getByUuidFailure', error) | |||||
); | |||||
}, | |||||
update({ commit }, user) { | |||||
commit('updateRequest', user); | |||||
userService.update(user) | |||||
.then( | |||||
user => commit('updateSuccess', user), | |||||
error => commit('updateFailure', { user, error: error.toString() }) | |||||
); | |||||
}, | |||||
delete({ commit }, id) { | |||||
commit('deleteRequest', id); | |||||
userService.delete(id) | |||||
.then( | |||||
id => commit('deleteSuccess', id), | |||||
error => commit('deleteFailure', { id, error: error.toString() }) | |||||
); | |||||
} | |||||
}; | |||||
const mutations = { | |||||
getAllRequest(state) { | |||||
state.all = { loading: true }; | |||||
}, | |||||
getAllSuccess(state, users) { | |||||
state.all = { items: users }; | |||||
}, | |||||
getAllFailure(state, error) { | |||||
state.all = { error }; | |||||
}, | |||||
getByUuidRequest(state) { | |||||
state.user = { loading: true }; | |||||
}, | |||||
getByUuidSuccess(state, user) { | |||||
state.user = user; | |||||
}, | |||||
getByUuidFailure(state, error) { | |||||
state.user = { error }; | |||||
}, | |||||
updateRequest(state, user) { | |||||
// todo: add 'updating:true' property to user being updated | |||||
}, | |||||
updateSuccess(state, user) { | |||||
// todo: why doesn't this work? | |||||
user.token = account.user.token; // preserve token | |||||
state.user = account.user = user; | |||||
localStorage.setItem('user', JSON.stringify(user)); | |||||
}, | |||||
updateFailure(state, { id, error }) { | |||||
// todo: remove 'updating:true' property and add 'updateError:[error]' property to user | |||||
}, | |||||
deleteRequest(state, id) { | |||||
// add 'deleting:true' property to user being deleted | |||||
state.all.items = state.all.items.map(user => | |||||
user.uuid === id | |||||
? { ...user, deleting: true } | |||||
: user | |||||
) | |||||
}, | |||||
deleteSuccess(state, id) { | |||||
// remove deleted user from state | |||||
state.all.items = state.all.items.filter(user => user.uuid !== id) | |||||
}, | |||||
deleteFailure(state, { id, error }) { | |||||
// remove 'deleting:true' property and add 'deleteError:[error]' property to user | |||||
state.all.items = state.items.map(user => { | |||||
if (user.uuid === id) { | |||||
// make copy of user without 'deleting:true' property | |||||
const { deleting, ...userCopy } = user; | |||||
// return copy of user with 'deleteError:[error]' property | |||||
return { ...userCopy, deleteError: error }; | |||||
} | |||||
return user; | |||||
}) | |||||
} | |||||
}; | |||||
export const users = { | |||||
namespaced: true, | |||||
state, | |||||
actions, | |||||
mutations | |||||
}; |
@@ -0,0 +1,33 @@ | |||||
<template> | |||||
<div> | |||||
<h1>{{messages.label_homepage_hello && messages.label_homepage_hello.parseMessage(this)}}!</h1> | |||||
<hr/> | |||||
<router-view></router-view> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { mapState, mapActions } from 'vuex' | |||||
export default { | |||||
computed: { | |||||
...mapState({ | |||||
account: state => state.account, | |||||
users: state => state.users.all | |||||
}), | |||||
...mapState('system', ['messages']) | |||||
}, | |||||
created () { | |||||
// todo: allow user to choose locale | |||||
const locale = 'detect'; | |||||
this.loadMessages('post_auth', locale); | |||||
}, | |||||
methods: { | |||||
...mapActions('users', { | |||||
getAllUsers: 'getAll', | |||||
deleteUser: 'delete' | |||||
}), | |||||
...mapActions('system', ['loadMessages', 'loadTimezones']) | |||||
} | |||||
}; | |||||
</script> |
@@ -0,0 +1,28 @@ | |||||
<template> | |||||
<div> | |||||
Single network page | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { mapState, mapActions } from 'vuex' | |||||
export default { | |||||
computed: { | |||||
...mapState({ | |||||
loading: state => state.loading, | |||||
network: state => state.network, | |||||
error: state => state.error | |||||
}) | |||||
}, | |||||
created () { | |||||
this.getById(this.$route.params.uuid); | |||||
}, | |||||
methods: { | |||||
...mapActions('networks', { | |||||
getById: 'getByUuid', | |||||
deleteNetwork: 'delete' | |||||
}) | |||||
} | |||||
}; | |||||
</script> |
@@ -0,0 +1,61 @@ | |||||
<template> | |||||
<div> | |||||
<em v-if="loading">{{messages.loading_networks}}</em> | |||||
<h2>{{messages.table_title_networks}}</h2> | |||||
<div v-if="!networks || networks.length === 0"> | |||||
{{messages.empty_networks}} | |||||
</div> | |||||
<div v-if="networks && networks.length > 0"> | |||||
<table border="1"> | |||||
<thead> | |||||
<tr> | |||||
<th nowrap="nowrap">{{messages.table_head_networks_name}}</th> | |||||
<th nowrap="nowrap">{{messages.table_head_networks_locale}}</th> | |||||
<th nowrap="nowrap">{{messages.table_head_networks_timezone}}</th> | |||||
<th nowrap="nowrap">{{messages.table_head_networks_object_state}}</th> | |||||
<th nowrap="nowrap">{{messages.table_head_networks_action_view}}</th> | |||||
<th nowrap="nowrap">{{messages.table_head_networks_action_start_stop}}</th> | |||||
<th nowrap="nowrap">{{messages.table_head_networks_action_delete}}</th> | |||||
</tr> | |||||
</thead> | |||||
<tbody> | |||||
<tr v-for="network in networks" :key="network.uuid"> | |||||
<td>{{network.name}}.{{network.domainName}}</td> | |||||
<td>{{network.locale}}</td> | |||||
<td>{{network.timezone}}</td> | |||||
<td>{{network.state}}</td> | |||||
<td><router-link :to="{ path: '/networks/'+ network.uuid }">{{messages.table_row_networks_action_view}}</router-link></td> | |||||
<td v-if="network.state === 'running'">{{messages.table_row_networks_action_stop}}</td> | |||||
<td v-if="network.state === 'created'">{{messages.table_row_networks_action_start}}</td> | |||||
<td><a @click="deleteNetwork(network.uuid)" class="text-danger">{{messages.table_row_networks_action_delete}}</a></td> | |||||
</tr> | |||||
</tbody> | |||||
</table> | |||||
</div> | |||||
<hr/> | |||||
<router-link to="/networks/new">{{messages.button_label_new_network}}</router-link> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { mapState, mapActions } from 'vuex' | |||||
export default { | |||||
computed: { | |||||
...mapState({ | |||||
loading: state => state.networks.loading, | |||||
networks: state => state.networks.networks | |||||
}), | |||||
...mapState('system', ['messages']) | |||||
}, | |||||
created () { | |||||
this.getAllNetworks(); | |||||
}, | |||||
methods: { | |||||
...mapActions('networks', { | |||||
getAllNetworks: 'getAll', | |||||
deleteNetwork: 'delete' | |||||
}) | |||||
} | |||||
}; | |||||
</script> |
@@ -0,0 +1,140 @@ | |||||
<template> | |||||
<div> | |||||
<h2>{{messages.form_label_title_new_network}}</h2> | |||||
<form @submit.prevent="handleSubmit"> | |||||
<div class="form-group"> | |||||
<label for="name">{{messages.field_label_network_name}}</label> | |||||
<input type="text" v-model="network.name" v-validate="'required'" name="name" class="form-control" :class="{ 'is-invalid': submitted && errors.has('name') }" /> | |||||
<v-select v-if="domains" :options="domains" label="name" type="text" v-model="network.domain" name="domain" class="form-control" :class="{ 'is-invalid': submitted && errors.has('domain') }"></v-select> | |||||
<div v-if="submitted && errors.has('name')" class="invalid-feedback">{{ errors.first('name') }}</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label htmlFor="locale">{{messages.field_label_locale}}</label> | |||||
<v-select v-if="locales" :options="locales" :reduce="locale => locale.localeCode" label="localeName" type="text" v-model="network.locale" name="locale" class="form-control" :class="{ 'is-invalid': submitted && errors.has('locale') }"></v-select> | |||||
<div v-if="submitted && errors.has('locale')" class="invalid-feedback">{{ errors.first('locale') }}</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label htmlFor="timezone">{{messages.field_label_timezone}}</label> | |||||
<v-select :options="timezoneObjects" :reduce="tz => tz.timezoneId" label="timezoneDescription" :value="detectedTimezone ? detectedTimezone.timeZoneId : null" type="text" v-model="network.timezone" name="timezone" class="form-control" :class="{ 'is-invalid': submitted && errors.has('timezone') }"></v-select> | |||||
<div v-if="submitted && errors.has('timezone')" class="invalid-feedback">{{ errors.first('timezone') }}</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label htmlFor="plan">{{messages.field_label_plan}}</label> | |||||
<v-select v-if="planObjects" :options="planObjects" :reduce="plan => plan.name" label="localName" type="text" v-model="network.plan" name="plan" class="form-control" :class="{ 'is-invalid': submitted && errors.has('plan') }"></v-select> | |||||
<div v-if="submitted && errors.has('plan')" class="invalid-feedback">{{ errors.first('plan') }}</div> | |||||
<div> | |||||
{{messages['plan_description_'+network.plan]}} | |||||
</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label htmlFor="footprint">{{messages.field_label_footprint}}</label> | |||||
<v-select v-if="footprintObjects" :options="footprintObjects" :reduce="footprint => footprint.name" label="name" :value="network.footprint" type="text" v-model="network.footprint" name="footprint" class="form-control" :class="{ 'is-invalid': submitted && errors.has('footprint') }"></v-select> | |||||
<div v-if="submitted && errors.has('footprint')" class="invalid-feedback">{{ errors.first('footprint') }}</div> | |||||
<div> | |||||
{{messages['footprint_description_'+network.footprint]}} | |||||
</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<button class="btn btn-primary" :disabled="status.creating">{{messages.button_label_create_new_network}}</button> | |||||
<img v-show="status.creating" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" /> | |||||
<router-link to="/" class="btn btn-link">{{messages.button_label_cancel}}</router-link> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { mapState, mapActions } from 'vuex' | |||||
import { currentUser } from '../_helpers' | |||||
export default { | |||||
data() { | |||||
return { | |||||
network: { | |||||
name: '', | |||||
domain: '', | |||||
locale: 'en_US', | |||||
timezone: '', | |||||
plan: 'bubble', | |||||
footprint: 'Worldwide' | |||||
}, | |||||
user: currentUser(), | |||||
submitted: false, | |||||
status: '' | |||||
}; | |||||
}, | |||||
computed: { | |||||
...mapState('system', ['messages', 'locales', 'timezones', 'detectedTimezone']), | |||||
...mapState('domains', ['domains']), | |||||
...mapState('plans', ['plans']), | |||||
...mapState('footprints', ['footprints']), | |||||
...mapState('networks', { | |||||
creating: state => state.loading, | |||||
error: state => state.error | |||||
}), | |||||
timezoneObjects: function () { | |||||
const tz_objects = []; | |||||
for (let i=0; i<this.timezones.length; i++) { | |||||
tz_objects.push({ | |||||
timezoneId: this.timezones[i], | |||||
timezoneDescription: this.messages['tz_name_'+this.timezones[i]] + " - " + this.messages['tz_description_'+this.timezones[i]] | |||||
}) | |||||
} | |||||
return tz_objects; | |||||
}, | |||||
planObjects: function () { | |||||
const plans_array = []; | |||||
if (this.plans) { | |||||
for (let i = 0; i < this.plans.length; i++) { | |||||
plans_array.push({ | |||||
...this.plans[i], | |||||
localName: this.messages['plan_name_' + this.plans[i].name], | |||||
description: this.messages['plan_description_' + this.plans[i].name] | |||||
}) | |||||
} | |||||
} | |||||
return plans_array; | |||||
}, | |||||
footprintObjects: function () { | |||||
const fp_array = []; | |||||
if (this.footprints) { | |||||
for (let i = 0; i < this.footprints.length; i++) { | |||||
fp_array.push({ | |||||
...this.footprints[i], | |||||
localName: this.messages['footprint_name_' + this.footprints[i].name], | |||||
description: this.messages['footprint_description_' + this.footprints[i].name] | |||||
}) | |||||
} | |||||
} | |||||
return fp_array; | |||||
} | |||||
}, | |||||
methods: { | |||||
...mapActions('networks', { | |||||
createNewNetwork: 'create' | |||||
}), | |||||
...mapActions('domains', { | |||||
loadDomains: 'getAll' | |||||
}), | |||||
...mapActions('plans', { | |||||
loadPlans: 'getAll' | |||||
}), | |||||
...mapActions('footprints', { | |||||
loadFootprints: 'getAll' | |||||
}), | |||||
handleSubmit(e) { | |||||
this.submitted = true; | |||||
this.$validator.validate().then(valid => { | |||||
if (valid) { | |||||
this.createNewNetwork(this.network); | |||||
} | |||||
}); | |||||
} | |||||
}, | |||||
created() { | |||||
this.loadDomains(currentUser().uuid); | |||||
this.loadPlans(); | |||||
this.loadFootprints(); | |||||
} | |||||
}; | |||||
</script> |
@@ -0,0 +1,93 @@ | |||||
<template> | |||||
<div> | |||||
<h2>Edit Profile</h2> | |||||
<form @submit.prevent="handleSubmit"> | |||||
<div class="form-group"> | |||||
<label for="name">Username</label> | |||||
<input type="text" v-model="name" name="name" class="form-control" :class="{ 'is-invalid': submitted && !name }" /> | |||||
<div v-show="submitted && !name" class="invalid-feedback">Name is required</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label htmlFor="url">Url</label> | |||||
<input type="text" v-model="url" name="url" class="form-control"/> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label htmlFor="description">Description</label> | |||||
<input type="text" v-model="description" name="description" class="form-control"/> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="admin">Admin</label> | |||||
<input type="checkbox" id="admin" v-model="admin"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="suspended">Suspended</label> | |||||
<input type="checkbox" id="suspended" v-model="suspended"> | |||||
</div> | |||||
<div> | |||||
<h3>Auto-Update Policy</h3> | |||||
<div class="form-group"> | |||||
<label for="autoUpdatePolicy.driverUpdates">Drivers</label> | |||||
<input type="checkbox" id="autoUpdatePolicy.driverUpdates" v-model="autoUpdatePolicy.driverUpdates"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="autoUpdatePolicy.appUpdates">Apps</label> | |||||
<input type="checkbox" id="autoUpdatePolicy.appUpdates" v-model="autoUpdatePolicy.appUpdates"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="autoUpdatePolicy.dataUpdates">App Data</label> | |||||
<input type="checkbox" id="autoUpdatePolicy.dataUpdates" v-model="autoUpdatePolicy.dataUpdates"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="autoUpdatePolicy.newStuff">New Drivers/Apps</label> | |||||
<input type="checkbox" id="autoUpdatePolicy.newStuff" v-model="autoUpdatePolicy.newStuff"> | |||||
</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<button class="btn btn-primary" :disabled="status.updating">Update</button> | |||||
<img v-show="status.updating" 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 } from 'vuex' | |||||
import { account } from '../_store/account.module' | |||||
import { currentUser } from '../_helpers' | |||||
export default { | |||||
data () { | |||||
if (this.$router.params && this.$router.params.uuid) { | |||||
return { | |||||
...this.loadUser($this.router.params.uuid), | |||||
submitted: false | |||||
} | |||||
} | |||||
return { | |||||
...currentUser(), | |||||
submitted: false | |||||
}; | |||||
}, | |||||
computed: { | |||||
...mapState('account', ['status']) | |||||
}, | |||||
methods: { | |||||
...mapActions('users', ['update', 'loadUser']), | |||||
handleSubmit (e) { | |||||
this.submitted = true; | |||||
const updatedProfile = { | |||||
uuid: this.uuid, | |||||
name: this.name, | |||||
url: this.url, | |||||
description: this.description, | |||||
admin: this.admin, | |||||
suspended: this.suspended, | |||||
autoUpdatePolicy: this.autoUpdatePolicy | |||||
}; | |||||
if (this.name) { | |||||
this.update(updatedProfile); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
</script> |
@@ -0,0 +1,42 @@ | |||||
<template> | |||||
<div> | |||||
<h1>Hi {{account.user.name}}!</h1> | |||||
<em v-if="users.loading">Loading users...</em> | |||||
<span v-if="users.error" class="text-danger">ERROR: {{users.error}}</span> | |||||
<ul v-if="users.items"> | |||||
<li v-for="user in users.items" :key="user.id"> | |||||
{{user.name}} | |||||
<span v-if="user.deleting"><em> - Deleting...</em></span> | |||||
<span v-else-if="user.deleteError" class="text-danger"> - ERROR: {{user.deleteError}}</span> | |||||
<span v-else> | |||||
<router-link to="/me">Edit</router-link> | |||||
<span v-if="user.name !== account.user.name"> | |||||
- <a @click="deleteUser(user.uuid)" class="text-danger">Delete</a> | |||||
</span> | |||||
</span> | |||||
</li> | |||||
</ul> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { mapState, mapActions } from 'vuex' | |||||
export default { | |||||
computed: { | |||||
...mapState({ | |||||
account: state => state.account, | |||||
users: state => state.users.all | |||||
}) | |||||
}, | |||||
created () { | |||||
this.getAllUsers(); | |||||
}, | |||||
methods: { | |||||
...mapActions('users', { | |||||
getAllUsers: 'getAll', | |||||
deleteUser: 'delete' | |||||
}) | |||||
} | |||||
}; | |||||
</script> |
@@ -0,0 +1,48 @@ | |||||
<template> | |||||
<div class="jumbotron"> | |||||
<sidebar-menu :hide-toggle="true" :menu="menu" v-if="status.loggedIn"/> | |||||
<div class="container"> | |||||
<div class="row"> | |||||
<div class="col-sm-6 offset-sm-3"> | |||||
<div v-if="alert.message" :class="`alert ${alert.type}`">{{alert.message}}</div> | |||||
<router-view></router-view> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { mapState, mapActions } from 'vuex' | |||||
export default { | |||||
name: 'app', | |||||
computed: { | |||||
...mapState('account', ['status']), | |||||
...mapState('system', ['configs', 'messages', 'menu']), | |||||
...mapState({ | |||||
alert: state => state.alert | |||||
}) | |||||
}, | |||||
methods: { | |||||
...mapActions({ clearAlert: 'alert/clear' }), | |||||
...mapActions('system', ['loadSystemConfigs', 'loadMessages', 'loadTimezones', 'detectTimezone']) | |||||
}, | |||||
watch: { | |||||
$route (to, from){ | |||||
// clear alert on location change | |||||
this.clearAlert(); | |||||
} | |||||
}, | |||||
created() { | |||||
// todo: allow user to choose locale | |||||
const locale = 'detect'; | |||||
this.loadTimezones(); | |||||
this.loadMessages('pre_auth', locale); | |||||
this.loadMessages('countries', locale); | |||||
this.loadMessages('timezones', locale); | |||||
this.detectTimezone(); | |||||
this.loadSystemConfigs(); // determine if we can show the registration link | |||||
} | |||||
}; | |||||
</script> |
@@ -0,0 +1,54 @@ | |||||
<template> | |||||
<div> | |||||
<h2>{{messages.form_label_title_login}}</h2> | |||||
<form @submit.prevent="handleSubmit"> | |||||
<div class="form-group"> | |||||
<label for="name">{{messages.field_label_username}}</label> | |||||
<input type="text" v-model="name" name="name" class="form-control" :class="{ 'is-invalid': submitted && !name }" /> | |||||
<div v-show="submitted && !name" class="invalid-feedback">Name is required</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">Password is required</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<button class="btn btn-primary" :disabled="status.loggingIn">{{messages.button_label_login}}</button> | |||||
<img v-show="status.loggingIn" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" /> | |||||
<router-link v-if="configs && configs.allowRegistration" to="/register" class="btn btn-link">{{messages.button_label_register}}</router-link> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { mapState, mapActions } from 'vuex' | |||||
export default { | |||||
data () { | |||||
return { | |||||
name: '', | |||||
password: '', | |||||
submitted: false | |||||
} | |||||
}, | |||||
computed: { | |||||
...mapState('account', ['status']), | |||||
...mapState('system', ['configs', 'messages']) | |||||
}, | |||||
created () { | |||||
this.logout(); // reset login status | |||||
}, | |||||
methods: { | |||||
...mapActions('account', ['login', 'logout']), | |||||
...mapActions('system', ['loadSystemConfigs']), | |||||
handleSubmit (e) { | |||||
this.submitted = true; | |||||
const { name, password } = this; | |||||
if (name && password) { | |||||
this.login({user: {name, password}, messages: this.messages, errors: this.errors}); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
</script> |
@@ -0,0 +1,75 @@ | |||||
<template> | |||||
<div> | |||||
<h2>{{messages.form_label_title_register}}</h2> | |||||
<form @submit.prevent="handleSubmit"> | |||||
<div class="form-group"> | |||||
<label for="name">{{messages.field_label_username}}</label> | |||||
<input type="text" v-model="user.name" v-validate="'required'" name="name" class="form-control" :class="{ 'is-invalid': submitted && errors.has('name') }" /> | |||||
<div v-if="submitted && errors.has('name')" class="invalid-feedback">{{ errors.first('name') }}</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label htmlFor="password">{{messages.field_label_password}}</label> | |||||
<input type="password" v-model="user.password" v-validate="{ required: true, min: 6 }" name="password" class="form-control" :class="{ 'is-invalid': submitted && errors.has('password') }" /> | |||||
<div v-if="submitted && errors.has('password')" class="invalid-feedback">{{ errors.first('password') }}</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label htmlFor="type">{{messages.field_label_contactType}}</label> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="email">{{messages.field_label_email}}</label> | |||||
<input type="text" v-model="user.contact.info" name="email" class="form-control" :class="{ 'is-invalid': submitted && errors.has('email') }" /> | |||||
<div v-if="submitted && errors.has('email')" class="invalid-feedback">{{ errors.first('email') }}</div> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="user.contact.receiveInformationalMessages">{{messages.field_label_receiveInformationalMessages}}</label> | |||||
<input type="checkbox" id="user.contact.receiveInformationalMessages" v-model="user.contact.receiveInformationalMessages"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<label for="user.contact.receivePromotionalMessages">{{messages.field_label_receiveInformationalMessages}}</label> | |||||
<input type="checkbox" id="user.contact.receivePromotionalMessages" v-model="user.contact.receivePromotionalMessages"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<button class="btn btn-primary" :disabled="status.registering">{{messages.button_label_register}}</button> | |||||
<img v-show="status.registering" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" /> | |||||
<router-link to="/login" class="btn btn-link">{{messages.button_label_cancel}}</router-link> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { mapState, mapActions } from 'vuex' | |||||
export default { | |||||
data () { | |||||
return { | |||||
user: { | |||||
name: '', | |||||
password: '', | |||||
contact: { | |||||
type: 'email', | |||||
info: '', | |||||
receiveInformationalMessages: false, | |||||
receivePromotionalMessages: false | |||||
} | |||||
}, | |||||
submitted: false | |||||
} | |||||
}, | |||||
computed: { | |||||
...mapState('account', ['status']), | |||||
...mapState('system', ['messages', 'countries']) | |||||
}, | |||||
methods: { | |||||
...mapActions('account', ['register']), | |||||
handleSubmit(e) { | |||||
this.submitted = true; | |||||
this.$validator.validate().then(valid => { | |||||
if (valid) { | |||||
this.register({user: this.user, messages: this.messages, errors: this.errors}); | |||||
} | |||||
}); | |||||
} | |||||
} | |||||
}; | |||||
</script> |
@@ -0,0 +1,15 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<meta charset="UTF-8"> | |||||
<title>Bubble</title> | |||||
<link href="//netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" /> | |||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous"> | |||||
<style> | |||||
a { cursor: pointer; } | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<div id="app"></div> | |||||
</body> | |||||
</html> |
@@ -0,0 +1,32 @@ | |||||
import Vue from 'vue'; | |||||
import VeeValidate from 'vee-validate'; | |||||
import VueSidebarMenu from 'vue-sidebar-menu' | |||||
import vSelect from 'vue-select' | |||||
// not sure what the best way is to include these icons | |||||
// import { library } from '@fortawesome/fontawesome-svg-core' | |||||
// import { faCoffee } from '@fortawesome/free-solid-svg-icons' | |||||
// import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' | |||||
// library.add(faCoffee); | |||||
import { store } from './_store'; | |||||
import { router } from './_helpers'; | |||||
import App from './app/App'; | |||||
// why can't i import this? | |||||
// import 'vue-select/dist/vue-select.css'; | |||||
Vue.use(VeeValidate); | |||||
Vue.use(VueSidebarMenu); | |||||
Vue.component('v-select', vSelect); | |||||
Vue.config.productionTip = false; | |||||
// not sure what the best way is to include these icons, we reference them programmatically via string resource/messages | |||||
// Vue.component('font-awesome-icon', FontAwesomeIcon); | |||||
new Vue({ | |||||
el: '#app', | |||||
router, | |||||
store, | |||||
render: h => h(App) | |||||
}); |
@@ -0,0 +1,21 @@ | |||||
MIT License | |||||
Copyright (c) 2018 Jason Watmore | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. |
@@ -0,0 +1,5 @@ | |||||
# vue-vuex-registration-login-example | |||||
Vue + Vuex - User Registration and Login Tutorial & Example | |||||
To see a demo and further details go to http://jasonwatmore.com/post/2018/07/14/vue-vuex-user-registration-and-login-tutorial-example |
@@ -0,0 +1,36 @@ | |||||
var path = require('path'); | |||||
var HtmlWebpackPlugin = require('html-webpack-plugin'); | |||||
module.exports = { | |||||
mode: 'development', | |||||
resolve: { | |||||
extensions: ['.js', '.vue'] | |||||
}, | |||||
module: { | |||||
rules: [ | |||||
{ | |||||
test: /\.vue?$/, | |||||
exclude: /(node_modules)/, | |||||
use: 'vue-loader' | |||||
}, | |||||
{ | |||||
test: /\.js?$/, | |||||
exclude: /(node_modules)/, | |||||
use: 'babel-loader' | |||||
} | |||||
] | |||||
}, | |||||
plugins: [new HtmlWebpackPlugin({ | |||||
template: './src/index.html' | |||||
})], | |||||
devServer: { | |||||
historyApiFallback: true | |||||
}, | |||||
externals: { | |||||
// global app config object | |||||
config: JSON.stringify({ | |||||
// apiUrl: 'http://localhost:4000' | |||||
apiUrl: '/api' | |||||
}) | |||||
} | |||||
} |