From 789d0f5fcd173c94977df4badc6f97fcc72144ff Mon Sep 17 00:00:00 2001 From: Simone Gotti Date: Mon, 13 May 2019 16:29:47 +0200 Subject: [PATCH] *: handle api errors * Use a global error when fetching data * Use a local error message when calling a mutating api --- src/App.vue | 42 ++++++++- src/components/createproject.vue | 33 +++++-- src/components/createprojectgroup.vue | 23 +++-- src/components/projectgroupsettings.vue | 40 ++++++-- src/components/projects.vue | 39 +++++--- src/components/projectsettings.vue | 64 ++++++++++--- src/components/remoterepos.vue | 7 +- src/components/run.vue | 8 +- src/components/runs.vue | 33 +++++-- src/components/task.vue | 14 ++- src/components/usersettings.vue | 77 ++++++++++++++-- src/router.js | 11 ++- src/store.js | 13 ++- src/util/auth.js | 29 ++++-- src/util/data.js | 116 ++++++++++++++++-------- src/views/Login.vue | 55 ++++++----- src/views/Logout.vue | 4 +- src/views/Oauth2.vue | 29 ++++-- src/views/Project.vue | 14 ++- src/views/ProjectGroup.vue | 19 ---- src/views/Register.vue | 18 ++-- src/views/User.vue | 7 +- 22 files changed, 522 insertions(+), 173 deletions(-) diff --git a/src/App.vue b/src/App.vue index d349903..13b9964 100644 --- a/src/App.vue +++ b/src/App.vue @@ -43,8 +43,27 @@ -
- + +
+
+
+ +
+
+
+
+
@@ -57,7 +76,12 @@ export default { name: "App", components: {}, computed: { - ...mapGetters(["user"]) + ...mapGetters(["error", "user"]) + }, + data() { + return { + routerActive: true + }; }, watch: { user: function(user) { @@ -68,6 +92,14 @@ export default { }); } } + }, + // method to reload current view from https://github.com/vuejs/vue-router/issues/296#issuecomment-356530037 + methods: { + reload() { + this.$store.dispatch("setError", null); + this.routerActive = false; + this.$nextTick(() => (this.routerActive = true)); + } } }; @@ -78,4 +110,8 @@ export default { .main-container { margin-top: 2rem; } + +.global-error-message { + margin-top: 10rem; +} \ No newline at end of file diff --git a/src/components/createproject.vue b/src/components/createproject.vue index d6a8591..156b386 100644 --- a/src/components/createproject.vue +++ b/src/components/createproject.vue @@ -19,9 +19,9 @@
-
- -
+ +
+
{{ createProjectError }}
@@ -47,6 +47,7 @@ export default { }, data() { return { + createProjectError: null, user: null, remoteSources: null, remoteRepos: [], @@ -56,23 +57,32 @@ export default { }; }, methods: { + resetErrors() { + this.createProjectError = null; + }, repoSelected(remoteSource, repoPath) { this.selectedRemoteSource = remoteSource; this.remoteRepoPath = repoPath; }, async createProject() { + this.resetErrors(); + let refArray = [this.ownertype, this.ownername]; if (this.projectgroupref) { refArray = [...refArray, ...this.projectgroupref]; } let parentref = refArray.join("/"); - await createProject( + let { error } = await createProject( parentref, this.projectName, this.selectedRemoteSource.name, this.remoteRepoPath ); + if (error) { + this.createProjectError = error; + return; + } let projectref = [this.projectName]; if (this.projectgroupref) { @@ -84,9 +94,20 @@ export default { } }, created: async function() { - this.user = await fetchCurrentUser(); + let { data, error } = await fetchCurrentUser(); + if (error) { + this.$store.dispatch("setError", error); + return; + } + this.user = data; + // TODO(sgotti) filter only remote source where the user has a linked account - this.remoteSources = await fetchRemoteSources(); + ({ data, error } = await fetchRemoteSources()); + if (error) { + this.$store.dispatch("setError", error); + return; + } + this.remoteSources = data; } }; diff --git a/src/components/createprojectgroup.vue b/src/components/createprojectgroup.vue index 289b3ea..b506ed9 100644 --- a/src/components/createprojectgroup.vue +++ b/src/components/createprojectgroup.vue @@ -15,9 +15,9 @@
-
- -
+ +
+
{{ createProjectGroupError }}
@@ -27,8 +27,6 @@ import { createProjectGroup } from "@/util/data.js"; import { projectGroupLink } from "@/util/link.js"; -import remoterepos from "@/components/remoterepos.vue"; - export default { components: {}, name: "createprojectgroup", @@ -39,18 +37,31 @@ export default { }, data() { return { + createProjectGroupError: null, projectGroupName: null }; }, methods: { + resetErrors() { + this.createProjectGroupError = null; + }, async createProjectGroup() { + this.resetErrors(); + let refArray = [this.ownertype, this.ownername]; if (this.projectgroupref) { refArray = [...refArray, ...this.projectgroupref]; } let parentref = refArray.join("/"); - await createProjectGroup(parentref, this.projectGroupName); + let { error } = await createProjectGroup( + parentref, + this.projectGroupName + ); + if (error) { + this.createProjectGroupError = error; + return; + } let projectgroupref = [this.projectGroupName]; if (this.projectgroupref) { diff --git a/src/components/projectgroupsettings.vue b/src/components/projectgroupsettings.vue index 8450287..0b1b8c2 100644 --- a/src/components/projectgroupsettings.vue +++ b/src/components/projectgroupsettings.vue @@ -42,6 +42,9 @@ >Delete Project Group +
+
{{ deleteProjectGroupError }}
+
@@ -64,6 +67,7 @@ export default { }, data() { return { + deleteProjectGroupError: null, variables: [], allvariables: [], projectGroupNameToDelete: "" @@ -83,6 +87,9 @@ export default { } }, methods: { + resetErrors() { + this.deleteProjectGroupError = null; + }, async deleteProjectGroup() { let projectgroupref = [ this.ownertype, @@ -91,7 +98,11 @@ export default { ].join("/"); if (this.projectGroupNameToDelete == this.projectGroupName) { - let res = await deleteProjectGroup(projectgroupref); + let { error } = await deleteProjectGroup(projectgroupref); + if (error) { + this.deleteProjectGroupError = error; + return; + } this.$router.push( projectGroupLink( this.ownertype, @@ -103,16 +114,33 @@ export default { } }, created: async function() { - this.variables = await fetchVariables( + let projectgroupref = [ + this.ownertype, + this.ownername, + ...this.projectgroupref + ].join("/"); + + let { data, error } = await fetchVariables( "projectgroup", - [this.ownertype, this.ownername, ...this.projectgroupref].join("/"), + projectgroupref, false ); - this.allvariables = await fetchVariables( + if (error) { + this.$store.dispatch("setError", error); + return; + } + this.variables = data; + + ({ data, error } = await fetchVariables( "projectgroup", - [this.ownertype, this.ownername, ...this.projectgroupref].join("/"), + projectgroupref, true - ); + )); + if (error) { + this.$store.dispatch("setError", error); + return; + } + this.allvariables = data; } }; diff --git a/src/components/projects.vue b/src/components/projects.vue index 76dbf1e..86554d3 100644 --- a/src/components/projects.vue +++ b/src/components/projects.vue @@ -32,7 +32,11 @@ diff --git a/src/components/remoterepos.vue b/src/components/remoterepos.vue index bff9970..b8aed6c 100644 --- a/src/components/remoterepos.vue +++ b/src/components/remoterepos.vue @@ -43,7 +43,12 @@ export default { this.$emit("reposelected", this.remoterepos[index].path); }, async fetchRemoteRepos(remotesourceid) { - this.remoterepos = await userRemoteRepos(remotesourceid); + let { data, error } = await userRemoteRepos(remotesourceid); + if (error) { + this.$store.dispatch("setError", error); + return; + } + this.remoterepos = data; } }, created: function() { diff --git a/src/components/run.vue b/src/components/run.vue index bf85026..4000c00 100644 --- a/src/components/run.vue +++ b/src/components/run.vue @@ -101,7 +101,13 @@ export default { return "unknown"; }, async fetchRun() { - this.run = await fetchRun(this.runid); + let { data, error } = await fetchRun(this.runid); + if (error) { + this.$store.dispatch("setError", error); + return; + } + this.run = data; + // sort tasks by level let tasks = this.run.tasks; let sortedTasks = Object.keys(this.run.tasks) diff --git a/src/components/runs.vue b/src/components/runs.vue index 1f3f479..9e926a8 100644 --- a/src/components/runs.vue +++ b/src/components/runs.vue @@ -67,8 +67,7 @@ diff --git a/src/router.js b/src/router.js index e292339..6add202 100644 --- a/src/router.js +++ b/src/router.js @@ -21,9 +21,11 @@ import Logout from "./views/Logout.vue"; import { parseRef } from "@/util/link.js"; +import store from "./store"; + Vue.use(VueRouter); -export default new VueRouter({ +const router = new VueRouter({ mode: "history", routes: [ { @@ -327,3 +329,10 @@ export default new VueRouter({ }, ] }); + +router.beforeEach((to, from, next) => { + store.dispatch("setError", null); + next() +}) + +export default router \ No newline at end of file diff --git a/src/store.js b/src/store.js index 2ab2d39..3369abf 100644 --- a/src/store.js +++ b/src/store.js @@ -4,11 +4,16 @@ import Vuex from 'vuex' Vue.use(Vuex) const state = { + error: null, user: null, - registeruser: null + registeruser: null, } const getters = { + // global error + error: state => { + return state.error + }, user: state => { return state.user }, @@ -18,6 +23,9 @@ const getters = { } const mutations = { + setError(state, error) { + state.error = error + }, setUser(state, user) { state.user = user }, @@ -27,6 +35,9 @@ const mutations = { } const actions = { + setError({ commit }, error) { + commit('setError', error) + }, setUser({ commit }, user) { commit('setUser', user) }, diff --git a/src/util/auth.js b/src/util/auth.js index f3efb1d..3f47149 100644 --- a/src/util/auth.js +++ b/src/util/auth.js @@ -7,13 +7,13 @@ const USER_KEY = 'user'; let API_URL = window.CONFIG.API_URL; let API_BASE_PATH = window.CONFIG.API_BASE_PATH; -export function login(token, user) { +export function setLoggedUser(token, user) { setIdToken(token); setUser(user); store.dispatch('setUser', user) } -export function logout() { +export function doLogout() { unsetIdToken(); unsetUser() store.dispatch('setUser', null) @@ -48,6 +48,19 @@ export function oauth2callbackurl() { return new URL(API_URL + "/oauth2/callback"); } +export async function loginapi(init) { + if (init === undefined) { + init = {} + } + + try { + let res = await window.fetch(loginurl(), init) + return res + } catch (e) { + throw e + } +} + export async function fetch(url, init) { if (init === undefined) { init = {} @@ -60,10 +73,14 @@ export async function fetch(url, init) { init.headers["Authorization"] = "bearer " + idToken } - let res = await window.fetch(url, init) - if (res.status === 401) { - router.push({ name: "login" }) - } else { return res } + try { + let res = await window.fetch(url, init) + if (res.status === 401) { + router.push({ name: "login" }) + } else { return res } + } catch (e) { + throw e + } } export function setIdToken(idToken) { diff --git a/src/util/data.js b/src/util/data.js index 8446fb7..1edcc7c 100644 --- a/src/util/data.js +++ b/src/util/data.js @@ -1,9 +1,49 @@ -import { apiurl, fetch } from "@/util/auth"; +import { apiurl, loginapi } from "@/util/auth"; +import { fetch as authfetch } from "@/util/auth"; + +export async function fetch(url, init) { + try { + let res = await authfetch(url, init) + if (!res.ok) { + let data = await res.json() + return { data: null, error: data.message } + } else { + if (res.status == 204) { + return { data: null, error: null } + } + return { data: await res.json(), error: null } + } + } catch (e) { + return { data: null, error: "api call failed: " + e } + } +} + +export async function login(username, password, remotesourcename) { + let init = { + method: "POST", + body: JSON.stringify({ + remote_source_name: remotesourcename, + login_name: username, + password: password + }) + } + + try { + let res = await loginapi(init) + if (!res.ok) { + let data = await res.json() + return { data: null, error: data.message } + } else { + return { data: await res.json(), error: null } + } + } catch (e) { + return { data: null, error: "api call failed: " + e } + } +} export async function fetchCurrentUser() { let path = "/user" - let res = await fetch(apiurl(path)); - return res.json(); + return await fetch(apiurl(path)); } export async function fetchRuns(group, lastrun) { @@ -14,25 +54,40 @@ export async function fetchRuns(group, lastrun) { if (lastrun) { u.searchParams.append("lastrun", true) } - let res = await fetch(u) - return res.json(); + return await fetch(u) } export async function fetchRun(runid) { - let res = await fetch(apiurl("/runs/" + runid)); - return res.json(); + return await fetch(apiurl("/runs/" + runid)); } export async function fetchTask(runid, taskid) { - let res = await fetch(apiurl("/runs/" + runid + "/tasks/" + taskid)) - return res.json(); + return await fetch(apiurl("/runs/" + runid + "/tasks/" + taskid)) +} + +export async function fetchUser(username) { + let path = "/users/" + username + return await fetch(apiurl(path)); +} + +export async function fetchProjectGroupSubgroups(projectgroupref) { + let path = "/projectgroups/" + encodeURIComponent(projectgroupref) + path += "/subgroups"; + + return await fetch(apiurl(path)); +} + +export async function fetchProjectGroupProjects(projectgroupref) { + let path = "/projectgroups/" + encodeURIComponent(projectgroupref) + path += "/projects"; + + return await fetch(apiurl(path)); } export async function fetchProject(ref) { let path = "/projects/" + encodeURIComponent(ref) - let res = await fetch(apiurl(path)); - return res.json(); + return await fetch(apiurl(path)); } export async function fetchVariables(ownertype, ref, all) { @@ -47,8 +102,7 @@ export async function fetchVariables(ownertype, ref, all) { if (all) { path += "?tree&removeoverridden"; } - let res = await fetch(apiurl(path)); - return res.json(); + return await fetch(apiurl(path)); } export async function createUserToken(username, tokenname) { @@ -59,8 +113,7 @@ export async function createUserToken(username, tokenname) { token_name: tokenname, }) } - let res = await fetch(apiurl(path), init) - return res.json(); + return await fetch(apiurl(path), init) } export async function deleteUserToken(username, tokenname) { @@ -68,8 +121,7 @@ export async function deleteUserToken(username, tokenname) { let init = { method: "DELETE", } - let res = await fetch(apiurl(path), init) - return res.text(); + return await fetch(apiurl(path), init) } export async function restartRun(runid, fromStart) { @@ -81,8 +133,7 @@ export async function restartRun(runid, fromStart) { from_start: fromStart }) } - let res = await fetch(apiurl(path), init) - return res.json(); + return await fetch(apiurl(path), init) } export async function stopRun(runid) { @@ -93,8 +144,7 @@ export async function stopRun(runid) { action_type: "stop" }) } - let res = await fetch(apiurl(path), init) - return res.json(); + return await fetch(apiurl(path), init) } export async function approveTask(runid, taskid) { @@ -105,20 +155,17 @@ export async function approveTask(runid, taskid) { action_type: "approve" }) } - let res = await fetch(apiurl(path), init) - return res.json(); + return await fetch(apiurl(path), init) } export async function fetchRemoteSources() { let path = "/remotesources" - let res = await fetch(apiurl(path)); - return res.json(); + return await fetch(apiurl(path)); } export async function userRemoteRepos(remotesourceid) { let path = "/user/remoterepos/" + remotesourceid - let res = await fetch(apiurl(path)); - return res.json(); + return await fetch(apiurl(path)); } export async function createProjectGroup(parentref, name) { @@ -131,8 +178,7 @@ export async function createProjectGroup(parentref, name) { visibility: "public", }) } - let res = await fetch(apiurl(path), init) - return res.json(); + return await fetch(apiurl(path), init) } export async function createProject(parentref, name, remotesourcename, remoterepopath) { @@ -147,8 +193,7 @@ export async function createProject(parentref, name, remotesourcename, remoterep repo_path: remoterepopath, }) } - let res = await fetch(apiurl(path), init) - return res.json(); + return await fetch(apiurl(path), init) } export async function updateProject(projectref, name, visibility) { @@ -160,8 +205,7 @@ export async function updateProject(projectref, name, visibility) { visibility: visibility, }) } - let res = await fetch(apiurl(path), init) - return res.json() + return await fetch(apiurl(path), init) } export async function deleteProject(projectref) { @@ -169,8 +213,7 @@ export async function deleteProject(projectref) { let init = { method: "DELETE", } - let res = await fetch(apiurl(path), init) - return res.text(); + return await fetch(apiurl(path), init) } export async function deleteProjectGroup(projectgroupref) { @@ -178,6 +221,5 @@ export async function deleteProjectGroup(projectgroupref) { let init = { method: "DELETE", } - let res = await fetch(apiurl(path), init) - return res.text(); + return await fetch(apiurl(path), init) } \ No newline at end of file diff --git a/src/views/Login.vue b/src/views/Login.vue index 70fa7fa..0268e91 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -1,17 +1,23 @@ diff --git a/src/views/Logout.vue b/src/views/Logout.vue index 8a7bde5..921b5b5 100644 --- a/src/views/Logout.vue +++ b/src/views/Logout.vue @@ -1,10 +1,10 @@ diff --git a/src/views/Register.vue b/src/views/Register.vue index 0ca1f84..9d0f091 100644 --- a/src/views/Register.vue +++ b/src/views/Register.vue @@ -31,7 +31,9 @@ import { mapGetters } from "vuex"; import LoginForm from "@/components/loginform"; import RegisterForm from "@/components/registerform"; -import { apiurl, authorizeurl, registerurl, fetch, logout } from "@/util/auth"; +import { fetchRemoteSources } from "@/util/data"; + +import { authorizeurl, registerurl, fetch, doLogout } from "@/util/auth"; export default { name: "Register", @@ -48,9 +50,13 @@ export default { ...mapGetters(["registeruser"]) }, methods: { - async getRemoteSources() { - let res = await (await fetch(apiurl("/remotesources"))).json(); - this.remotesources = res; + async fetchRemoteSources() { + let { data, error } = await fetchRemoteSources(); + if (error) { + this.$store.dispatch("setError", error); + return; + } + this.remotesources = data; }, async doAuthorize(rsName, username, password) { let u = authorizeurl(); @@ -99,8 +105,8 @@ export default { } }, created: function() { - logout(); - this.getRemoteSources(); + doLogout(); + this.fetchRemoteSources(); } }; diff --git a/src/views/User.vue b/src/views/User.vue index b1042f6..0feb2d5 100644 --- a/src/views/User.vue +++ b/src/views/User.vue @@ -117,7 +117,12 @@ export default { watch: { $route: async function(route) { if (route.params.runid) { - this.run = await fetchRun(route.params.runid); + let { data, error } = await fetchRun(route.params.runid); + if (error) { + this.$store.dispatch("setError", error); + return; + } + this.run = data; } } },