chg: [front] Reorganised project
This commit is contained in:
parent
1a2ff4c3c9
commit
2cdb05276f
6 changed files with 291 additions and 268 deletions
|
@ -1,7 +1,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import TheDahboard from './components/TheDahboard.vue'
|
import TheDahboard from './TheDahboard.vue'
|
||||||
import { connectionState } from "@/socket";
|
import { socketConnected } from "@/socket";
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@ onMounted(() => {
|
||||||
document.getElementById('app').classList.add('w-5/6')
|
document.getElementById('app').classList.add('w-5/6')
|
||||||
})
|
})
|
||||||
|
|
||||||
const socketConnected = computed(() => connectionState.connected)
|
|
||||||
|
|
||||||
watch(darkMode, (newValue) => {
|
watch(darkMode, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
document.getElementsByTagName('body')[0].classList.add('dark')
|
document.getElementsByTagName('body')[0].classList.add('dark')
|
||||||
|
|
28
src/TheDahboard.vue
Normal file
28
src/TheDahboard.vue
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import TheLiveLogs from './components/TheLiveLogs.vue'
|
||||||
|
import TheScores from './components/TheScores.vue'
|
||||||
|
import { resetState, fullReload, socketConnected } from "@/socket";
|
||||||
|
|
||||||
|
|
||||||
|
watch(socketConnected, (isConnected) => {
|
||||||
|
if (isConnected) {
|
||||||
|
resetState()
|
||||||
|
fullReload()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fullReload()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1 class="text-3xl font-bold text-center text-slate-600 dark:text-slate-300">MISP Exercise Dashboard</h1>
|
||||||
|
<TheScores
|
||||||
|
></TheScores>
|
||||||
|
|
||||||
|
<TheLiveLogs
|
||||||
|
></TheLiveLogs>
|
||||||
|
</template>
|
|
@ -1,257 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
|
||||||
import { state as socketState, socket, resetState, connectionState } from "@/socket";
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
import { faCheck, faTimes, faSignal, faGraduationCap, faCloud, faCog, faUser, faCircle } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
const exercises = ref([])
|
|
||||||
|
|
||||||
const notifications = computed(() => socketState.notificationEvents)
|
|
||||||
|
|
||||||
const progresses = computed(() => socketState.progresses)
|
|
||||||
|
|
||||||
const user_count = computed(() => Object.keys(socketState.progresses).length)
|
|
||||||
|
|
||||||
function toggle_completed(completed, user_id, exec_uuid, task_uuid) {
|
|
||||||
const payload = {
|
|
||||||
user_id: user_id,
|
|
||||||
exercise_uuid: exec_uuid,
|
|
||||||
task_uuid: task_uuid,
|
|
||||||
}
|
|
||||||
const event_name = !completed ? "mark_task_completed": "mark_task_incomplete"
|
|
||||||
socket.emit(event_name, payload, () => {
|
|
||||||
socket.emit("get_progress", (all_progress) => {
|
|
||||||
socketState.progresses = all_progress
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getClassFromResponseCode(response_code) {
|
|
||||||
if (String(response_code).startsWith('2')) {
|
|
||||||
return 'text-green-500'
|
|
||||||
} else if (String(response_code).startsWith('5')) {
|
|
||||||
return 'text-red-600'
|
|
||||||
} else {
|
|
||||||
return 'text-amber-600'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fullReload() {
|
|
||||||
socket.emit("get_exercises", (all_exercises) => {
|
|
||||||
exercises.value = all_exercises
|
|
||||||
})
|
|
||||||
socket.emit("get_notifications", (all_notifications) => {
|
|
||||||
socketState.notificationEvents = all_notifications
|
|
||||||
})
|
|
||||||
socket.emit("get_progress", (all_progress) => {
|
|
||||||
socketState.progresses = all_progress
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const socketConnected = computed(() => connectionState.connected)
|
|
||||||
watch(socketConnected, (isConnected) => {
|
|
||||||
if (isConnected) {
|
|
||||||
resetState()
|
|
||||||
fullReload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fullReload()
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1 class="text-3xl font-bold text-center text-slate-600 dark:text-slate-300">MISP Exercise Dashboard</h1>
|
|
||||||
|
|
||||||
<h3 class="text-2xl mt-6 mb-2 font-bold text-blue-500 dark:text-blue-400">
|
|
||||||
<FontAwesomeIcon :icon="faGraduationCap"></FontAwesomeIcon>
|
|
||||||
Active Exercises
|
|
||||||
</h3>
|
|
||||||
<table
|
|
||||||
v-for="(exercise, exercise_index) in exercises"
|
|
||||||
:key="exercise.name"
|
|
||||||
class="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full mb-4"
|
|
||||||
>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th :colspan="2 + exercise.tasks.length" class="rounded-t-lg border-b border-slate-100 dark:border-slate-700 text-md p-3 pl-6 text-center dark:bg-blue-800 bg-blue-500 dark:text-slate-300 text-slate-100">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="dark:text-blue-200 text-slate-200 "># {{ exercise_index + 1 }}</span>
|
|
||||||
<span class="text-lg">{{ exercise.name }}</span>
|
|
||||||
<span class="">
|
|
||||||
Level: <span :class="{
|
|
||||||
'rounded-lg px-1 ml-2': true,
|
|
||||||
'dark:bg-sky-400 bg-sky-400 text-neutral-950': exercise.level == 'beginner',
|
|
||||||
'dark:bg-orange-400 bg-orange-400 text-neutral-950': exercise.level == 'advanced',
|
|
||||||
'dark:bg-red-600 bg-red-600 text-neutral-950': exercise.level == 'expert',
|
|
||||||
}">{{ exercise.level }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
<tr class="font-medium text-slate-600 dark:text-slate-200">
|
|
||||||
<th class="border-b border-slate-100 dark:border-slate-700 p-3 pl-6 text-left">User</th>
|
|
||||||
<th
|
|
||||||
v-for="(task, task_index) in exercise.tasks"
|
|
||||||
:key="task.name"
|
|
||||||
class="border-b border-slate-100 dark:border-slate-700 p-3"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="text-center font-normal text-sm dark:text-blue-200 text-slate-500">Task {{ task_index + 1 }}</span>
|
|
||||||
<i class="text-center">{{ task.name }}</i>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th class="border-b border-slate-100 dark:border-slate-700 p-3 text-left">Progress</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-if="Object.keys(progresses).length == 0">
|
|
||||||
<td
|
|
||||||
:colspan="2 + exercise.tasks.length"
|
|
||||||
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
|
|
||||||
>
|
|
||||||
<i>- No user yet -</i>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<template v-else>
|
|
||||||
<tr v-for="(progress, user_id) in progresses" :key="user_id" class="bg-slate-200 dark:bg-slate-900">
|
|
||||||
<td class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6">
|
|
||||||
<span :title="user_id">
|
|
||||||
<span class="text-lg font-bold font-mono">{{ progress.email.split('@')[0] }}</span>
|
|
||||||
<span class="text-xs font-mono">@{{ progress.email.split('@')[1] }}</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
v-for="(task, task_index) in exercise.tasks"
|
|
||||||
:key="task_index"
|
|
||||||
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-500 dark:text-slate-400 p-3"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="select-none cursor-pointer"
|
|
||||||
@click="toggle_completed(progress.exercises[exercise.uuid].tasks_completion[task.uuid], user_id, exercise.uuid, task.uuid)"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
:icon="progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? faCheck : faTimes"
|
|
||||||
:class="`text-xl ${progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? 'dark:text-green-400 text-green-600' : 'dark:text-slate-500 text-slate-400'}`"
|
|
||||||
/>
|
|
||||||
<small :class="progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? 'dark:text-green-400 text-green-600' : 'dark:text-slate-500 text-slate-400'"> (+{{ task.score }})</small>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="border-b border-slate-100 dark:border-slate-700 text-slate-500 dark:text-slate-400 p-3">
|
|
||||||
<div class="flex w-full h-2 bg-gray-200 rounded-full overflow-hidden dark:bg-neutral-600" role="progressbar" :aria-valuenow="progress.exercises[exercise.uuid].percentage" :aria-valuemin="0" aria-valuemax="100">
|
|
||||||
<div
|
|
||||||
class="flex flex-col justify-center rounded-full overflow-hidden bg-green-600 text-xs text-white text-center whitespace-nowrap transition duration-500 dark:bg-green-500 transition-width transition-slowest ease"
|
|
||||||
:style="`width: ${progress.exercises[exercise.uuid].score}%`"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h3 class="text-2xl mt-6 mb-2 font-bold text-blue-500 dark:text-blue-400">
|
|
||||||
<FontAwesomeIcon :icon="faSignal"></FontAwesomeIcon>
|
|
||||||
Live logs
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="mb-2 flex flex-wrap gap-x-3">
|
|
||||||
<span class="rounded-lg py-1 px-2 dark:bg-sky-700 bg-sky-400 text-slate-800 dark:text-slate-200">
|
|
||||||
<span class="mr-1">
|
|
||||||
<FontAwesomeIcon :icon="faUser" size="sm"></FontAwesomeIcon>
|
|
||||||
User online:
|
|
||||||
</span>
|
|
||||||
<span class="font-bold">{{ user_count }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="rounded-lg py-1 px-2 dark:bg-sky-700 bg-sky-400 text-slate-800 dark:text-slate-200">
|
|
||||||
<span class="mr-1">
|
|
||||||
<FontAwesomeIcon :icon="faSignal" size="sm"></FontAwesomeIcon>
|
|
||||||
Total Queries:
|
|
||||||
</span>
|
|
||||||
<span class="font-bold">{{ socketState.notificationCounter }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="rounded-lg py-1 px-2 dark:bg-sky-700 bg-sky-400 text-slate-800 dark:text-slate-200">
|
|
||||||
<span class="mr-1">
|
|
||||||
<FontAwesomeIcon :icon="faCog" size="sm" :mask="faCloud" transform="shrink-7 left-1"></FontAwesomeIcon>
|
|
||||||
Total API Queries:
|
|
||||||
</span>
|
|
||||||
<span class="font-bold">{{ socketState.notificationAPICounter }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table class="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full">
|
|
||||||
<thead>
|
|
||||||
<tr class="font-medium dark:text-slate-200 text-slate-600 ">
|
|
||||||
<th class="border-b border-slate-100 dark:border-slate-700 p-3 pl-6 text-left"></th>
|
|
||||||
<th class="border-b border-slate-100 dark:border-slate-700 p-3 pl-2 text-left">User</th>
|
|
||||||
<th class="border-b border-slate-100 dark:border-slate-700 p-3 text-left">Time</th>
|
|
||||||
<th class="border-b border-slate-100 dark:border-slate-700 p-3 text-left">URL</th>
|
|
||||||
<th class="border-b border-slate-100 dark:border-slate-700 p-3 text-left">Payload</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-if="Object.keys(notifications).length == 0">
|
|
||||||
<td
|
|
||||||
colspan="5"
|
|
||||||
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
|
|
||||||
>
|
|
||||||
<i>- No logs yet -</i>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<template v-else>
|
|
||||||
<tr v-for="(notification, index) in notifications" :key="index">
|
|
||||||
<td
|
|
||||||
class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-1 pl-2 w-12 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="faCircle" size="xs"
|
|
||||||
:class="getClassFromResponseCode(notification.response_code)"
|
|
||||||
></FontAwesomeIcon>
|
|
||||||
<pre class="inline ml-1">{{ notification.response_code }}</pre>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-1 pl-2"
|
|
||||||
:title="notification.user_id"
|
|
||||||
>
|
|
||||||
<span class="text-lg font-bold font-mono">{{ notification.user.split('@')[0] }}</span>
|
|
||||||
<span class="text-xs font-mono">@{{ notification.user.split('@')[1] }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-1">{{ notification.time }}</td>
|
|
||||||
<td class="border-b border-slate-100 dark:border-slate-700 text-sky-600 dark:text-sky-400 p-1">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span v-if="notification.http_method == 'POST'"
|
|
||||||
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
|
||||||
dark:bg-amber-600 dark:text-neutral-100 bg-amber-600 text-neutral-100"
|
|
||||||
>POST</span>
|
|
||||||
<span v-else-if="notification.http_method == 'DELETE'"
|
|
||||||
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
|
||||||
dark:bg-red-600 dark:text-neutral-100 bg-red-600 text-neutral-100"
|
|
||||||
>DEL</span>
|
|
||||||
<span v-else
|
|
||||||
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
|
||||||
dark:bg-blue-600 dark:text-neutral-100 bg-blue-600 text-neutral-100"
|
|
||||||
>{{ notification.http_method }}</span>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
v-if="notification.is_api_request"
|
|
||||||
class="text-slate-800 dark:text-slate-100 mr-1 inline-block"
|
|
||||||
:icon="faCog" :mask="faCloud" transform="shrink-7 left-1"
|
|
||||||
></FontAwesomeIcon>
|
|
||||||
<pre class="text-sm inline">{{ notification.url }}</pre>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-300 p-1">
|
|
||||||
<div v-if="notification.http_method == 'POST'"
|
|
||||||
class="border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-600 rounded-md"
|
|
||||||
>
|
|
||||||
<pre
|
|
||||||
class="p-1 text-xs"
|
|
||||||
>{{ JSON.stringify(notification.payload, null, 2) }}</pre>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</template>
|
|
122
src/components/TheLiveLogs.vue
Normal file
122
src/components/TheLiveLogs.vue
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { notifications, userCount, notificationCounter, notificationAPICounter } from "@/socket";
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
import { faSignal, faCloud, faCog, faUser, faCircle } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
|
||||||
|
function getClassFromResponseCode(response_code) {
|
||||||
|
if (String(response_code).startsWith('2')) {
|
||||||
|
return 'text-green-500'
|
||||||
|
} else if (String(response_code).startsWith('5')) {
|
||||||
|
return 'text-red-600'
|
||||||
|
} else {
|
||||||
|
return 'text-amber-600'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h3 class="text-2xl mt-6 mb-2 font-bold text-blue-500 dark:text-blue-400">
|
||||||
|
<FontAwesomeIcon :icon="faSignal"></FontAwesomeIcon>
|
||||||
|
Live logs
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="mb-2 flex flex-wrap gap-x-3">
|
||||||
|
<span class="rounded-lg py-1 px-2 dark:bg-sky-700 bg-sky-400 text-slate-800 dark:text-slate-200">
|
||||||
|
<span class="mr-1">
|
||||||
|
<FontAwesomeIcon :icon="faUser" size="sm"></FontAwesomeIcon>
|
||||||
|
User online:
|
||||||
|
</span>
|
||||||
|
<span class="font-bold">{{ userCount }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="rounded-lg py-1 px-2 dark:bg-sky-700 bg-sky-400 text-slate-800 dark:text-slate-200">
|
||||||
|
<span class="mr-1">
|
||||||
|
<FontAwesomeIcon :icon="faSignal" size="sm"></FontAwesomeIcon>
|
||||||
|
Total Queries:
|
||||||
|
</span>
|
||||||
|
<span class="font-bold">{{ notificationCounter }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="rounded-lg py-1 px-2 dark:bg-sky-700 bg-sky-400 text-slate-800 dark:text-slate-200">
|
||||||
|
<span class="mr-1">
|
||||||
|
<FontAwesomeIcon :icon="faCog" size="sm" :mask="faCloud" transform="shrink-7 left-1"></FontAwesomeIcon>
|
||||||
|
Total API Queries:
|
||||||
|
</span>
|
||||||
|
<span class="font-bold">{{ notificationAPICounter }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="font-medium dark:text-slate-200 text-slate-600 ">
|
||||||
|
<th class="border-b border-slate-100 dark:border-slate-700 p-3 pl-6 text-left"></th>
|
||||||
|
<th class="border-b border-slate-100 dark:border-slate-700 p-3 pl-2 text-left">User</th>
|
||||||
|
<th class="border-b border-slate-100 dark:border-slate-700 p-3 text-left">Time</th>
|
||||||
|
<th class="border-b border-slate-100 dark:border-slate-700 p-3 text-left">URL</th>
|
||||||
|
<th class="border-b border-slate-100 dark:border-slate-700 p-3 text-left">Payload</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="Object.keys(notifications).length == 0">
|
||||||
|
<td
|
||||||
|
colspan="5"
|
||||||
|
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
|
||||||
|
>
|
||||||
|
<i>- No logs yet -</i>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<template v-else>
|
||||||
|
<tr v-for="(notification, index) in notifications" :key="index">
|
||||||
|
<td
|
||||||
|
class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-1 pl-2 w-12 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faCircle" size="xs"
|
||||||
|
:class="getClassFromResponseCode(notification.response_code)"
|
||||||
|
></FontAwesomeIcon>
|
||||||
|
<pre class="inline ml-1">{{ notification.response_code }}</pre>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-1 pl-2"
|
||||||
|
:title="notification.user_id"
|
||||||
|
>
|
||||||
|
<span class="text-lg font-bold font-mono">{{ notification.user.split('@')[0] }}</span>
|
||||||
|
<span class="text-xs font-mono">@{{ notification.user.split('@')[1] }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-1">{{ notification.time }}</td>
|
||||||
|
<td class="border-b border-slate-100 dark:border-slate-700 text-sky-600 dark:text-sky-400 p-1">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span v-if="notification.http_method == 'POST'"
|
||||||
|
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
||||||
|
dark:bg-amber-600 dark:text-neutral-100 bg-amber-600 text-neutral-100"
|
||||||
|
>POST</span>
|
||||||
|
<span v-else-if="notification.http_method == 'DELETE'"
|
||||||
|
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
||||||
|
dark:bg-red-600 dark:text-neutral-100 bg-red-600 text-neutral-100"
|
||||||
|
>DEL</span>
|
||||||
|
<span v-else
|
||||||
|
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
||||||
|
dark:bg-blue-600 dark:text-neutral-100 bg-blue-600 text-neutral-100"
|
||||||
|
>{{ notification.http_method }}</span>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
v-if="notification.is_api_request"
|
||||||
|
class="text-slate-800 dark:text-slate-100 mr-1 inline-block"
|
||||||
|
:icon="faCog" :mask="faCloud" transform="shrink-7 left-1"
|
||||||
|
></FontAwesomeIcon>
|
||||||
|
<pre class="text-sm inline">{{ notification.url }}</pre>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-300 p-1">
|
||||||
|
<div v-if="notification.http_method == 'POST'"
|
||||||
|
class="border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-600 rounded-md"
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
class="p-1 text-xs"
|
||||||
|
>{{ JSON.stringify(notification.payload, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
111
src/components/TheScores.vue
Normal file
111
src/components/TheScores.vue
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { exercises, progresses } from "@/socket";
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
import { faCheck, faTimes, faGraduationCap } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
|
||||||
|
function toggle_completed(completed, user_id, exec_uuid, task_uuid) {
|
||||||
|
const payload = {
|
||||||
|
user_id: user_id,
|
||||||
|
exercise_uuid: exec_uuid,
|
||||||
|
task_uuid: task_uuid,
|
||||||
|
}
|
||||||
|
const event_name = !completed ? "mark_task_completed": "mark_task_incomplete"
|
||||||
|
socket.emit(event_name, payload, () => {
|
||||||
|
socket.emit("get_progress", (all_progress) => {
|
||||||
|
socketState.progresses = all_progress
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h3 class="text-2xl mt-6 mb-2 font-bold text-blue-500 dark:text-blue-400">
|
||||||
|
<FontAwesomeIcon :icon="faGraduationCap"></FontAwesomeIcon>
|
||||||
|
Active Exercises
|
||||||
|
</h3>
|
||||||
|
<table
|
||||||
|
v-for="(exercise, exercise_index) in exercises"
|
||||||
|
:key="exercise.name"
|
||||||
|
class="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full mb-4"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th :colspan="2 + exercise.tasks.length" class="rounded-t-lg border-b border-slate-100 dark:border-slate-700 text-md p-3 pl-6 text-center dark:bg-blue-800 bg-blue-500 dark:text-slate-300 text-slate-100">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="dark:text-blue-200 text-slate-200 "># {{ exercise_index + 1 }}</span>
|
||||||
|
<span class="text-lg">{{ exercise.name }}</span>
|
||||||
|
<span class="">
|
||||||
|
Level: <span :class="{
|
||||||
|
'rounded-lg px-1 ml-2': true,
|
||||||
|
'dark:bg-sky-400 bg-sky-400 text-neutral-950': exercise.level == 'beginner',
|
||||||
|
'dark:bg-orange-400 bg-orange-400 text-neutral-950': exercise.level == 'advanced',
|
||||||
|
'dark:bg-red-600 bg-red-600 text-neutral-950': exercise.level == 'expert',
|
||||||
|
}">{{ exercise.level }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="font-medium text-slate-600 dark:text-slate-200">
|
||||||
|
<th class="border-b border-slate-100 dark:border-slate-700 p-3 pl-6 text-left">User</th>
|
||||||
|
<th
|
||||||
|
v-for="(task, task_index) in exercise.tasks"
|
||||||
|
:key="task.name"
|
||||||
|
class="border-b border-slate-100 dark:border-slate-700 p-3"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-center font-normal text-sm dark:text-blue-200 text-slate-500">Task {{ task_index + 1 }}</span>
|
||||||
|
<i class="text-center">{{ task.name }}</i>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="border-b border-slate-100 dark:border-slate-700 p-3 text-left">Progress</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="Object.keys(progresses).length == 0">
|
||||||
|
<td
|
||||||
|
:colspan="2 + exercise.tasks.length"
|
||||||
|
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6"
|
||||||
|
>
|
||||||
|
<i>- No user yet -</i>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<template v-else>
|
||||||
|
<tr v-for="(progress, user_id) in progresses" :key="user_id" class="bg-slate-200 dark:bg-slate-900">
|
||||||
|
<td class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-3 pl-6">
|
||||||
|
<span :title="user_id">
|
||||||
|
<span class="text-lg font-bold font-mono">{{ progress.email.split('@')[0] }}</span>
|
||||||
|
<span class="text-xs font-mono">@{{ progress.email.split('@')[1] }}</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
v-for="(task, task_index) in exercise.tasks"
|
||||||
|
:key="task_index"
|
||||||
|
class="text-center border-b border-slate-100 dark:border-slate-700 text-slate-500 dark:text-slate-400 p-3"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="select-none cursor-pointer"
|
||||||
|
@click="toggle_completed(progress.exercises[exercise.uuid].tasks_completion[task.uuid], user_id, exercise.uuid, task.uuid)"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? faCheck : faTimes"
|
||||||
|
:class="`text-xl ${progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? 'dark:text-green-400 text-green-600' : 'dark:text-slate-500 text-slate-400'}`"
|
||||||
|
/>
|
||||||
|
<small :class="progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? 'dark:text-green-400 text-green-600' : 'dark:text-slate-500 text-slate-400'"> (+{{ task.score }})</small>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="border-b border-slate-100 dark:border-slate-700 text-slate-500 dark:text-slate-400 p-3">
|
||||||
|
<div class="flex w-full h-2 bg-gray-200 rounded-full overflow-hidden dark:bg-neutral-600" role="progressbar" :aria-valuenow="progress.exercises[exercise.uuid].percentage" :aria-valuemin="0" aria-valuemax="100">
|
||||||
|
<div
|
||||||
|
class="flex flex-col justify-center rounded-full overflow-hidden bg-green-600 text-xs text-white text-center whitespace-nowrap transition duration-500 dark:bg-green-500 transition-width transition-slowest ease"
|
||||||
|
:style="`width: ${progress.exercises[exercise.uuid].score}%`"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
|
@ -1,28 +1,49 @@
|
||||||
import { reactive } from "vue";
|
import { reactive, computed } from "vue";
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
|
|
||||||
|
// "undefined" means the URL will be computed from the `window.location` object
|
||||||
|
const URL = process.env.NODE_ENV === "production" ? undefined : "http://localhost:3000";
|
||||||
|
const MAX_LIVE_LOG = 30
|
||||||
|
|
||||||
const initial_state = {
|
const initial_state = {
|
||||||
notificationEvents: [],
|
notificationEvents: [],
|
||||||
notificationCounter: 0,
|
notificationCounter: 0,
|
||||||
notificationAPICounter: 0,
|
notificationAPICounter: 0,
|
||||||
|
exercises: [],
|
||||||
progresses: {},
|
progresses: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const state = reactive({ ...initial_state });
|
const state = reactive({ ...initial_state });
|
||||||
export const connectionState = reactive({
|
const connectionState = reactive({
|
||||||
connected: false
|
connected: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const exercises = computed(() => state.exercises)
|
||||||
|
export const progresses = computed(() => state.progresses)
|
||||||
|
export const notifications = computed(() => state.notificationEvents)
|
||||||
|
export const notificationCounter = computed(() => state.notificationCounter)
|
||||||
|
export const notificationAPICounter = computed(() => state.notificationAPICounter)
|
||||||
|
export const userCount = computed(() => Object.keys(state.progresses).length)
|
||||||
|
export const socketConnected = computed(() => connectionState.connected)
|
||||||
|
|
||||||
export function resetState() {
|
export function resetState() {
|
||||||
Object.assign(state, initial_state);
|
Object.assign(state, initial_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_LIVE_LOG = 30
|
export function fullReload() {
|
||||||
|
socket.emit("get_exercises", (all_exercises) => {
|
||||||
|
state.exercises = all_exercises
|
||||||
|
})
|
||||||
|
socket.emit("get_notifications", (all_notifications) => {
|
||||||
|
state.notificationEvents = all_notifications
|
||||||
|
})
|
||||||
|
socket.emit("get_progress", (all_progress) => {
|
||||||
|
state.progresses = all_progress
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// "undefined" means the URL will be computed from the `window.location` object
|
|
||||||
const URL = process.env.NODE_ENV === "production" ? undefined : "http://localhost:3000";
|
|
||||||
|
|
||||||
export const socket = io(URL, {
|
const socket = io(URL, {
|
||||||
autoConnect: true
|
autoConnect: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue