Browse Source

feat: Started adding frontend UI and logic for long process handling

Owen Diffey 2 years ago
parent
commit
6e1d75e045

+ 4 - 0
frontend/src/App.vue

@@ -10,6 +10,7 @@
 		</div>
 		<falling-snow v-if="christmas" />
 		<modal-manager />
+		<long-jobs />
 	</div>
 </template>
 
@@ -27,6 +28,9 @@ export default {
 		ModalManager: defineAsyncComponent(() =>
 			import("@/components/ModalManager.vue")
 		),
+		LongJobs: defineAsyncComponent(() =>
+			import("@/components/LongJobs.vue")
+		),
 		Banned: defineAsyncComponent(() => import("@/pages/Banned.vue")),
 		FallingSnow: defineAsyncComponent(() =>
 			import("@/components/FallingSnow.vue")

+ 35 - 14
frontend/src/components/FloatingBox.vue

@@ -6,7 +6,7 @@
 			column
 		}"
 		:id="id"
-		v-if="shown"
+		v-if="persist || shown"
 		:style="{
 			width: width + 'px',
 			height: height + 'px',
@@ -16,7 +16,12 @@
 		@mousedown.left="onResizeBox"
 	>
 		<div class="box-header item-draggable" @mousedown.left="onDragBox">
-			<span class="delete material-icons" @click="toggleBox()"
+			<span class="drag material-icons">drag_indicator</span>
+			<span v-if="title" class="box-title">{{ title }}</span>
+			<span
+				v-if="!persist"
+				class="delete material-icons"
+				@click="toggleBox()"
 				>highlight_off</span
 			>
 		</div>
@@ -30,7 +35,9 @@
 export default {
 	props: {
 		id: { type: String, default: null },
-		column: { type: Boolean, default: true }
+		column: { type: Boolean, default: true },
+		title: { type: String, default: null },
+		persist: { type: Boolean, default: false }
 	},
 	data() {
 		return {
@@ -160,23 +167,37 @@ export default {
 	padding: 0;
 
 	.box-header {
-		z-index: 100000001;
-		background-color: var(--primary-color);
-		display: block;
-		height: 24px;
+		display: flex;
+		height: 30px;
 		width: 100%;
+		background-color: var(--primary-color);
+		color: var(--white);
+		z-index: 100000001;
+
+		.box-title {
+			font-size: 16px;
+			font-weight: 600;
+			line-height: 30px;
+			margin-right: 5px;
+		}
 
-		.delete.material-icons {
-			position: absolute;
-			top: 2px;
-			right: 2px;
+		.material-icons {
 			font-size: 20px;
-			color: var(--white);
-			cursor: pointer;
+			line-height: 30px;
+
 			&:hover,
 			&:focus {
 				filter: brightness(90%);
 			}
+
+			&.drag {
+				margin: 0 5px;
+			}
+
+			&.delete {
+				margin: 0 5px 0 auto;
+				cursor: pointer;
+			}
 		}
 	}
 
@@ -184,7 +205,7 @@ export default {
 		display: flex;
 		flex-wrap: wrap;
 		padding: 10px;
-		height: calc(100% - 24px); /* 24px is the height of the box-header */
+		height: calc(100% - 30px); /* 30px is the height of the box-header */
 		overflow: auto;
 
 		span {

+ 162 - 0
frontend/src/components/LongJobs.vue

@@ -0,0 +1,162 @@
+<template>
+	<floating-box
+		v-if="activeJobs.length > 0"
+		title="Jobs"
+		id="longJobs"
+		ref="longJobs"
+		:persist="true"
+	>
+		<template #body>
+			<div class="active-jobs">
+				<div
+					v-for="job in activeJobs"
+					:key="`activeJob-${job.id}`"
+					class="active-job"
+				>
+					<div class="name" :title="job.name">{{ job.name }}</div>
+					<div class="actions">
+						<i
+							v-if="
+								job.status === 'started' ||
+								job.status === 'update'
+							"
+							class="material-icons"
+							content="In Progress"
+							v-tippy="{ theme: 'info' }"
+						>
+							pending
+						</i>
+						<i
+							v-else-if="job.status === 'success'"
+							class="material-icons success"
+							content="Complete"
+							v-tippy="{ theme: 'info' }"
+						>
+							check_circle
+						</i>
+						<i
+							v-else
+							class="material-icons error"
+							content="Failed"
+							v-tippy="{ theme: 'info' }"
+						>
+							error
+						</i>
+						<i class="material-icons" content="View Log" v-tippy>
+							description
+						</i>
+						<i
+							class="material-icons clear"
+							:class="{ disabled: job.status !== 'success' }"
+							content="Clear"
+							v-tippy
+							@click="remove(job)"
+						>
+							remove_circle
+						</i>
+					</div>
+				</div>
+			</div>
+		</template>
+	</floating-box>
+</template>
+
+<script>
+import { mapState, mapActions } from "vuex";
+
+import FloatingBox from "@/components/FloatingBox.vue";
+
+export default {
+	components: {
+		FloatingBox
+	},
+	data() {
+		return {
+			minimise: true
+		};
+	},
+	computed: {
+		...mapState("longJobs", {
+			activeJobs: state => state.activeJobs
+		})
+	},
+	methods: {
+		remove(job) {
+			if (job.status === "success" || job.status === "error")
+				this.removeJob(job.id);
+		},
+		...mapActions("longJobs", ["removeJob"])
+	}
+};
+</script>
+
+<style lang="less" scoped>
+.night-mode #longJobs {
+	.active-jobs {
+		.active-job {
+			background-color: var(--dark-grey);
+			border: 0;
+		}
+	}
+}
+
+#longJobs {
+	min-width: 200px !important;
+	max-width: 400px !important;
+	min-height: 200px !important;
+	z-index: 5000 !important;
+
+	.active-jobs {
+		.active-job {
+			display: flex;
+			padding: 5px;
+			margin: 5px 0;
+			border: 1px solid var(--light-grey-3);
+			border-radius: @border-radius;
+
+			&:first-child {
+				margin-top: 0;
+			}
+
+			&:last-child {
+				margin-bottom: 0;
+			}
+
+			.name {
+				line-height: 24px;
+				font-weight: 600;
+				text-transform: capitalize;
+				text-overflow: ellipsis;
+				white-space: nowrap;
+				overflow: hidden;
+				margin-right: auto;
+			}
+
+			.actions {
+				display: flex;
+
+				.material-icons {
+					font-size: 20px;
+					color: var(--primary-color);
+					margin: auto 0 auto 5px;
+					cursor: pointer;
+
+					&.success {
+						color: var(--green);
+					}
+
+					&.error,
+					&.clear {
+						color: var(--red);
+					}
+
+					&.disabled {
+						color: var(--light-grey-3);
+						cursor: not-allowed;
+					}
+				}
+			}
+		}
+	}
+}
+</style>

+ 12 - 3
frontend/src/components/modals/BulkActions.vue

@@ -1,6 +1,6 @@
 <template>
 	<div>
-		<modal title="Bulk Actions" class="bulk-actions-modal">
+		<modal title="Bulk Actions" class="bulk-actions-modal" size="slim">
 			<template #body>
 				<label class="label">Method</label>
 				<div class="control is-expanded select">
@@ -132,6 +132,7 @@ export default {
 		},
 		applyChanges() {
 			let toast;
+			const id = Date.now();
 
 			this.socket.dispatch(
 				this.type.action,
@@ -142,7 +143,7 @@ export default {
 					cb: () => {
 						// new Toast(res.message);
 						// if (res.status === "success")
-						// 	this.closeModal("bulkActions");
+						// 	this.closeCurrentModal();
 					},
 					onProgress: res => {
 						if (!toast) {
@@ -162,11 +163,19 @@ export default {
 								toast.destroy();
 							}, 4000);
 						}
+
+						if (res.status === "started") this.closeCurrentModal();
+						this.setJob({
+							id,
+							name: `Bulk ${this.method} ${this.type.name}`,
+							...res
+						});
 					}
 				}
 			);
 		},
-		...mapActions("modalVisibility", ["closeModal"])
+		...mapActions("modalVisibility", ["closeCurrentModal"]),
+		...mapActions("longJobs", ["setJob"])
 	}
 };
 </script>

+ 1 - 1
frontend/src/pages/Admin/YouTube/index.vue

@@ -365,7 +365,7 @@ export default {
 			column-gap: 10px;
 
 			.card.quota {
-				background-color: var(--light-grey-2) !important;
+				background-color: var(--light-grey) !important;
 				padding: 10px !important;
 				flex-basis: 33.33%;
 

+ 3 - 1
frontend/src/store/index.js

@@ -8,6 +8,7 @@ import settings from "./modules/settings";
 import modalVisibility from "./modules/modalVisibility";
 import station from "./modules/station";
 import admin from "./modules/admin";
+import longJobs from "./modules/longJobs";
 
 const emptyModule = {
 	namespaced: true
@@ -41,7 +42,8 @@ export default createStore({
 				bulkActions: emptyModule,
 				viewYoutubeVideo: emptyModule
 			}
-		}
+		},
+		longJobs
 	},
 	strict: false
 });

+ 56 - 0
frontend/src/store/modules/longJobs.js

@@ -0,0 +1,56 @@
+/* eslint no-param-reassign: 0 */
+
+const state = {
+	activeJobs: [
+		{
+			id: 1,
+			name: "test",
+			status: "success",
+			log: [{ status: "success", message: "test" }]
+		}
+	]
+};
+
+const getters = {};
+
+const actions = {
+	setJob: ({ commit }, job) => commit("setJob", job),
+	removeJob: ({ commit }, job) => commit("removeJob", job)
+};
+
+const mutations = {
+	setJob(state, { id, name, status, message }) {
+		if (status === "started")
+			state.activeJobs.push({
+				id,
+				name,
+				status,
+				log: [{ status, message }]
+			});
+		else
+			state.activeJobs.forEach((activeJob, index) => {
+				if (activeJob.id === id) {
+					state.activeJobs[index] = {
+						...state.activeJobs[index],
+						status
+					};
+					state.activeJobs[index].log.push({ status, message });
+				}
+			});
+	},
+	removeJob(state, jobId) {
+		state.activeJobs.forEach((activeJob, index) => {
+			if (activeJob.id === jobId) {
+				state.activeJobs.splice(index, 1);
+			}
+		});
+	}
+};
+
+export default {
+	namespaced: true,
+	state,
+	getters,
+	actions,
+	mutations
+};