From 75e8ae6ebd9df23275fb14eea88da0b56d006313 Mon Sep 17 00:00:00 2001
From: HampusM <hampus@hampusmat.com>
Date: Wed, 9 Jun 2021 17:37:43 +0200
Subject: Frontend has proper error handling & api fetching is in it's own file

---
 .../client/src/components/BaseErrorMessage.vue     | 27 ++++++
 .../client/src/components/RepositoryTreeTree.vue   |  2 +-
 packages/client/src/util/fetch.js                  | 35 ++++++++
 packages/client/src/views/Home.vue                 | 29 +++++--
 packages/client/src/views/RepositoryCommit.vue     | 96 ++++++++++++----------
 packages/client/src/views/RepositoryLog.vue        | 32 +++++---
 packages/client/src/views/RepositoryTree.vue       | 49 +++++------
 7 files changed, 185 insertions(+), 85 deletions(-)
 create mode 100644 packages/client/src/components/BaseErrorMessage.vue
 create mode 100644 packages/client/src/util/fetch.js

(limited to 'packages/client')

diff --git a/packages/client/src/components/BaseErrorMessage.vue b/packages/client/src/components/BaseErrorMessage.vue
new file mode 100644
index 0000000..7f193ce
--- /dev/null
+++ b/packages/client/src/components/BaseErrorMessage.vue
@@ -0,0 +1,27 @@
+<template>
+	<div v-if="fetchFailed" class="fs-5">
+		<span class="fetch-error-title">Error</span>
+		<p>{{ fetchFailed }}</p>
+	</div>
+</template>
+
+<script>
+export default {
+	name: "BaseErrorMessage",
+	props: {
+		fetchFailed: {
+			type: String,
+			required: true
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@use "../scss/colors";
+
+.fetch-error-title {
+	color: colors.$danger;
+	font-weight: 700;
+}
+</style>
diff --git a/packages/client/src/components/RepositoryTreeTree.vue b/packages/client/src/components/RepositoryTreeTree.vue
index 89ffb81..376cafc 100644
--- a/packages/client/src/components/RepositoryTreeTree.vue
+++ b/packages/client/src/components/RepositoryTreeTree.vue
@@ -38,7 +38,7 @@
 					</a>
 				</td>
 				<td>
-					{{ getPrettyLastUpdated(entry.last_commit.time) }}
+					{{ getPrettyLastUpdated(entry.last_commit.date) }}
 				</td>
 			</tr>
 		</tbody>
diff --git a/packages/client/src/util/fetch.js b/packages/client/src/util/fetch.js
new file mode 100644
index 0000000..a6a43b0
--- /dev/null
+++ b/packages/client/src/util/fetch.js
@@ -0,0 +1,35 @@
+export default async function(endpoint, fetch_failed, is_loading, data_name) {
+	const fetch_timeout = setTimeout(() => {
+		if(!fetch_failed.value) {
+			fetch_failed.value = `Failed to fetch ${data_name} data.`;
+			is_loading.value = false;
+		}
+	}, 5000);
+
+	const data_req = await fetch(`${window.location.protocol}//${window.location.host}/api/v1/${endpoint}`).catch(() => {
+		if(!fetch_failed.value) {
+			fetch_failed.value = `Failed to fetch ${data_name} data.`;
+			is_loading.value = false;
+			clearTimeout(fetch_timeout);
+		}
+		return null;
+	});
+
+	if(data_req !== null) {
+		const data = await data_req.json().catch(() => {
+			fetch_failed.value = "Failed to parse server response.";
+		});
+
+		if(data_req.ok) {
+			clearTimeout(fetch_timeout);
+			is_loading.value = false;
+			return data.data;
+		} else {
+			fetch_failed.value = `Failed to fetch ${data_name} data.`;
+		}
+	}
+
+	clearTimeout(fetch_timeout);
+	is_loading.value = false;
+	return null;
+};
diff --git a/packages/client/src/views/Home.vue b/packages/client/src/views/Home.vue
index 305d42b..397a3a1 100644
--- a/packages/client/src/views/Home.vue
+++ b/packages/client/src/views/Home.vue
@@ -3,8 +3,8 @@
 		<HomeHeader />
 		<HomeProjectsHeader />
 		<div class="row mx-0">
-			<div class="col ms-4">
-				<ul id="repos">
+			<div class="col ms-4 vld-parent">
+				<ul id="repos" v-if="projects">
 					<li v-for="(project, project_name, index) in projects" :key="index">
 						<div v-if="(search !== null && project_name.includes(search)) || search == null">
 							<span class="fs-3">
@@ -17,6 +17,11 @@
 						</div>
 					</li>
 				</ul>
+				<BaseErrorMessage :fetch-failed="fetch_failed" />
+				<Loading
+					:active="is_loading" :height="24"
+					:width="24" color="#ffffff"
+					:opacity="0" :is-full-page="false" />
 			</div>
 		</div>
 	</div>
@@ -25,26 +30,33 @@
 <script>
 import HomeHeader from "@/components/HomeHeader";
 import HomeProjectsHeader from "@/components/HomeProjectsHeader";
+import Loading from "vue-loading-overlay";
+import BaseErrorMessage from "@/components/BaseErrorMessage";
+import fetchData from "@/util/fetch";
 import { ref } from "vue";
 
 export default {
 	name: "Home",
 	components: {
 		HomeHeader,
-		HomeProjectsHeader
+		HomeProjectsHeader,
+		Loading,
+		BaseErrorMessage
 	},
 	setup() {
 		const projects = ref({});
 		const search = ref("");
+		const is_loading = ref(true);
+		const fetch_failed = ref(null);
 
 		const fetchProjects = async() => {
-			const projects_data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/repos`)).json();
-			projects.value = projects_data.data;
+			const projects_data = await fetchData("repos", fetch_failed, is_loading, "projects");
+			projects.value = projects_data;
 		};
 
 		search.value = (new URLSearchParams(window.location.search)).get("q");
 
-		return { projects, search, fetchProjects };
+		return { projects, search, is_loading, fetch_failed, fetchProjects };
 	},
 	mount() {
 		this.fetchProjects();
@@ -58,6 +70,7 @@ export default {
 <style lang="scss" scoped>
 @use "../scss/colors";
 @import "../scss/bootstrap";
+@import "~vue-loading-overlay/dist/vue-loading.css";
 
 #repos {
 	margin-top: 25px;
@@ -81,4 +94,8 @@ ul {
 	flex-flow: column;
 	height: 100vh;
 }
+
+.row {
+	height: 100%;
+}
 </style>
diff --git a/packages/client/src/views/RepositoryCommit.vue b/packages/client/src/views/RepositoryCommit.vue
index 6bbbcaa..97c06b0 100644
--- a/packages/client/src/views/RepositoryCommit.vue
+++ b/packages/client/src/views/RepositoryCommit.vue
@@ -2,45 +2,49 @@
 	<div class="row mx-0">
 		<div class="col ms-2 ps-4 ps-sm-5 fs-5 vld-parent">
 			<BaseBreadcrumb :items="[{ name: 'Log', path: '/' + $router.currentRoute._rawValue.params.repo + '/log' }]" :active-item="$router.currentRoute._rawValue.params.commit" />
-			<table id="commit-info" class="table table-dark">
-				<tbody>
-					<tr>
-						<td class="commit-info-title">
-							Author
-						</td>
-						<td>{{ commit_data["author"] }}</td>
-					</tr>
-					<tr>
-						<td class="commit-info-title">
-							Date
-						</td>
-						<td>{{ commit_data["date"] }}</td>
-					</tr>
-					<tr>
-						<td class="commit-info-title">
-							Message
-						</td>
-						<td>{{ commit_data["message"] }}</td>
-					</tr>
-				</tbody>
-			</table>
+			<template v-if="commit">
+				<table
+					id="commit-info" class="table table-dark">
+					<tbody>
+						<tr>
+							<td class="commit-info-title">
+								Author
+							</td>
+							<td>{{ commit["author"] }}</td>
+						</tr>
+						<tr>
+							<td class="commit-info-title">
+								Date
+							</td>
+							<td>{{ commit["date"] }}</td>
+						</tr>
+						<tr>
+							<td class="commit-info-title">
+								Message
+							</td>
+							<td>{{ commit["message"] }}</td>
+						</tr>
+					</tbody>
+				</table>
+				<div
+					v-for="(patch, index) in commit['patches']" :key="index"
+					class="commit-patch">
+					<div class="commit-patch-header">
+						<span class="fw-bold">{{ (patch.to === "/dev/null") ? patch.from : patch.to }} </span>
+						<span v-if="patch.to === '/dev/null'">Deleted</span>
+						<div class="commit-patch-add-del">
+							<span>+{{ patch.additions }}</span>
+							<span>-{{ patch.deletions }}</span>
+						</div>
+					</div>
+					<CommitPatch :patch="patch" />
+				</div>
+			</template>
+			<BaseErrorMessage :fetch-failed="fetch_failed" />
 			<Loading
 				:active="is_loading" :height="24"
 				:width="24" color="#ffffff"
 				:opacity="0" :is-full-page="false" />
-			<div
-				v-for="(patch, index) in commit_data['patches']" :key="index"
-				class="commit-patch">
-				<div class="commit-patch-header">
-					<span class="fw-bold">{{ (patch.to === "/dev/null") ? patch.from : patch.to }} </span>
-					<span v-if="patch.to === '/dev/null'">Deleted</span>
-					<div class="commit-patch-add-del">
-						<span>+{{ patch.additions }}</span>
-						<span>-{{ patch.deletions }}</span>
-					</div>
-				</div>
-				<CommitPatch :patch="patch" />
-			</div>
 		</div>
 	</div>
 </template>
@@ -49,28 +53,34 @@
 import BaseBreadcrumb from "@/components/BaseBreadcrumb";
 import CommitPatch from "@/components/CommitPatch";
 import Loading from "vue-loading-overlay";
+import BaseErrorMessage from "@/components/BaseErrorMessage";
 import { ref } from "vue";
 import { format } from "date-fns";
+import fetchData from "@/util/fetch";
 
 export default {
 	name: "RepositoryCommit",
 	components: {
 		BaseBreadcrumb,
 		Loading,
-		CommitPatch
+		CommitPatch,
+		BaseErrorMessage
 	},
 	setup() {
-		const commit_data = ref({});
+		const commit = ref(null);
 		const is_loading = ref(true);
+		const fetch_failed = ref(null);
+
+		const fetchCommit = async(repository, commit_id) => {
+			const commit_data = await fetchData(`repos/${repository}/log/${commit_id}`, fetch_failed, is_loading, "commit");
 
-		const fetchCommit = async(repository, commit) => {
-			const data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/repos/${repository}/log/${commit}`)).json();
-			data.data.date = format(new Date(data.data.date), "yyyy-MM-dd hh:mm");
-			commit_data.value = data.data;
-			is_loading.value = false;
+			if(commit_data) {
+				commit_data.date = format(new Date(commit_data.date), "yyyy-MM-dd hh:mm");
+				commit.value = commit_data;
+			}
 		};
 
-		return { commit_data, is_loading, fetchCommit };
+		return { commit, is_loading, fetch_failed, fetchCommit };
 	},
 	created() {
 		this.fetchCommit(this.$router.currentRoute._rawValue.params.repo, this.$router.currentRoute._rawValue.params.commit);
diff --git a/packages/client/src/views/RepositoryLog.vue b/packages/client/src/views/RepositoryLog.vue
index 542831f..3b6248c 100644
--- a/packages/client/src/views/RepositoryLog.vue
+++ b/packages/client/src/views/RepositoryLog.vue
@@ -1,11 +1,9 @@
 <template>
-	<div class="row mx-0 vld-parent">
-		<Loading
-			:active="is_loading" :height="24"
-			:width="24" color="#ffffff"
-			:opacity="0" />
+	<div class="row mx-0 vld-parent flex-fill">
 		<div class="col ms-4 ps-4 ps-sm-5 mt-3">
-			<table id="log" class="table table-dark fs-5">
+			<table
+				id="log" class="table table-dark fs-5"
+				v-if="commits">
 				<thead>
 					<tr>
 						<th class="text-secondary">
@@ -39,19 +37,27 @@
 					</tr>
 				</tbody>
 			</table>
+			<BaseErrorMessage :fetch-failed="fetch_failed" />
+			<Loading
+				:active="is_loading" :height="24"
+				:width="24" color="#ffffff"
+				:opacity="0" :is-full-page="false" />
 		</div>
 	</div>
 </template>
 
 <script>
 import Loading from "vue-loading-overlay";
+import BaseErrorMessage from "@/components/BaseErrorMessage";
 import { ref } from "vue";
 import { format } from "date-fns";
+import fetchData from "@/util/fetch";
 
 export default {
 	name: "RepositoryLog",
 	components: {
-		Loading
+		Loading,
+		BaseErrorMessage
 	},
 	data() {
 		return {
@@ -59,16 +65,18 @@ export default {
 		};
 	},
 	setup() {
-		const commits = ref({});
+		const commits = ref(null);
 		const is_loading = ref(true);
+		const fetch_failed = ref(null);
 
 		const fetchLog = async(repository) => {
-			const log_data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/repos/${repository}/log`)).json();
-			commits.value = log_data.data;
-			is_loading.value = false;
+			const log_data = await fetchData(`repos/${repository}/log`, fetch_failed, is_loading, "log");
+			if(log_data) {
+				commits.value = log_data;
+			}
 		};
 
-		return { commits, is_loading, fetchLog };
+		return { commits, is_loading, fetch_failed, fetchLog };
 	},
 	created() {
 		this.fetchLog(this.$router.currentRoute._rawValue.params.repo);
diff --git a/packages/client/src/views/RepositoryTree.vue b/packages/client/src/views/RepositoryTree.vue
index 1b61c42..898b12c 100644
--- a/packages/client/src/views/RepositoryTree.vue
+++ b/packages/client/src/views/RepositoryTree.vue
@@ -11,11 +11,11 @@
 				}))" :active-item="(pathArr.length === 0) ? $router.currentRoute._rawValue.params.repo : pathArr[pathArr.length - 1]" />
 			<RepositoryTreeTree
 				:repository="$router.currentRoute._rawValue.params.repo" :path="path"
-				:tree="tree" v-if="type === 'tree'"
-				:is-loading="is_loading" />
+				:tree="tree" v-if="tree" />
 			<RepositoryTreeBlob
 				:repository="$router.currentRoute._rawValue.params.repo" :path="path"
-				:content="blob_content" v-else />
+				:content="blob_content" v-if="blob_content" />
+			<BaseErrorMessage :fetch-failed="fetch_failed" />
 			<Loading
 				:active="is_loading" :height="24"
 				:width="24" color="#ffffff"
@@ -28,8 +28,10 @@
 import BaseBreadcrumb from "@/components/BaseBreadcrumb";
 import RepositoryTreeBlob from "@/components/RepositoryTreeBlob";
 import RepositoryTreeTree from "@/components/RepositoryTreeTree";
+import BaseErrorMessage from "@/components/BaseErrorMessage";
 import Loading from "vue-loading-overlay";
 import { ref } from "vue";
+import fetchData from "@/util/fetch";
 
 export default {
 	name: "RepositoryTree",
@@ -37,7 +39,8 @@ export default {
 		BaseBreadcrumb,
 		RepositoryTreeBlob,
 		RepositoryTreeTree,
-		Loading
+		Loading,
+		BaseErrorMessage
 	},
 	props: {
 		pathArr: {
@@ -48,45 +51,45 @@ export default {
 	watch: {
 		pathArr() {
 			this.is_loading = true;
-			this.tree = {};
 			this.fetchTree(this.$router.currentRoute._rawValue.params.repo);
 		}
 	},
 	setup(props) {
-		const type = ref("");
-		const tree = ref({});
-		const blob_content = ref("");
+		const tree = ref(null);
+		const blob_content = ref(null);
 		const is_loading = ref(true);
+		const fetch_failed = ref(null);
 		const path = ref("");
 
 		const fetchTree = async(repository) => {
+			blob_content.value = null;
+			tree.value = null;
+
 			path.value = props.pathArr ? props.pathArr.join("/") : undefined;
-			const data = await (await fetch(`${window.location.protocol}//${window.location.host}/api/v1/repos/${repository}/tree${path.value ? "?path=" + path.value : ""}`)).json();
-			console.log(path.value);
-			type.value = data.data.type;
 
-			if(data.data.type === "tree") {
-				const tree_data = data.data.tree;
+			const tree_data = await fetchData(`repos/${repository}/tree${path.value ? "?path=" + path.value : ""}`, fetch_failed, is_loading, "tree");
 
-				let tree_trees = Object.entries(tree_data).filter((entry) => entry[1].type === "tree");
-				tree_trees = tree_trees.sort((a, b) => a[0].localeCompare(b[0]));
+			if(tree_data) {
+				if(tree_data.type === "tree") {
+					let tree_trees = Object.entries(tree_data.tree).filter((entry) => entry[1].type === "tree");
+					tree_trees = tree_trees.sort((a, b) => a[0].localeCompare(b[0]));
 
-				let tree_blobs = Object.entries(tree_data).filter((entry) => entry[1].type === "blob");
-				tree_blobs = tree_blobs.sort((a, b) => a[0].localeCompare(b[0]));
+					let tree_blobs = Object.entries(tree_data.tree).filter((entry) => entry[1].type === "blob");
+					tree_blobs = tree_blobs.sort((a, b) => a[0].localeCompare(b[0]));
 
-				tree.value = Object.fromEntries(tree_trees.concat(tree_blobs));
-			} else {
-				blob_content.value = data.data.content;
+					tree.value = Object.fromEntries(tree_trees.concat(tree_blobs));
+					console.log(tree.value);
+				} else {
+					blob_content.value = tree_data.content;
+				}
 			}
-
-			is_loading.value = false;
 		};
 
 		return {
-			type,
 			tree,
 			blob_content,
 			is_loading,
+			fetch_failed,
 			path,
 			fetchTree
 		};
-- 
cgit v1.2.3-18-g5258