Compare commits
4 commits
main
...
scenario-d
Author | SHA1 | Date | |
---|---|---|---|
|
586a5585be | ||
|
76ce0607f7 | ||
|
0370ad08a7 | ||
|
b040f85d59 |
32 changed files with 1874 additions and 1009 deletions
8
.vite/deps/_metadata.json
Normal file
8
.vite/deps/_metadata.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"hash": "d30f2833",
|
||||||
|
"configHash": "e49d25ea",
|
||||||
|
"lockfileHash": "e3b0c442",
|
||||||
|
"browserHash": "f22e7dd7",
|
||||||
|
"optimized": {},
|
||||||
|
"chunks": {}
|
||||||
|
}
|
3
.vite/deps/package.json
Normal file
3
.vite/deps/package.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
45
package-lock.json
generated
45
package-lock.json
generated
|
@ -23,7 +23,6 @@
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"daisyui": "^4.12.10",
|
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
|
@ -1444,16 +1443,6 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/css-selector-tokenizer": {
|
|
||||||
"version": "0.8.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz",
|
|
||||||
"integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"cssesc": "^3.0.0",
|
|
||||||
"fastparse": "^1.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
|
@ -1471,34 +1460,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
},
|
},
|
||||||
"node_modules/culori": {
|
|
||||||
"version": "3.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
|
|
||||||
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/daisyui": {
|
|
||||||
"version": "4.12.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.10.tgz",
|
|
||||||
"integrity": "sha512-jp1RAuzbHhGdXmn957Z2XsTZStXGHzFfF0FgIOZj3Wv9sH7OZgLfXTRZNfKVYxltGUOBsG1kbWAdF5SrqjebvA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"css-selector-tokenizer": "^0.8",
|
|
||||||
"culori": "^3",
|
|
||||||
"picocolors": "^1",
|
|
||||||
"postcss-js": "^4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.9.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/daisyui"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
||||||
|
@ -1919,12 +1880,6 @@
|
||||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fastparse": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"daisyui": "^4.12.10",
|
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
|
|
|
@ -4,16 +4,15 @@ import TheThemeButton from './components/TheThemeButton.vue'
|
||||||
import TheAdminPanel from './components/TheAdminPanel.vue'
|
import TheAdminPanel from './components/TheAdminPanel.vue'
|
||||||
import TheSocketConnectionState from './components/TheSocketConnectionState.vue'
|
import TheSocketConnectionState from './components/TheSocketConnectionState.vue'
|
||||||
import TheDahboard from './TheDahboard.vue'
|
import TheDahboard from './TheDahboard.vue'
|
||||||
import { socketConnected } from "@/socket";
|
import Toaster from '@/components/elements/Toaster.vue'
|
||||||
import { darkModeEnabled } from "@/settings.js"
|
import { socketConnected } from '@/socket'
|
||||||
|
import { darkModeEnabled } from '@/settings.js'
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (darkModeEnabled.value) {
|
if (darkModeEnabled.value) {
|
||||||
document.getElementsByTagName('body')[0].classList.add('dark')
|
document.getElementsByTagName('body')[0].classList.add('dark')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -34,6 +33,7 @@ onMounted(() => {
|
||||||
<div class="mt-12">
|
<div class="mt-12">
|
||||||
<TheDahboard></TheDahboard>
|
<TheDahboard></TheDahboard>
|
||||||
</div>
|
</div>
|
||||||
|
<Toaster></Toaster>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -63,5 +63,4 @@ body {
|
||||||
/* cyan-400 */
|
/* cyan-400 */
|
||||||
/* filter: invert(71%) sepia(97%) saturate(1333%) hue-rotate(147deg) brightness(95%) contrast(96%); */
|
/* filter: invert(71%) sepia(97%) saturate(1333%) hue-rotate(147deg) brightness(95%) contrast(96%); */
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
|
@ -2,9 +2,8 @@
|
||||||
import { onMounted, watch } from 'vue'
|
import { onMounted, watch } from 'vue'
|
||||||
import TheLiveLogs from './components/TheLiveLogs.vue'
|
import TheLiveLogs from './components/TheLiveLogs.vue'
|
||||||
import TheScores from './components/TheScores.vue'
|
import TheScores from './components/TheScores.vue'
|
||||||
import { resetState, fullReload, socketConnected } from "@/socket";
|
import { resetState, fullReload, socketConnected } from '@/socket'
|
||||||
import { fullscreenModeOn } from "@/settings.js"
|
import { fullscreenModeOn } from '@/settings.js'
|
||||||
|
|
||||||
|
|
||||||
watch(socketConnected, (isConnected) => {
|
watch(socketConnected, (isConnected) => {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
|
@ -16,7 +15,6 @@ watch(socketConnected, (isConnected) => {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fullReload()
|
fullReload()
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
@tailwind base;
|
@import 'tailwindcss/base';
|
||||||
@tailwind components;
|
@import 'tailwindcss/components';
|
||||||
@tailwind utilities;
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
|
@import './styled-components/toggle.css';
|
||||||
|
@import './styled-components/button.css';
|
||||||
|
@import './styled-components/transitions.css';
|
||||||
|
|
82
src/assets/styled-components/button.css
Normal file
82
src/assets/styled-components/button.css
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
.btn {
|
||||||
|
@apply px-2 py-1 font-semibold rounded border text-nowrap select-none;
|
||||||
|
@apply transition-all duration-75;
|
||||||
|
@apply border-slate-300;
|
||||||
|
|
||||||
|
&.btn-sm {
|
||||||
|
@apply px-1 py-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-xs {
|
||||||
|
@apply px-0.5 py-0;
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-lg {
|
||||||
|
@apply px-3 py-1;
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn:not(:disabled) {
|
||||||
|
@apply hover:bg-slate-200 hover:border-slate-300;
|
||||||
|
@apply active:scale-90;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn:disabled {
|
||||||
|
@apply cursor-not-allowed opacity-60;
|
||||||
|
}
|
||||||
|
&.btn-primary:not(:disabled) {
|
||||||
|
@apply border-none;
|
||||||
|
@apply bg-blue-600 text-white hover:bg-blue-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-info:not(:disabled) {
|
||||||
|
@apply border-none;
|
||||||
|
@apply bg-blue-500 text-white hover:bg-blue-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-danger:not(:disabled) {
|
||||||
|
@apply border-none;
|
||||||
|
@apply bg-red-600 text-white hover:bg-red-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-success:not(:disabled) {
|
||||||
|
@apply border-none;
|
||||||
|
@apply bg-green-600 text-white hover:bg-green-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-warning:not(:disabled) {
|
||||||
|
@apply border-none;
|
||||||
|
@apply bg-amber-600 text-white hover:bg-amber-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-outline-primary:not(:disabled) {
|
||||||
|
@apply hover:bg-blue-600 hover:border-blue-700 hover:text-white;
|
||||||
|
@apply border-blue-700 hover:border-blue-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-outline-info:not(:disabled) {
|
||||||
|
@apply hover:bg-blue-500 hover:border-blue-700 hover:text-white;
|
||||||
|
@apply border-blue-700 hover:border-blue-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-outline-danger:not(:disabled) {
|
||||||
|
@apply hover:bg-red-500 hover:border-red-700 hover:text-white;
|
||||||
|
@apply border-red-700 hover:border-red-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-outline-success:not(:disabled) {
|
||||||
|
@apply hover:bg-green-500 hover:border-green-700 hover:text-white;
|
||||||
|
@apply border-green-700 hover:border-green-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-outline-warning:not(:disabled) {
|
||||||
|
@apply hover:bg-amber-500 hover:border-amber-700 hover:text-white;
|
||||||
|
@apply border-amber-700 hover:border-amber-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-link:not(:disabled) {
|
||||||
|
@apply border-none;
|
||||||
|
@apply hover:bg-transparent hover:border-transparent hover:text-inherit;
|
||||||
|
}
|
||||||
|
}
|
90
src/assets/styled-components/toggle.css
Normal file
90
src/assets/styled-components/toggle.css
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
.toggle:where(.dark, .dark *) {
|
||||||
|
--tglbg: theme(colors.slate.800) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
--tglbg: theme(colors.slate.200);
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--handleoffset: 1.5rem;
|
||||||
|
--handleoffsetcalculator: calc(var(--handleoffset) * -1);
|
||||||
|
--togglehandleborder: 0 0;
|
||||||
|
@apply h-6 w-12 rounded-3xl cursor-pointer appearance-none border border-current bg-current;
|
||||||
|
transition:
|
||||||
|
background,
|
||||||
|
box-shadow var(--animation-input, 0.2s) ease-out;
|
||||||
|
box-shadow:
|
||||||
|
var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset,
|
||||||
|
0 0 0 2px var(--tglbg) inset,
|
||||||
|
var(--togglehandleborder);
|
||||||
|
@apply text-slate-500;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
@apply outline outline-2 outline-offset-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-current;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked,
|
||||||
|
&[aria-checked='true'] {
|
||||||
|
background-image: none;
|
||||||
|
--handleoffsetcalculator: var(--handleoffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:indeterminate {
|
||||||
|
box-shadow:
|
||||||
|
calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset,
|
||||||
|
calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset,
|
||||||
|
0 0 0 2px var(--tglbg) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
@apply cursor-not-allowed bg-transparent opacity-30;
|
||||||
|
--togglehandleborder: 0 0 0 3px #000 inset, var(--handleoffsetcalculator) 0 0 3px #000 inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggle-success {
|
||||||
|
&:focus-visible {
|
||||||
|
@apply outline-green-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked,
|
||||||
|
&[aria-checked='true'] {
|
||||||
|
@apply border-green-500 bg-green-400 text-slate-900 border-opacity-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggle-warning {
|
||||||
|
&:focus-visible {
|
||||||
|
@apply outline-amber-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked,
|
||||||
|
&[aria-checked='true'] {
|
||||||
|
@apply border-amber-500 bg-amber-400 text-slate-900 border-opacity-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggle-info {
|
||||||
|
&:focus-visible {
|
||||||
|
@apply outline-blue-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked,
|
||||||
|
&[aria-checked='true'] {
|
||||||
|
@apply border-blue-500 bg-blue-400 text-slate-900 border-opacity-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggle-danger {
|
||||||
|
&:focus-visible {
|
||||||
|
@apply outline-red-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked,
|
||||||
|
&[aria-checked='true'] {
|
||||||
|
@apply border-red-500 bg-red-400 text-slate-900 border-opacity-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/assets/styled-components/transitions.css
Normal file
27
src/assets/styled-components/transitions.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.slide-fade-enter-active {
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from,
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-reverse-enter-active {
|
||||||
|
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-reverse-leave-active {
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-reverse-enter-from,
|
||||||
|
.slide-fade-reverse-leave-to {
|
||||||
|
transform: translateX(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, computed } from "vue"
|
import { ref, watch, computed } from 'vue'
|
||||||
import { userActivity, userActivityConfig } from "@/socket";
|
import { userActivity, userActivityConfig } from '@/socket'
|
||||||
import { darkModeEnabled } from "@/settings.js"
|
import { darkModeEnabled } from '@/settings.js'
|
||||||
|
|
||||||
const props = defineProps(['user_id', 'compact_view', 'ultra_compact_view'])
|
const props = defineProps(['user_id', 'compact_view', 'ultra_compact_view'])
|
||||||
|
|
||||||
|
@ -16,40 +16,28 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const activitySeries = computed(() => {
|
const activitySeries = computed(() => {
|
||||||
const data = userActivity.value[props.user_id] === undefined ? chartInitSeries.value : userActivity.value[props.user_id]
|
const data =
|
||||||
|
userActivity.value[props.user_id] === undefined
|
||||||
|
? chartInitSeries.value
|
||||||
|
: userActivity.value[props.user_id]
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
|
||||||
const colorRanges = [0, 1, 2, 3, 4, 5, 1000]
|
const colorRanges = [0, 1, 2, 3, 4, 5, 1000]
|
||||||
const palleteColor = 'blue'
|
const palleteColor = 'blue'
|
||||||
const colorPalleteIndexDark = [
|
const colorPalleteIndexDark = ['900', '700', '600', '500', '400', '300', '200']
|
||||||
'900',
|
const colorPalleteIndexLight = ['50', '100', '300', '400', '500', '600', '700']
|
||||||
'700',
|
|
||||||
'600',
|
|
||||||
'500',
|
|
||||||
'400',
|
|
||||||
'300',
|
|
||||||
'200',
|
|
||||||
]
|
|
||||||
const colorPalleteIndexLight = [
|
|
||||||
'50',
|
|
||||||
'100',
|
|
||||||
'300',
|
|
||||||
'400',
|
|
||||||
'500',
|
|
||||||
'600',
|
|
||||||
'700',
|
|
||||||
]
|
|
||||||
|
|
||||||
function getPalleteIndexFromValue(value) {
|
function getPalleteIndexFromValue(value) {
|
||||||
for (let palleteIndex = 0; palleteIndex < colorRanges.length; palleteIndex++) {
|
for (let palleteIndex = 0; palleteIndex < colorRanges.length; palleteIndex++) {
|
||||||
const colorRangeValue = colorRanges[palleteIndex];
|
const colorRangeValue = colorRanges[palleteIndex]
|
||||||
if (value <= colorRangeValue) {
|
if (value <= colorRangeValue) {
|
||||||
return darkModeEnabled.value ? colorPalleteIndexDark[palleteIndex] : colorPalleteIndexLight[palleteIndex]
|
return darkModeEnabled.value
|
||||||
|
? colorPalleteIndexDark[palleteIndex]
|
||||||
|
: colorPalleteIndexLight[palleteIndex]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -60,7 +48,11 @@
|
||||||
<span
|
<span
|
||||||
v-for="(value, i) in chartSeries"
|
v-for="(value, i) in chartSeries"
|
||||||
:key="i"
|
:key="i"
|
||||||
:class="[`inline-block rounded-[1px] mr-px`, props.compact_view ? 'h-1.5' : 'h-3', `bg-${palleteColor}-${getPalleteIndexFromValue(value)}`]"
|
:class="[
|
||||||
|
`inline-block rounded-[1px] mr-px`,
|
||||||
|
props.compact_view ? 'h-1.5' : 'h-3',
|
||||||
|
`bg-${palleteColor}-${getPalleteIndexFromValue(value)}`
|
||||||
|
]"
|
||||||
:style="`width: ${(((props.ultra_compact_view ? 120 : 240) - chartSeries.length) / chartSeries.length).toFixed(1)}px`"
|
:style="`width: ${(((props.ultra_compact_view ? 120 : 240) - chartSeries.length) / chartSeries.length).toFixed(1)}px`"
|
||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,8 +1,28 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { exercises, selected_exercises, diagnostic, fullReload, resetAllExerciseProgress, resetAll, resetLiveLogs, changeExerciseSelection, debouncedGetDiangostic, remediateSetting } from "@/socket";
|
import {
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
exercises,
|
||||||
import { faScrewdriverWrench, faTrash, faSuitcaseMedical, faGraduationCap, faBan, faRotate, faHammer, faCheck } from '@fortawesome/free-solid-svg-icons'
|
selected_exercises,
|
||||||
|
diagnostic,
|
||||||
|
fullReload,
|
||||||
|
resetAllExerciseProgress,
|
||||||
|
resetAll,
|
||||||
|
resetLiveLogs,
|
||||||
|
changeExerciseSelection,
|
||||||
|
debouncedGetDiangostic,
|
||||||
|
remediateSetting
|
||||||
|
} from '@/socket'
|
||||||
|
import {
|
||||||
|
faScrewdriverWrench,
|
||||||
|
faTrash,
|
||||||
|
faSuitcaseMedical,
|
||||||
|
faGraduationCap,
|
||||||
|
faBan,
|
||||||
|
faRotate,
|
||||||
|
faHammer,
|
||||||
|
faCheck
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { toast } from '@/utils.js'
|
||||||
|
|
||||||
const admin_modal = ref(null)
|
const admin_modal = ref(null)
|
||||||
const clickedButtons = ref([])
|
const clickedButtons = ref([])
|
||||||
|
@ -13,68 +33,55 @@
|
||||||
const ZMQMessageCount = computed(() => diagnostic.value.zmq_message_count)
|
const ZMQMessageCount = computed(() => diagnostic.value.zmq_message_count)
|
||||||
|
|
||||||
function changeSelectionState(state_enabled, exec_uuid) {
|
function changeSelectionState(state_enabled, exec_uuid) {
|
||||||
changeExerciseSelection(exec_uuid, state_enabled);
|
changeExerciseSelection(exec_uuid, state_enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
function settingHandler(setting) {
|
function settingHandler(setting) {
|
||||||
remediateSetting(setting)
|
remediateSetting(setting)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showModal = ref(false)
|
||||||
function showTheModal() {
|
function showTheModal() {
|
||||||
admin_modal.value.showModal()
|
showModal.value = true
|
||||||
clickedButtons.value = []
|
clickedButtons.value = []
|
||||||
debouncedGetDiangostic()
|
debouncedGetDiangostic()
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button @click="showTheModal()" class="btn btn-primary">
|
||||||
@click="showTheModal()"
|
|
||||||
class="px-2 py-1 rounded-md focus-outline font-semibold bg-blue-600 text-slate-200 hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="faScrewdriverWrench" class="mr-1"></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faScrewdriverWrench" class="mr-1"></FontAwesomeIcon>
|
||||||
Admin panel
|
Admin panel
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<dialog ref="admin_modal" class="modal">
|
<Modal :showModal="showModal" @modal-close="showModal = false">
|
||||||
<div class="modal-box w-11/12 max-w-6xl top-24 absolute bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200">
|
<template #header>
|
||||||
<h2 class="text-2xl font-bold">
|
<h2 class="text-2xl font-bold">
|
||||||
<FontAwesomeIcon :icon="faScrewdriverWrench" class=""></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faScrewdriverWrench" class=""></FontAwesomeIcon>
|
||||||
Admin panel
|
Admin panel
|
||||||
</h2>
|
</h2>
|
||||||
<div class="modal-action">
|
</template>
|
||||||
<form method="dialog">
|
<template #body>
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
<div class="dark:text-slate-700 text-slate-700">
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<div class="flex mb-5 gap-2">
|
<div class="flex mb-5 gap-2">
|
||||||
<button
|
<button @click="fullReload()" class="h-10 min-h-10 font-semibold btn-info btn gap-1">
|
||||||
@click="fullReload()"
|
|
||||||
class="h-10 min-h-10 px-2 py-1 font-semibold bg-blue-600 text-slate-200 hover:bg-blue-700 btn btn-sm gap-1"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="faRotate" size="lg" fixed-width></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faRotate" size="lg" fixed-width></FontAwesomeIcon>
|
||||||
Full refresh
|
Full refresh
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="resetAllExerciseProgress()"
|
@click="resetAllExerciseProgress()"
|
||||||
class="h-10 min-h-10 px-2 py-1 font-semibold bg-red-600 text-slate-200 hover:bg-red-700 btn btn-sm gap-1"
|
class="h-10 min-h-10 font-semibold btn-danger btn gap-1"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon :icon="faTrash" size="lg" fixed-width></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faTrash" size="lg" fixed-width></FontAwesomeIcon>
|
||||||
Reset All Exercises
|
Reset All Exercises
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button @click="resetAll()" class="h-10 min-h-10 font-semibold btn-danger btn gap-1">
|
||||||
@click="resetAll()"
|
|
||||||
class="h-10 min-h-10 px-2 py-1 font-semibold bg-red-600 text-slate-200 hover:bg-red-700 btn btn-sm gap-1"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="faTrash" size="lg" fixed-width></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faTrash" size="lg" fixed-width></FontAwesomeIcon>
|
||||||
Reset All
|
Reset All
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="resetLiveLogs()"
|
@click="resetLiveLogs()"
|
||||||
class="h-10 min-h-10 px-2 py-1 font-semibold bg-amber-600 text-slate-200 hover:bg-amber-700 btn btn-sm gap-1"
|
class="h-10 min-h-10 font-semibold btn-warning btn gap-1"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon :icon="faBan" size="lg"> fixed-width</FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faBan" size="lg"> fixed-width</FontAwesomeIcon>
|
||||||
Clear Live Logs
|
Clear Live Logs
|
||||||
|
@ -85,18 +92,16 @@
|
||||||
<FontAwesomeIcon :icon="faGraduationCap" class="mr-1"></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faGraduationCap" class="mr-1"></FontAwesomeIcon>
|
||||||
Selected Exercises
|
Selected Exercises
|
||||||
</h3>
|
</h3>
|
||||||
<div
|
<div v-for="exercise in exercises" :key="exercise.name" class="form-control pl-3">
|
||||||
v-for="(exercise) in exercises"
|
|
||||||
:key="exercise.name"
|
|
||||||
class="form-control pl-3"
|
|
||||||
>
|
|
||||||
<label class="label cursor-pointer justify-start">
|
<label class="label cursor-pointer justify-start">
|
||||||
<input
|
<input
|
||||||
@change="changeSelectionState($event.target.checked, exercise.uuid)"
|
@change="changeSelectionState($event.target.checked, exercise.uuid)"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="selected_exercises.includes(exercise.uuid)"
|
:checked="selected_exercises.includes(exercise.uuid)"
|
||||||
:value="exercise.uuid"
|
:value="exercise.uuid"
|
||||||
:class="`checkbox ${selected_exercises.includes(exercise.uuid) ? 'checkbox-success' : ''} [--fallback-bc:#94a3b8]`"
|
:class="`checkbox ${
|
||||||
|
selected_exercises.includes(exercise.uuid) ? 'checkbox-success' : ''
|
||||||
|
} [--fallback-bc:#94a3b8]`"
|
||||||
/>
|
/>
|
||||||
<span class="font-mono font-semibold text-base ml-3">{{ exercise.name }}</span>
|
<span class="font-mono font-semibold text-base ml-3">{{ exercise.name }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -109,13 +114,18 @@
|
||||||
<h4 class="font-semibold ml-1 my-3">
|
<h4 class="font-semibold ml-1 my-3">
|
||||||
<strong>MISP Status:</strong>
|
<strong>MISP Status:</strong>
|
||||||
<span class="ml-2">
|
<span class="ml-2">
|
||||||
<span :class="{
|
<span
|
||||||
'rounded-lg py-1 px-2': true,
|
:class="{
|
||||||
'dark:bg-neutral-800 bg-neutral-400 text-slate-800 dark:text-slate-200': diagnosticLoading,
|
'rounded-lg py-1 px-2 inline-flex': true,
|
||||||
'dark:bg-green-700 bg-green-500 text-slate-800 dark:text-slate-200': !diagnosticLoading && isMISPOnline,
|
'dark:bg-neutral-800 bg-neutral-400 text-slate-800 dark:text-slate-200':
|
||||||
'dark:bg-red-700 bg-red-700 text-slate-200 dark:text-slate-200': !diagnosticLoading && !isMISPOnline,
|
diagnosticLoading,
|
||||||
}">
|
'dark:bg-green-700 bg-green-500 text-slate-800 dark:text-slate-200':
|
||||||
<span v-if="diagnosticLoading" class="loading loading-dots loading-sm h-4 inline-block align-middle"></span>
|
!diagnosticLoading && isMISPOnline,
|
||||||
|
'dark:bg-red-700 bg-red-700 text-slate-200 dark:text-slate-200':
|
||||||
|
!diagnosticLoading && !isMISPOnline
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Loading v-if="diagnosticLoading" class="inline-block text-xl"></Loading>
|
||||||
<span v-else class="font-bold">
|
<span v-else class="font-bold">
|
||||||
{{ !isMISPOnline ? 'Unreachable' : `Online (${diagnostic['version']['version']})` }}
|
{{ !isMISPOnline ? 'Unreachable' : `Online (${diagnostic['version']['version']})` }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -125,15 +135,24 @@
|
||||||
<h4 class="font-semibold ml-1 my-3">
|
<h4 class="font-semibold ml-1 my-3">
|
||||||
<strong>ZMQ Status:</strong>
|
<strong>ZMQ Status:</strong>
|
||||||
<span class="ml-2">
|
<span class="ml-2">
|
||||||
<span :class="{
|
<span
|
||||||
'rounded-lg py-1 px-2': true,
|
:class="{
|
||||||
'dark:bg-neutral-800 bg-neutral-400 text-slate-800 dark:text-slate-200': diagnosticLoading,
|
'rounded-lg py-1 px-2 inline-flex': true,
|
||||||
'dark:bg-green-700 bg-green-500 text-slate-800 dark:text-slate-200': !diagnosticLoading && isZMQActive,
|
'dark:bg-neutral-800 bg-neutral-400 text-slate-800 dark:text-slate-200':
|
||||||
'dark:bg-red-700 bg-red-700 text-slate-200 dark:text-slate-200': !diagnosticLoading && !isZMQActive,
|
diagnosticLoading,
|
||||||
}">
|
'dark:bg-green-700 bg-green-500 text-slate-800 dark:text-slate-200':
|
||||||
<span v-if="diagnosticLoading" class="loading loading-dots loading-sm h-4 inline-block align-middle"></span>
|
!diagnosticLoading && isZMQActive,
|
||||||
|
'dark:bg-red-700 bg-red-700 text-slate-200 dark:text-slate-200':
|
||||||
|
!diagnosticLoading && !isZMQActive
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Loading v-if="diagnosticLoading" class="inline-block text-xl"></Loading>
|
||||||
<span v-else class="font-bold">
|
<span v-else class="font-bold">
|
||||||
{{ !isZMQActive ? 'No message received yet' : `ZMQ Active (${ZMQMessageCount} messages)` }}
|
{{
|
||||||
|
!isZMQActive
|
||||||
|
? 'No message received yet'
|
||||||
|
: `ZMQ Active (${ZMQMessageCount} messages)`
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -143,34 +162,52 @@
|
||||||
<h4 class="font-semibold ml-1"><strong>MISP Settings:</strong></h4>
|
<h4 class="font-semibold ml-1"><strong>MISP Settings:</strong></h4>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<div v-if="diagnosticLoading" class="flex justify-center">
|
<div v-if="diagnosticLoading" class="flex justify-center">
|
||||||
<span class="loading loading-dots loading-lg"></span>
|
<Loading class="text-3xl"></Loading>
|
||||||
</div>
|
</div>
|
||||||
<table v-else class="bg-white dark:bg-slate-700 rounded-lg shadow-xl w-full mt-2">
|
<table
|
||||||
|
v-else
|
||||||
|
class="bg-white dark:bg-slate-700 dark:text-slate-100 text-slate-700 rounded-lg shadow-xl w-full mt-2"
|
||||||
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-left">Setting</th>
|
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-left">
|
||||||
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-left">Value</th>
|
Setting
|
||||||
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-left">Expected Value</th>
|
</th>
|
||||||
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-center">Action</th>
|
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-left">
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-left">
|
||||||
|
Expected Value
|
||||||
|
</th>
|
||||||
|
<th class="border-b border-slate-200 dark:border-slate-600 p-2 text-center">
|
||||||
|
Action
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr v-for="(settingValues, setting) in diagnostic['settings']" :key="setting">
|
||||||
v-for="(settingValues, setting) in diagnostic['settings']"
|
|
||||||
:key="setting"
|
|
||||||
>
|
|
||||||
<td class="font-mono font-semibold text-base px-2">{{ setting }}</td>
|
<td class="font-mono font-semibold text-base px-2">{{ setting }}</td>
|
||||||
<td
|
<td
|
||||||
:class="`font-mono text-base tracking-tight px-2 ${settingValues.expected_value != settingValues.value ? 'text-red-600 dark:text-red-600' : ''}`"
|
:class="`font-mono text-base tracking-tight px-2 ${
|
||||||
|
settingValues.expected_value != settingValues.value
|
||||||
|
? 'text-red-600 dark:text-red-600'
|
||||||
|
: ''
|
||||||
|
}`"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="settingValues.value === undefined || settingValues.value === null"
|
||||||
|
class="text-nowrap"
|
||||||
|
>- none -</i
|
||||||
>
|
>
|
||||||
<i v-if="settingValues.value === undefined || settingValues.value === null" class="text-nowrap">- none -</i>
|
|
||||||
{{ settingValues.value }}
|
{{ settingValues.value }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-base tracking-tight px-2">{{ settingValues.expected_value }}</td>
|
<td class="font-mono text-base tracking-tight px-2">
|
||||||
|
{{ settingValues.expected_value }}
|
||||||
|
</td>
|
||||||
<td class="px-2 text-center">
|
<td class="px-2 text-center">
|
||||||
<span v-if="settingValues.error === true"
|
<span v-if="settingValues.error === true" class="text-red-600 dark:text-red-600"
|
||||||
class="text-red-600 dark:text-red-600"
|
>Error: {{ settingValues.errorMessage }}</span
|
||||||
>Error: {{ settingValues.errorMessage }}</span>
|
>
|
||||||
<button
|
<button
|
||||||
v-else-if="settingValues.expected_value != settingValues.value"
|
v-else-if="settingValues.expected_value != settingValues.value"
|
||||||
@click="clickedButtons.push(setting) && settingHandler(setting)"
|
@click="clickedButtons.push(setting) && settingHandler(setting)"
|
||||||
|
@ -182,7 +219,7 @@
|
||||||
Remediate
|
Remediate
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="loading loading-dots loading-sm"></span>
|
<Loading class="text-xl"></Loading>
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
<span v-else class="text-base font-bold text-green-600 dark:text-green-600">
|
<span v-else class="text-base font-bold text-green-600 dark:text-green-600">
|
||||||
|
@ -195,11 +232,7 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<form method="dialog" class="modal-backdrop backdrop-blur">
|
</Modal>
|
||||||
<button>close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, computed } from "vue"
|
import { ref, watch, computed } from 'vue'
|
||||||
import { notifications, userCount, notificationCounter, notificationAPICounter, toggleVerboseMode, toggleApiQueryMode } from "@/socket";
|
import {
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
notifications,
|
||||||
|
userCount,
|
||||||
|
notificationCounter,
|
||||||
|
notificationAPICounter,
|
||||||
|
toggleVerboseMode,
|
||||||
|
toggleApiQueryMode
|
||||||
|
} from '@/socket'
|
||||||
import { faSignal, faCloud, faCog, faUsers, faCircle } from '@fortawesome/free-solid-svg-icons'
|
import { faSignal, faCloud, faCog, faUsers, faCircle } from '@fortawesome/free-solid-svg-icons'
|
||||||
import TheLiveLogsActivityGraphVue from "./TheLiveLogsActivityGraph.vue";
|
import TheLiveLogsActivityGraphVue from './TheLiveLogsActivityGraph.vue'
|
||||||
|
import ToggleSwitch from '@/components/elements/ToggleSwitch.vue'
|
||||||
|
|
||||||
const verbose = ref(false)
|
const verbose = ref(false)
|
||||||
const api_query = ref(false)
|
const api_query = ref(false)
|
||||||
|
@ -26,48 +32,74 @@
|
||||||
return 'text-amber-600'
|
return 'text-amber-600'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-2xl mt-6 mb-2 font-bold text-blue-500 dark:text-blue-400">
|
<h3 class="text-2xl mt-6 mb-2 font-bold text-blue-500 dark:text-blue-400 uppercase">
|
||||||
<FontAwesomeIcon :icon="faSignal"></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faSignal"></FontAwesomeIcon>
|
||||||
Live logs
|
Live logs
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="mb-2 flex flex-wrap gap-x-3">
|
<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="rounded-lg py-1 px-2 dark:bg-sky-700 bg-sky-400 text-slate-800 dark:text-slate-200"
|
||||||
|
>
|
||||||
<span class="mr-1">
|
<span class="mr-1">
|
||||||
<FontAwesomeIcon :icon="faUsers" size="sm"></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faUsers" size="sm"></FontAwesomeIcon>
|
||||||
Players:
|
Players:
|
||||||
</span>
|
</span>
|
||||||
<span class="font-bold">{{ userCount }}</span>
|
<span class="font-bold">{{ userCount }}</span>
|
||||||
</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="rounded-lg py-1 px-2 dark:bg-sky-700 bg-sky-400 text-slate-800 dark:text-slate-200"
|
||||||
|
>
|
||||||
<span class="mr-1">
|
<span class="mr-1">
|
||||||
<FontAwesomeIcon :icon="faSignal" size="sm"></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faSignal" size="sm"></FontAwesomeIcon>
|
||||||
Total Queries:
|
Total Queries:
|
||||||
</span>
|
</span>
|
||||||
<span class="font-bold">{{ notificationCounter }}</span>
|
<span class="font-bold">{{ notificationCounter }}</span>
|
||||||
</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="rounded-lg py-1 px-2 dark:bg-sky-700 bg-sky-400 text-slate-800 dark:text-slate-200"
|
||||||
|
>
|
||||||
<span class="mr-1">
|
<span class="mr-1">
|
||||||
<FontAwesomeIcon :icon="faCog" size="sm" :mask="faCloud" transform="shrink-7 left-1"></FontAwesomeIcon>
|
<FontAwesomeIcon
|
||||||
|
:icon="faCog"
|
||||||
|
size="sm"
|
||||||
|
:mask="faCloud"
|
||||||
|
transform="shrink-7 left-1"
|
||||||
|
></FontAwesomeIcon>
|
||||||
Total API Queries:
|
Total API Queries:
|
||||||
</span>
|
</span>
|
||||||
<span class="font-bold">{{ notificationAPICounter }}</span>
|
<span class="font-bold">{{ notificationAPICounter }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<label class="mr-1 flex items-center cursor-pointer text-slate-700 dark:text-slate-300">
|
<label class="mr-1 flex items-center cursor-pointer text-slate-700 dark:text-slate-300">
|
||||||
<input type="checkbox" class="toggle toggle-warning [--fallback-su:#22c55e] mr-1" :checked="verbose" @change="verbose = !verbose"/>
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-warning mr-1"
|
||||||
|
:checked="verbose"
|
||||||
|
@change="verbose = !verbose"
|
||||||
|
/>
|
||||||
Verbose
|
Verbose
|
||||||
</label>
|
</label>
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<label class="mr-1 flex items-center cursor-pointer text-slate-700 dark:text-slate-300">
|
<label class="mr-1 flex items-center cursor-pointer text-slate-700 dark:text-slate-300">
|
||||||
<input type="checkbox" class="toggle toggle-success [--fallback-su:#22c55e] mr-1" :checked="api_query" @change="api_query = !api_query"/>
|
<input
|
||||||
<FontAwesomeIcon :icon="faCog" size="sm" :mask="faCloud" transform="shrink-7 left-1" class="mr-1"></FontAwesomeIcon>
|
type="checkbox"
|
||||||
|
class="toggle toggle-success mr-1"
|
||||||
|
:checked="api_query"
|
||||||
|
@change="api_query = !api_query"
|
||||||
|
/>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="faCog"
|
||||||
|
size="sm"
|
||||||
|
:mask="faCloud"
|
||||||
|
transform="shrink-7 left-1"
|
||||||
|
class="mr-1"
|
||||||
|
></FontAwesomeIcon>
|
||||||
API Queries
|
API Queries
|
||||||
</label>
|
</label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -99,7 +131,9 @@
|
||||||
<td
|
<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"
|
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"
|
<FontAwesomeIcon
|
||||||
|
:icon="faCircle"
|
||||||
|
size="xs"
|
||||||
:class="getClassFromResponseCode(notification.response_code)"
|
:class="getClassFromResponseCode(notification.response_code)"
|
||||||
></FontAwesomeIcon>
|
></FontAwesomeIcon>
|
||||||
<pre class="inline ml-1">{{ notification.response_code }}</pre>
|
<pre class="inline ml-1">{{ notification.response_code }}</pre>
|
||||||
|
@ -111,40 +145,53 @@
|
||||||
<span class="text-lg font-bold font-mono">{{ notification.user.split('@')[0] }}</span>
|
<span class="text-lg font-bold font-mono">{{ notification.user.split('@')[0] }}</span>
|
||||||
<span class="text-xs font-mono">@{{ notification.user.split('@')[1] }}</span>
|
<span class="text-xs font-mono">@{{ notification.user.split('@')[1] }}</span>
|
||||||
</td>
|
</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
|
||||||
<td class="border-b border-slate-100 dark:border-slate-700 text-sky-600 dark:text-sky-400 p-1">
|
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">
|
<div class="flex items-center">
|
||||||
<span v-if="notification.http_method == 'POST'"
|
<span
|
||||||
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
v-if="notification.http_method == 'POST'"
|
||||||
dark:bg-amber-600 dark:text-neutral-100 bg-amber-600 text-neutral-100"
|
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>
|
>POST</span
|
||||||
<span v-else-if="notification.http_method == 'PUT'"
|
>
|
||||||
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
<span
|
||||||
dark:bg-amber-600 dark:text-neutral-100 bg-amber-600 text-neutral-100"
|
v-else-if="notification.http_method == 'PUT'"
|
||||||
>PUT</span>
|
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"
|
||||||
<span v-else-if="notification.http_method == 'DELETE'"
|
>PUT</span
|
||||||
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"
|
<span
|
||||||
>DEL</span>
|
v-else-if="notification.http_method == 'DELETE'"
|
||||||
<span v-else
|
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"
|
||||||
class="p-1 rounded-md font-bold text-xs mr-2 w-10 inline-block text-center
|
>DEL</span
|
||||||
dark:bg-blue-600 dark:text-neutral-100 bg-blue-600 text-neutral-100"
|
>
|
||||||
>{{ notification.http_method }}</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
|
<FontAwesomeIcon
|
||||||
v-if="notification.is_api_request"
|
v-if="notification.is_api_request"
|
||||||
class="text-slate-800 dark:text-slate-100 mr-1 inline-block"
|
class="text-slate-800 dark:text-slate-100 mr-1 inline-block"
|
||||||
:icon="faCog" :mask="faCloud" transform="shrink-7 left-1"
|
:icon="faCog"
|
||||||
|
:mask="faCloud"
|
||||||
|
transform="shrink-7 left-1"
|
||||||
></FontAwesomeIcon>
|
></FontAwesomeIcon>
|
||||||
<pre class="text-sm inline">{{ notification.url }}</pre>
|
<pre class="text-sm inline">{{ notification.url }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="border-b border-slate-100 dark:border-slate-700 text-slate-600 dark:text-slate-300 p-1">
|
<td
|
||||||
<div v-if="notification.http_method == 'POST'"
|
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"
|
class="border border-slate-200 dark:border-slate-600 bg-slate-100 dark:bg-slate-600 rounded-md"
|
||||||
>
|
>
|
||||||
<pre
|
<pre class="p-1 text-xs">{{ JSON.stringify(notification.payload, null, 2) }}</pre>
|
||||||
class="p-1 text-xs"
|
|
||||||
>{{ JSON.stringify(notification.payload, null, 2) }}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, computed } from "vue"
|
import { ref, watch, computed } from 'vue'
|
||||||
import { notificationHistory, notificationHistoryConfig } from "@/socket";
|
import { notificationHistory, notificationHistoryConfig } from '@/socket'
|
||||||
import { darkModeEnabled } from "@/settings.js"
|
import { darkModeEnabled } from '@/settings.js'
|
||||||
|
|
||||||
const theChart = ref(null)
|
const theChart = ref(null)
|
||||||
const chartInitSeries = [
|
const chartInitSeries = [{ data: Array.from(Array(12 * 20)).map(() => 0) }]
|
||||||
{data: Array.from(Array(12*20)).map(()=> 0)}
|
|
||||||
]
|
|
||||||
const hasActivity = computed(() => notificationHistory.value.length > 0)
|
const hasActivity = computed(() => notificationHistory.value.length > 0)
|
||||||
const chartSeries = computed(() => {
|
const chartSeries = computed(() => {
|
||||||
return notificationHistory.value ? notificationHistorySeries.value : chartInitSeries.value
|
return notificationHistory.value ? notificationHistorySeries.value : chartInitSeries.value
|
||||||
|
@ -37,8 +35,8 @@
|
||||||
animations: {
|
animations: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
easing: 'easeinout',
|
easing: 'easeinout',
|
||||||
speed: 200,
|
speed: 200
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
colors: [darkModeEnabled.value ? '#008ffb' : '#1f9eff'],
|
colors: [darkModeEnabled.value ? '#008ffb' : '#1f9eff'],
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
|
@ -50,33 +48,43 @@
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 20,
|
max: 20,
|
||||||
labels: {
|
labels: {
|
||||||
show: false,
|
show: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false,
|
enabled: false
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="my-2 --ml-1 bg-slate-50 dark:bg-slate-600 py-1 pl-1 pr-3 rounded-md relative flex flex-col">
|
<div
|
||||||
|
class="my-2 --ml-1 bg-slate-50 dark:bg-slate-600 py-1 pl-1 pr-3 rounded-md relative flex flex-col"
|
||||||
|
>
|
||||||
<div :class="`${!hasActivity ? 'hidden' : 'absolute'} h-10 -mt-1 w-full z-30`">
|
<div :class="`${!hasActivity ? 'hidden' : 'absolute'} h-10 -mt-1 w-full z-30`">
|
||||||
<div class="text-xxs flex justify-between h-full items-center text-slate-500 dark:text-slate-300 select-none">
|
<div
|
||||||
<span class="-rotate-90 w-8 -ml-3">- {{ notificationHistoryConfig.buffer_timestamp_min }}min</span>
|
class="text-2xs flex justify-between h-full items-center text-slate-500 dark:text-slate-300 select-none"
|
||||||
|
>
|
||||||
|
<span class="-rotate-90 w-8 -ml-3"
|
||||||
|
>- {{ notificationHistoryConfig.buffer_timestamp_min }}min</span
|
||||||
|
>
|
||||||
<span class="-rotate-90 w-8 text-xs">–</span>
|
<span class="-rotate-90 w-8 text-xs">–</span>
|
||||||
<span class="-rotate-90 w-8 text-lg">–</span>
|
<span class="-rotate-90 w-8 text-lg">–</span>
|
||||||
<span class="-rotate-90 w-8 text-xs">–</span>
|
<span class="-rotate-90 w-8 text-xs">–</span>
|
||||||
<span class="-rotate-90 w-8 -mr-1.5">- 0min</span>
|
<span class="-rotate-90 w-8 -mr-1.5">- 0min</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<i :class="['text-center text-slate-600 dark:text-slate-400', hasActivity ? 'hidden' : 'block']">
|
<i
|
||||||
|
:class="['text-center text-slate-600 dark:text-slate-400', hasActivity ? 'hidden' : 'block']"
|
||||||
|
>
|
||||||
- No recorded activity -
|
- No recorded activity -
|
||||||
</i>
|
</i>
|
||||||
<apexchart
|
<apexchart
|
||||||
ref="theChart" :class="hasActivity ? 'block' : 'absolute h-8 w-full'" height="32" width="100%"
|
ref="theChart"
|
||||||
|
:class="hasActivity ? 'block' : 'absolute h-8 w-full'"
|
||||||
|
height="32"
|
||||||
|
width="100%"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="chartSeries"
|
:series="chartSeries"
|
||||||
></apexchart>
|
></apexchart>
|
||||||
|
|
|
@ -1,49 +1,51 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from 'vue'
|
||||||
import { progresses, userCount } from "@/socket";
|
import { progresses, userCount } from '@/socket'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
import { faUsers } from '@fortawesome/free-solid-svg-icons'
|
import { faUsers } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { darkModeEnabled } from "@/settings.js"
|
import { darkModeEnabled } from '@/settings.js'
|
||||||
import LiveLogsUserActivityGraph from "./LiveLogsUserActivityGraph.vue"
|
import LiveLogsUserActivityGraph from './LiveLogsUserActivityGraph.vue'
|
||||||
|
|
||||||
|
const compactGrid = computed(() => {
|
||||||
const compactGrid = computed(() => { return userCount.value > 70 })
|
return userCount.value > 70
|
||||||
const sortedProgress = computed(() => Object.values(progresses.value).sort((a, b) => {
|
})
|
||||||
|
const sortedProgress = computed(() =>
|
||||||
|
Object.values(progresses.value).sort((a, b) => {
|
||||||
if (a.email < b.email) {
|
if (a.email < b.email) {
|
||||||
return -1;
|
return -1
|
||||||
}
|
}
|
||||||
if (a.email > b.email) {
|
if (a.email > b.email) {
|
||||||
return 1;
|
return 1
|
||||||
}
|
}
|
||||||
return 0;
|
return 0
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="
|
<div
|
||||||
mt-2 px-2 pt-1 pb-2 rounded border
|
class="mt-2 px-2 pt-1 pb-2 rounded border bg-slate-100 border-slate-300 dark:bg-slate-600 dark:border-slate-800"
|
||||||
bg-slate-100 border-slate-300 dark:bg-slate-600 dark:border-slate-800
|
>
|
||||||
">
|
<h4 class="text-xl mb-2 font-bold text-blue-500 dark:text-blue-400 uppercase">
|
||||||
|
|
||||||
<h4 class="text-xl mb-2 font-bold text-blue-500 dark:text-blue-400">
|
|
||||||
<FontAwesomeIcon :icon="faUsers"></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faUsers"></FontAwesomeIcon>
|
||||||
Active Players
|
Active Players
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div :class="`flex flex-wrap ${compactGrid ? 'gap-1' : 'gap-2'}`">
|
<div :class="`flex flex-wrap ${compactGrid ? 'gap-1' : 'gap-2'}`">
|
||||||
<span
|
<span
|
||||||
v-for="(progress) in sortedProgress"
|
v-for="progress in sortedProgress"
|
||||||
:key="progress.user_id"
|
:key="progress.user_id"
|
||||||
class="bg-slate-200 dark:bg-slate-900 rounded border drop-shadow-lg border-slate-700"
|
class="bg-slate-200 dark:bg-slate-900 rounded border drop-shadow-lg border-slate-700"
|
||||||
>
|
>
|
||||||
<span class="
|
<span class="flex p-2 mb-1 text-slate-600 dark:text-slate-400">
|
||||||
flex p-2 mb-1
|
|
||||||
text-slate-600 dark:text-slate-400
|
|
||||||
">
|
|
||||||
<span :class="`flex flex-col ${compactGrid ? 'w-[120px]' : 'w-60'}`">
|
<span :class="`flex flex-col ${compactGrid ? 'w-[120px]' : 'w-60'}`">
|
||||||
<span :title="progress.user_id" class="text-nowrap inline-block leading-5 truncate">
|
<span :title="progress.user_id" class="text-nowrap inline-block leading-5 truncate">
|
||||||
<span :class="`${compactGrid ? 'text-base' : 'text-lg'} font-bold font-mono leading-5 tracking-tight`">{{ progress.email.split('@')[0] }}</span>
|
<span
|
||||||
<span :class="`${compactGrid ? 'text-xs' : 'text-xs'} font-mono tracking-tight`">@{{ progress.email.split('@')[1] }}</span>
|
:class="`${compactGrid ? 'text-base' : 'text-lg'} font-bold font-mono leading-5 tracking-tight`"
|
||||||
|
>{{ progress.email.split('@')[0] }}</span
|
||||||
|
>
|
||||||
|
<span :class="`${compactGrid ? 'text-xs' : 'text-xs'} font-mono tracking-tight`"
|
||||||
|
>@{{ progress.email.split('@')[1] }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<LiveLogsUserActivityGraph
|
<LiveLogsUserActivityGraph
|
||||||
:user_id="progress.user_id"
|
:user_id="progress.user_id"
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from 'vue'
|
||||||
import { active_exercises as exercises } from "@/socket";
|
import { active_exercises as exercises } from '@/socket'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import {
|
||||||
import { faGraduationCap, faUpRightAndDownLeftFromCenter, faDownLeftAndUpRightToCenter, faWarning } from '@fortawesome/free-solid-svg-icons'
|
faGraduationCap,
|
||||||
import TheScoreTable from "./scoreViews/TheScoreTable.vue"
|
faUpRightAndDownLeftFromCenter,
|
||||||
import TheFullScreenScoreGrid from "./scoreViews/TheFullScreenScoreGrid.vue"
|
faDownLeftAndUpRightToCenter,
|
||||||
import ThePlayerGrid from "./ThePlayerGrid.vue"
|
faWarning
|
||||||
import { fullscreenModeOn } from "@/settings.js"
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import TheScoreTable from './scoreViews/TheScoreTable.vue'
|
||||||
|
import TheFullScreenScoreGrid from './scoreViews/TheFullScreenScoreGrid.vue'
|
||||||
|
import ThePlayerGrid from './ThePlayerGrid.vue'
|
||||||
|
import { fullscreenModeOn } from '@/settings.js'
|
||||||
|
|
||||||
const hasExercises = computed(() => exercises.value.length > 0)
|
const hasExercises = computed(() => exercises.value.length > 0)
|
||||||
const fullscreen_panel = ref(false)
|
const fullscreen_panel = ref(false)
|
||||||
|
@ -23,36 +27,28 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h3 class="text-2xl mt-6 mb-2 font-bold text-blue-500 dark:text-blue-400">
|
<h3 class="text-2xl mt-6 mb-2 font-bold text-blue-500 dark:text-blue-400 uppercase">
|
||||||
<FontAwesomeIcon :icon="faGraduationCap"></FontAwesomeIcon>
|
<FontAwesomeIcon :icon="faGraduationCap"></FontAwesomeIcon>
|
||||||
Active Exercises
|
Active Exercises
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div
|
<div v-if="!hasExercises" class="text-slate-600 dark:text-slate-400 p-3 pl-6">
|
||||||
v-if="!hasExercises"
|
<Alert variant="warning">
|
||||||
class="text-slate-600 dark:text-slate-400 p-3 pl-6"
|
|
||||||
>
|
|
||||||
<div class="
|
|
||||||
p-2 border-l-4 text-left rounded
|
|
||||||
dark:bg-yellow-300 dark:text-slate-900 dark:border-yellow-700
|
|
||||||
bg-yellow-200 text-slate-900 border-yellow-700
|
|
||||||
">
|
|
||||||
<FontAwesomeIcon :icon="faWarning" class="text-yellow-700 text-lg mx-3"></FontAwesomeIcon>
|
|
||||||
<strong class="">No Exercise available.</strong>
|
<strong class="">No Exercise available.</strong>
|
||||||
<span class="ml-1">Select an exercise in the <i class="underline">Admin panel</i>.</span>
|
<span class="ml-1">Select an exercise in the <i class="underline">Admin panel</i>.</span>
|
||||||
</div>
|
</Alert>
|
||||||
|
|
||||||
<ThePlayerGrid></ThePlayerGrid>
|
<ThePlayerGrid></ThePlayerGrid>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template
|
<template v-for="(exercise, exercise_index) in exercises" :key="exercise.name">
|
||||||
v-for="(exercise, exercise_index) in exercises"
|
|
||||||
:key="exercise.name"
|
|
||||||
>
|
|
||||||
<div :class="fullscreen_panel === false ? 'relative min-w-fit' : ''">
|
<div :class="fullscreen_panel === false ? 'relative min-w-fit' : ''">
|
||||||
<span
|
<span
|
||||||
v-show="fullscreen_panel === false || fullscreen_panel === exercise_index"
|
v-show="fullscreen_panel === false || fullscreen_panel === exercise_index"
|
||||||
:class="['inline-block absolute shadow-lg z-50', fullscreen_panel === false ? 'top-0 -right-7' : 'top-2 right-2']"
|
:class="[
|
||||||
|
'inline-block absolute shadow-lg z-50',
|
||||||
|
fullscreen_panel === false ? 'top-0 -right-7' : 'top-2 right-2'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@click="toggleFullScreen(exercise_index)"
|
@click="toggleFullScreen(exercise_index)"
|
||||||
|
@ -63,7 +59,14 @@
|
||||||
${fullscreen_panel === false ? 'rounded-r-md' : 'rounded-bl-md'}
|
${fullscreen_panel === false ? 'rounded-r-md' : 'rounded-bl-md'}
|
||||||
`"
|
`"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon :icon="fullscreen_panel !== exercise_index ? faUpRightAndDownLeftFromCenter : faDownLeftAndUpRightToCenter" fixed-width></FontAwesomeIcon>
|
<FontAwesomeIcon
|
||||||
|
:icon="
|
||||||
|
fullscreen_panel !== exercise_index
|
||||||
|
? faUpRightAndDownLeftFromCenter
|
||||||
|
: faDownLeftAndUpRightToCenter
|
||||||
|
"
|
||||||
|
fixed-width
|
||||||
|
></FontAwesomeIcon>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<KeepAlive>
|
<KeepAlive>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue"
|
import { ref, onMounted } from 'vue'
|
||||||
import { socketConnected, zmqLastTime } from "@/socket";
|
import { socketConnected, zmqLastTime } from '@/socket'
|
||||||
|
|
||||||
const zmqLastTimeSecond = ref('?')
|
const zmqLastTimeSecond = ref('?')
|
||||||
|
|
||||||
function refreshLastTime() {
|
function refreshLastTime() {
|
||||||
if (zmqLastTime.value !== false) {
|
if (zmqLastTime.value !== false) {
|
||||||
zmqLastTimeSecond.value = parseInt(((new Date()).getTime() - zmqLastTime.value * 1000) / 1000)
|
zmqLastTimeSecond.value = parseInt((new Date().getTime() - zmqLastTime.value * 1000) / 1000)
|
||||||
} else {
|
} else {
|
||||||
zmqLastTimeSecond.value = '?'
|
zmqLastTimeSecond.value = '?'
|
||||||
}
|
}
|
||||||
|
@ -17,30 +17,26 @@
|
||||||
refreshLastTime()
|
refreshLastTime()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="flex flex-col justify-center mt-1">
|
<span class="flex flex-col justify-center mt-1">
|
||||||
<span :class="{
|
<span
|
||||||
|
:class="{
|
||||||
'px-2 rounded-md inline-block w-48 leading-4': true,
|
'px-2 rounded-md inline-block w-48 leading-4': true,
|
||||||
'text-slate-900 dark:text-slate-400': socketConnected,
|
'text-slate-900 dark:text-slate-400': socketConnected,
|
||||||
'text-slate-50 bg-red-600 px-2 py-1': !socketConnected,
|
'text-slate-50 bg-red-600 px-2 py-1': !socketConnected
|
||||||
}">
|
}"
|
||||||
|
>
|
||||||
<span class="mr-1">Socket.IO:</span>
|
<span class="mr-1">Socket.IO:</span>
|
||||||
<span v-show="socketConnected" class="font-semibold text-green-600 dark:text-green-400">Connected</span>
|
<span v-show="socketConnected" class="font-semibold text-green-600 dark:text-green-400"
|
||||||
|
>Connected</span
|
||||||
|
>
|
||||||
<span v-show="!socketConnected" class="font-semibold text-slate-50">Disconnected</span>
|
<span v-show="!socketConnected" class="font-semibold text-slate-50">Disconnected</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span v-if="socketConnected" class="text-xs font-thin leading-3 inline-block text-center">
|
||||||
v-if="socketConnected"
|
<template v-if="zmqLastTimeSecond == 0"> online </template>
|
||||||
class="text-xs font-thin leading-3 inline-block text-center"
|
<template v-else> Last keep-alive: {{ zmqLastTimeSecond }}s ago </template>
|
||||||
>
|
|
||||||
<template v-if="zmqLastTimeSecond == 0">
|
|
||||||
online
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
Last keep-alive: {{ zmqLastTimeSecond }}s ago
|
|
||||||
</template>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
|
@ -1,8 +1,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { darkModeOn } from "@/settings.js"
|
import { darkModeOn } from '@/settings.js'
|
||||||
|
|
||||||
const darkMode = ref(darkModeOn.value)
|
const darkMode = ref(darkModeOn.value)
|
||||||
|
|
||||||
|
@ -23,9 +22,10 @@
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@click="darkMode = !darkMode"
|
@click="darkMode = !darkMode"
|
||||||
:checked="darkMode"
|
:checked="darkMode"
|
||||||
class="toggle theme-controller bg-slate-400 col-span-2 col-start-1 row-start-1 [--tglbg:#e2e8f0]" />
|
class="toggle theme-controller bg-slate-500 col-span-2 col-start-1 row-start-1 [--tglbg:#e2e8f0] dark:[--tglbg:#1d232a]"
|
||||||
|
/>
|
||||||
<svg
|
<svg
|
||||||
class="stroke-base-100 fill-base-100 col-start-1 row-start-1"
|
class="stroke-slate-800 dark:stroke-slate-100 fill-slate-800 dark:fill-slate-100 col-start-1 row-start-1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
|
@ -34,13 +34,15 @@
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round">
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
<circle cx="12" cy="12" r="5" />
|
<circle cx="12" cy="12" r="5" />
|
||||||
<path
|
<path
|
||||||
d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
|
d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg
|
<svg
|
||||||
class="stroke-base-100 fill-base-100 col-start-2 row-start-1"
|
class="dark:stroke-slate-800 stroke-slate-700 dark:fill-slate-800 fill-slate-700 col-start-2 row-start-1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
|
@ -49,9 +51,48 @@
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round">
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toggle {
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--handleoffset: 1.5rem;
|
||||||
|
--handleoffsetcalculator: calc(var(--handleoffset) * -1);
|
||||||
|
--togglehandleborder: 0 0;
|
||||||
|
@apply h-6 w-12 rounded-3xl cursor-pointer appearance-none border border-current bg-current;
|
||||||
|
transition:
|
||||||
|
background,
|
||||||
|
box-shadow var(--animation-input, 0.2s) ease-out;
|
||||||
|
box-shadow:
|
||||||
|
var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset,
|
||||||
|
0 0 0 2px var(--tglbg) inset,
|
||||||
|
var(--togglehandleborder);
|
||||||
|
&:focus-visible {
|
||||||
|
@apply outline outline-2 outline-offset-2;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
@apply bg-current;
|
||||||
|
}
|
||||||
|
&:checked,
|
||||||
|
&[aria-checked='true'] {
|
||||||
|
background-image: none;
|
||||||
|
--handleoffsetcalculator: var(--handleoffset);
|
||||||
|
}
|
||||||
|
&:indeterminate {
|
||||||
|
box-shadow:
|
||||||
|
calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset,
|
||||||
|
calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset,
|
||||||
|
0 0 0 2px var(--tglbg) inset;
|
||||||
|
}
|
||||||
|
&:disabled {
|
||||||
|
@apply cursor-not-allowed bg-transparent opacity-30;
|
||||||
|
--togglehandleborder: 0 0 0 3px #000 inset, var(--handleoffsetcalculator) 0 0 3px #000 inset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
63
src/components/elements/Alert.vue
Normal file
63
src/components/elements/Alert.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
faCircleCheck,
|
||||||
|
faCircleExclamation,
|
||||||
|
faCircleInfo,
|
||||||
|
faTriangleExclamation
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: String,
|
||||||
|
message: [Array, String],
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
validator(value, props) {
|
||||||
|
return ['success', 'warning', 'danger', 'info'].includes(value)
|
||||||
|
},
|
||||||
|
default: 'info'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
if (props.variant == 'success') {
|
||||||
|
return faCircleCheck
|
||||||
|
} else if (props.variant == 'warning') {
|
||||||
|
return faCircleExclamation
|
||||||
|
} else if (props.variant == 'danger') {
|
||||||
|
return faTriangleExclamation
|
||||||
|
}
|
||||||
|
return faCircleInfo
|
||||||
|
})
|
||||||
|
|
||||||
|
const variantClass = computed(() => {
|
||||||
|
if (props.variant == 'success') {
|
||||||
|
return 'green'
|
||||||
|
} else if (props.variant == 'warning') {
|
||||||
|
return 'amber'
|
||||||
|
} else if (props.variant == 'danger') {
|
||||||
|
return 'red'
|
||||||
|
}
|
||||||
|
return 'blue'
|
||||||
|
})
|
||||||
|
|
||||||
|
const messages = computed(() => {
|
||||||
|
return Array.isArray(props.message)
|
||||||
|
? props.message
|
||||||
|
: props.message !== undefined
|
||||||
|
? [props.message]
|
||||||
|
: []
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="`mb-2 p-2 border-l-4 text-left rounded dark:bg-${variantClass}-300 dark:text-slate-900 dark:border-${variantClass}-700 bg-${variantClass}-200 text-slate-900 border-${variantClass}-700`"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="icon"
|
||||||
|
:class="`text-${variantClass}-700 text-lg mx-3`"
|
||||||
|
></FontAwesomeIcon>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
7
src/components/elements/Loading.vue
Normal file
7
src/components/elements/Loading.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<script setup>
|
||||||
|
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FontAwesomeIcon :icon="faSpinner" class="fa-spin" fixed-width></FontAwesomeIcon>
|
||||||
|
</template>
|
73
src/components/elements/Modal.vue
Normal file
73
src/components/elements/Modal.vue
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
showModal: Boolean
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialog = ref(null)
|
||||||
|
|
||||||
|
const emit = defineEmits(['modal-close'])
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
emit('modal-close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div>
|
||||||
|
<Transition>
|
||||||
|
<div
|
||||||
|
v-if="props.showModal"
|
||||||
|
class="fixed w-4/6 top-20 left-2/4 -translate-x-1/2 rounded-lg border border-slate-800 shadow-lg z-50"
|
||||||
|
>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
@click.stop="closeModal()"
|
||||||
|
class="bg-white/30 backdrop-blur-sm fixed top-0 bottom-0 left-0 right-0 z-40 cursor-pointer"
|
||||||
|
></div>
|
||||||
|
</Teleport>
|
||||||
|
<div class="flex px-4 py-3 bg-slate-700 rounded-t-lg border-b border-slate-800">
|
||||||
|
<h2 class="text-white font-semibold text-lg">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</h2>
|
||||||
|
<span class="ml-auto text-xl">
|
||||||
|
<button
|
||||||
|
@click.stop="closeModal()"
|
||||||
|
class="hover:text-slate-200 hover:dark:text-slate-50 hover:bg-slate-200/20 rounded-full p-1"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon :icon="faTimes" class="fa-fw"></FontAwesomeIcon>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-3 bg-slate-100">
|
||||||
|
<slot name="body"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-3 bg-slate-100 rounded-b-lg">
|
||||||
|
<slot name="footer">
|
||||||
|
<div class="flex flex-row-reverse">
|
||||||
|
<button class="btn btn-primary btn-lg" @click.stop="closeModal()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-enter-active {
|
||||||
|
@apply transition duration-150 ease-out;
|
||||||
|
}
|
||||||
|
.v-leave-active {
|
||||||
|
@apply transition duration-150 ease-in;
|
||||||
|
}
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
@apply scale-90;
|
||||||
|
@apply opacity-0;
|
||||||
|
}
|
||||||
|
</style>
|
100
src/components/elements/Toast.vue
Normal file
100
src/components/elements/Toast.vue
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { removeToast } from '@/utils.js'
|
||||||
|
import {
|
||||||
|
faCancel,
|
||||||
|
faCheck,
|
||||||
|
faCircleCheck,
|
||||||
|
faCircleExclamation,
|
||||||
|
faCircleInfo,
|
||||||
|
faClose,
|
||||||
|
faTriangleExclamation
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: Number,
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
validator(value, props) {
|
||||||
|
return ['success', 'warning', 'danger', 'info'].includes(value)
|
||||||
|
},
|
||||||
|
default: 'info'
|
||||||
|
},
|
||||||
|
confirm: Boolean,
|
||||||
|
confirmCb: Function
|
||||||
|
})
|
||||||
|
|
||||||
|
const duration = 7000
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
if (props.variant == 'success') {
|
||||||
|
return faCircleCheck
|
||||||
|
} else if (props.variant == 'warning') {
|
||||||
|
return faCircleExclamation
|
||||||
|
} else if (props.variant == 'danger') {
|
||||||
|
return faTriangleExclamation
|
||||||
|
}
|
||||||
|
return faCircleInfo
|
||||||
|
})
|
||||||
|
|
||||||
|
const variantClass = computed(() => {
|
||||||
|
if (props.variant == 'success') {
|
||||||
|
return 'green'
|
||||||
|
} else if (props.variant == 'warning') {
|
||||||
|
return 'amber'
|
||||||
|
} else if (props.variant == 'danger') {
|
||||||
|
return 'red'
|
||||||
|
}
|
||||||
|
return 'blue'
|
||||||
|
})
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
function callConfirmCb() {
|
||||||
|
props.confirmCb()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
removeToast(props.id)
|
||||||
|
}, duration)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="`flex flex-col min-w-72 py-2 px-3 rounded bg-${variantClass}-100`">
|
||||||
|
<div :class="`text-${variantClass}-800 flex items-center`">
|
||||||
|
<FontAwesomeIcon :icon="icon" class="mr-2"></FontAwesomeIcon>
|
||||||
|
<span class="font-semibold">{{ props.title }}</span>
|
||||||
|
<button class="ml-auto w-6 btn btn-link !p-0">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
:icon="faClose"
|
||||||
|
class="text-gray-500 hover:text-gray-700"
|
||||||
|
fixed-width
|
||||||
|
@click="close()"
|
||||||
|
></FontAwesomeIcon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-slate-600 p-1 font-light"
|
||||||
|
v-if="props.message !== undefined && props.message.length > 0"
|
||||||
|
>
|
||||||
|
{{ props.message }}
|
||||||
|
</div>
|
||||||
|
<div v-if="props.confirm" class="flex gap-1">
|
||||||
|
<button class="btn btn-info" @click="callConfirmCb()">
|
||||||
|
<FontAwesomeIcon :icon="faCheck" class="fa-fw"></FontAwesomeIcon> Confirm
|
||||||
|
</button>
|
||||||
|
<button class="btn" @click="close()">
|
||||||
|
<FontAwesomeIcon :icon="faCancel" class="fa-fw"></FontAwesomeIcon> Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
47
src/components/elements/Toaster.vue
Normal file
47
src/components/elements/Toaster.vue
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { allToasts, removeToast } from '@/utils.js'
|
||||||
|
import Toast from '@/components/elements/Toast.vue'
|
||||||
|
|
||||||
|
const container = ref()
|
||||||
|
const toasts = computed(() => allToasts.value)
|
||||||
|
|
||||||
|
function remove(toast_id) {
|
||||||
|
removeToast(toast_id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div ref="container" class="fixed m-3 top-0 right-0">
|
||||||
|
<transition-group name="list" tag="div" class="flex flex-col-reverse gap-2">
|
||||||
|
<Toast
|
||||||
|
v-for="toast of toasts"
|
||||||
|
:key="toast.id"
|
||||||
|
:id="toast.id"
|
||||||
|
:title="toast.title"
|
||||||
|
:message="toast.message"
|
||||||
|
:variant="toast.variant"
|
||||||
|
:confirm="toast.confirm"
|
||||||
|
:confirmCb="toast.confirmCb"
|
||||||
|
@close="remove(toast.id)"
|
||||||
|
/>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.list-enter-active,
|
||||||
|
.list-leave-active {
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.list-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
.list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(10px);
|
||||||
|
}
|
||||||
|
</style>
|
56
src/components/elements/ToggleSwitch.vue
Normal file
56
src/components/elements/ToggleSwitch.vue
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label class="grid cursor-pointer place-items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle theme-controller bg-slate-500 col-span-2 col-start-1 row-start-1 [--tglbg:#e2e8f0] dark:[--tglbg:#1d232a]"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toggle {
|
||||||
|
--animation-input: 0.2s;
|
||||||
|
--handleoffset: 1.5rem;
|
||||||
|
--handleoffsetcalculator: calc(var(--handleoffset) * -1);
|
||||||
|
--togglehandleborder: 0 0;
|
||||||
|
@apply h-6 w-12 rounded-3xl cursor-pointer appearance-none border border-current bg-current;
|
||||||
|
transition:
|
||||||
|
background,
|
||||||
|
box-shadow var(--animation-input, 0.2s) ease-out;
|
||||||
|
box-shadow:
|
||||||
|
var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset,
|
||||||
|
0 0 0 2px var(--tglbg) inset,
|
||||||
|
var(--togglehandleborder);
|
||||||
|
&:focus-visible {
|
||||||
|
@apply outline outline-2 outline-offset-2;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
@apply bg-current;
|
||||||
|
}
|
||||||
|
&:checked,
|
||||||
|
&[aria-checked='true'] {
|
||||||
|
background-image: none;
|
||||||
|
--handleoffsetcalculator: var(--handleoffset);
|
||||||
|
}
|
||||||
|
&:indeterminate {
|
||||||
|
box-shadow:
|
||||||
|
calc(var(--handleoffset) / 2) 0 0 2px var(--tglbg) inset,
|
||||||
|
calc(var(--handleoffset) / -2) 0 0 2px var(--tglbg) inset,
|
||||||
|
0 0 0 2px var(--tglbg) inset;
|
||||||
|
}
|
||||||
|
&:disabled {
|
||||||
|
@apply cursor-not-allowed bg-transparent opacity-30;
|
||||||
|
--togglehandleborder: 0 0 0 3px #000 inset, var(--handleoffsetcalculator) 0 0 3px #000 inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-success {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* backward compatibility */
|
||||||
|
.toggle-mark {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,11 +1,10 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from 'vue'
|
||||||
import { active_exercises as exercises, progresses, userCount, setCompletedState } from "@/socket";
|
import { active_exercises as exercises, progresses, userCount, setCompletedState } from '@/socket'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
import { faCheck, faTimes, faMedal, faHourglassHalf } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck, faTimes, faMedal, faHourglassHalf } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faCircleCheck } from '@fortawesome/free-regular-svg-icons'
|
import { faCircleCheck } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { darkModeEnabled } from "@/settings.js"
|
import { darkModeEnabled } from '@/settings.js'
|
||||||
import LiveLogsUserActivityGraph from "../LiveLogsUserActivityGraph.vue"
|
import LiveLogsUserActivityGraph from '../LiveLogsUserActivityGraph.vue'
|
||||||
|
|
||||||
const props = defineProps(['exercise', 'exercise_index'])
|
const props = defineProps(['exercise', 'exercise_index'])
|
||||||
const collapsed_panels = ref([])
|
const collapsed_panels = ref([])
|
||||||
|
@ -21,8 +20,8 @@
|
||||||
animations: {
|
animations: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
easing: 'easeinout',
|
easing: 'easeinout',
|
||||||
speed: 200,
|
speed: 200
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
colors: [darkModeEnabled.value ? '#008ffb' : '#1f9eff'],
|
colors: [darkModeEnabled.value ? '#008ffb' : '#1f9eff'],
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
|
@ -57,16 +56,16 @@
|
||||||
dataLabels: {
|
dataLabels: {
|
||||||
show: true,
|
show: true,
|
||||||
name: {
|
name: {
|
||||||
show: false,
|
show: false
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
formatter: function (val) {
|
formatter: function (val) {
|
||||||
return parseInt(val*userCount.value / 100);
|
return parseInt((val * userCount.value) / 100)
|
||||||
},
|
},
|
||||||
offsetY: 7,
|
offsetY: 7,
|
||||||
color: darkModeEnabled.value ? '#cbd5e1' : '#f1f5f9',
|
color: darkModeEnabled.value ? '#cbd5e1' : '#f1f5f9',
|
||||||
fontSize: '1.25rem',
|
fontSize: '1.25rem',
|
||||||
show: true,
|
show: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,8 +76,8 @@
|
||||||
colors: [darkModeEnabled.value ? '#008ffb' : '#1f9eff'],
|
colors: [darkModeEnabled.value ? '#008ffb' : '#1f9eff'],
|
||||||
labels: ['Progress'],
|
labels: ['Progress'],
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false,
|
enabled: false
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -95,63 +94,69 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const compactGrid = computed(() => { return userCount.value > 70 })
|
const compactGrid = computed(() => {
|
||||||
const ultraCompactGrid = computed(() => { return userCount.value > 100 })
|
return userCount.value > 70
|
||||||
|
})
|
||||||
|
const ultraCompactGrid = computed(() => {
|
||||||
|
return userCount.value > 100
|
||||||
|
})
|
||||||
const hasProgress = computed(() => Object.keys(progresses.value).length > 0)
|
const hasProgress = computed(() => Object.keys(progresses.value).length > 0)
|
||||||
const sortedProgress = computed(() => Object.values(progresses.value).sort((a, b) => {
|
const sortedProgress = computed(() =>
|
||||||
|
Object.values(progresses.value).sort((a, b) => {
|
||||||
if (a.email < b.email) {
|
if (a.email < b.email) {
|
||||||
return -1;
|
return -1
|
||||||
}
|
}
|
||||||
if (a.email > b.email) {
|
if (a.email > b.email) {
|
||||||
return 1;
|
return 1
|
||||||
}
|
}
|
||||||
return 0;
|
return 0
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const taskCompletionPercentages = computed(() => {
|
const taskCompletionPercentages = computed(() => {
|
||||||
const completions = {}
|
const completions = {}
|
||||||
Object.values(props.exercise.tasks).forEach(task => {
|
Object.values(props.exercise.tasks).forEach((task) => {
|
||||||
completions[task.uuid] = 0
|
completions[task.uuid] = 0
|
||||||
})
|
})
|
||||||
|
|
||||||
sortedProgress.value.forEach(progress => {
|
sortedProgress.value.forEach((progress) => {
|
||||||
for (const [taskUuid, taskCompletion] of Object.entries(progress.exercises[props.exercise.uuid].tasks_completion)) {
|
for (const [taskUuid, taskCompletion] of Object.entries(
|
||||||
|
progress.exercises[props.exercise.uuid].tasks_completion
|
||||||
|
)) {
|
||||||
if (taskCompletion !== false) {
|
if (taskCompletion !== false) {
|
||||||
completions[taskUuid] += 1
|
completions[taskUuid] += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
for (const [taskUuid, taskCompletionSum] of Object.entries(completions)) {
|
for (const [taskUuid, taskCompletionSum] of Object.entries(completions)) {
|
||||||
completions[taskUuid] = 100 * (taskCompletionSum / userCount.value)
|
completions[taskUuid] = 100 * (taskCompletionSum / userCount.value)
|
||||||
}
|
}
|
||||||
return completions
|
return completions
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="
|
|
||||||
fixed inset-2 z-40 h-100 overflow-x-hidden
|
|
||||||
rounded-lg bg-slate-300 dark:bg-slate-800 border border-slate-400 dark:border-slate-800
|
|
||||||
">
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="
|
class="fixed inset-2 z-40 h-100 overflow-x-hidden rounded-lg bg-slate-300 dark:bg-slate-800 border border-slate-400 dark:border-slate-800"
|
||||||
rounded-t-lg text-md p-3 pl-6 text-center
|
>
|
||||||
dark:bg-blue-800 bg-blue-500 dark:text-slate-300 text-slate-100
|
<div
|
||||||
"
|
class="rounded-t-lg text-md p-3 pl-6 text-center dark:bg-blue-800 bg-blue-500 dark:text-slate-300 text-slate-100"
|
||||||
>
|
>
|
||||||
<!-- Modal header -->
|
<!-- Modal header -->
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<span class="text-lg font-semibold">{{ exercise.name }}</span>
|
<span class="text-lg font-semibold">{{ exercise.name }}</span>
|
||||||
<span class="mr-8">
|
<span class="mr-8">
|
||||||
Level: <span :class="{
|
Level:
|
||||||
|
<span
|
||||||
|
:class="{
|
||||||
'rounded-lg px-1 ml-2': true,
|
'rounded-lg px-1 ml-2': true,
|
||||||
'dark:bg-sky-400 bg-sky-400 text-neutral-950': exercise.level == 'beginner',
|
'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-orange-400 bg-orange-400 text-neutral-950': exercise.level == 'advanced',
|
||||||
'dark:bg-red-600 bg-red-600 text-neutral-950': exercise.level == 'expert',
|
'dark:bg-red-600 bg-red-600 text-neutral-950': exercise.level == 'expert'
|
||||||
}">{{ exercise.level }}</span>
|
}"
|
||||||
|
>{{ exercise.level }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -166,11 +171,17 @@
|
||||||
:title="task.description"
|
:title="task.description"
|
||||||
>
|
>
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span class="text-center font-normal text-sm dark:text-blue-200 text-slate-800 text-nowrap">Task {{ task_index + 1 }}</span>
|
<span
|
||||||
|
class="text-center font-normal text-sm dark:text-blue-200 text-slate-800 text-nowrap"
|
||||||
|
>Task {{ task_index + 1 }}</span
|
||||||
|
>
|
||||||
<i class="text-center leading-4 text-slate-600 dark:text-slate-400">{{ task.name }}</i>
|
<i class="text-center leading-4 text-slate-600 dark:text-slate-400">{{ task.name }}</i>
|
||||||
<span class="inline-block h-18 -mt-4 mx-auto">
|
<span class="inline-block h-18 -mt-4 mx-auto">
|
||||||
<apexchart
|
<apexchart
|
||||||
ref="theChart" class="" height="120" width="100"
|
ref="theChart"
|
||||||
|
class=""
|
||||||
|
height="120"
|
||||||
|
width="100"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
:series="[taskCompletionPercentages[task.uuid]]"
|
:series="[taskCompletionPercentages[task.uuid]]"
|
||||||
></apexchart>
|
></apexchart>
|
||||||
|
@ -182,25 +193,40 @@
|
||||||
<!-- User grid -->
|
<!-- User grid -->
|
||||||
<div :class="`flex flex-wrap ${compactGrid ? 'gap-1' : 'gap-2'}`">
|
<div :class="`flex flex-wrap ${compactGrid ? 'gap-1' : 'gap-2'}`">
|
||||||
<span
|
<span
|
||||||
v-for="(progress) in sortedProgress"
|
v-for="progress in sortedProgress"
|
||||||
:key="progress.user_id"
|
:key="progress.user_id"
|
||||||
:class="[
|
:class="[
|
||||||
'bg-slate-200 dark:bg-slate-900 rounded border drop-shadow-lg',
|
'bg-slate-200 dark:bg-slate-900 rounded border drop-shadow-lg',
|
||||||
progress.exercises[exercise.uuid].score / progress.exercises[exercise.uuid].max_score == 1 ? 'border-green-700' : 'border-slate-700',
|
progress.exercises[exercise.uuid].score / progress.exercises[exercise.uuid].max_score ==
|
||||||
|
1
|
||||||
|
? 'border-green-700'
|
||||||
|
: 'border-slate-700'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<span class="
|
<span class="flex p-2 mb-1 text-slate-600 dark:text-slate-400">
|
||||||
flex p-2 mb-1
|
<span
|
||||||
text-slate-600 dark:text-slate-400
|
:class="`flex flex-col ${compactGrid ? 'w-[120px]' : 'w-60'} ${compactGrid ? '' : 'mb-1'}`"
|
||||||
">
|
>
|
||||||
<span :class="`flex flex-col ${compactGrid ? 'w-[120px]' : 'w-60'} ${compactGrid ? '' : 'mb-1'}`">
|
<span
|
||||||
<span :title="progress.user_id" class="text-nowrap inline-block leading-5 truncate mb-1">
|
:title="progress.user_id"
|
||||||
|
class="text-nowrap inline-block leading-5 truncate mb-1"
|
||||||
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
v-if="progress.exercises[exercise.uuid].score / progress.exercises[exercise.uuid].max_score == 1"
|
v-if="
|
||||||
:icon="faMedal" class="mr-1 text-amber-300"
|
progress.exercises[exercise.uuid].score /
|
||||||
|
progress.exercises[exercise.uuid].max_score ==
|
||||||
|
1
|
||||||
|
"
|
||||||
|
:icon="faMedal"
|
||||||
|
class="mr-1 text-amber-300"
|
||||||
></FontAwesomeIcon>
|
></FontAwesomeIcon>
|
||||||
<span :class="`${compactGrid ? 'text-base' : 'text-lg'} font-bold font-mono leading-5 tracking-tight`">{{ progress.email.split('@')[0] }}</span>
|
<span
|
||||||
<span :class="`${compactGrid ? 'text-xs' : 'text-xs'} font-mono tracking-tight`">@{{ progress.email.split('@')[1] }}</span>
|
:class="`${compactGrid ? 'text-base' : 'text-lg'} font-bold font-mono leading-5 tracking-tight`"
|
||||||
|
>{{ progress.email.split('@')[0] }}</span
|
||||||
|
>
|
||||||
|
<span :class="`${compactGrid ? 'text-xs' : 'text-xs'} font-mono tracking-tight`"
|
||||||
|
>@{{ progress.email.split('@')[1] }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<LiveLogsUserActivityGraph
|
<LiveLogsUserActivityGraph
|
||||||
:user_id="progress.user_id"
|
:user_id="progress.user_id"
|
||||||
|
@ -210,26 +236,40 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="
|
<span class="flex flex-row justify-between px-2 text-slate-500 dark:text-slate-400">
|
||||||
flex flex-row justify-between px-2
|
|
||||||
text-slate-500 dark:text-slate-400
|
|
||||||
">
|
|
||||||
<span
|
<span
|
||||||
v-for="(task, task_index) in exercise.tasks"
|
v-for="(task, task_index) in exercise.tasks"
|
||||||
:key="task_index"
|
:key="task_index"
|
||||||
class="select-none cursor-pointer"
|
class="select-none cursor-pointer"
|
||||||
@click="toggleCompleted(progress.exercises[exercise.uuid].tasks_completion[task.uuid], progress.user_id, exercise.uuid, task.uuid)"
|
@click="
|
||||||
|
toggleCompleted(
|
||||||
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid],
|
||||||
|
progress.user_id,
|
||||||
|
exercise.uuid,
|
||||||
|
task.uuid
|
||||||
|
)
|
||||||
|
"
|
||||||
:title="task.name"
|
:title="task.name"
|
||||||
>
|
>
|
||||||
<span class="text-nowrap">
|
<span class="text-nowrap">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
v-if="progress.exercises[exercise.uuid].tasks_completion[task.uuid]"
|
v-if="progress.exercises[exercise.uuid].tasks_completion[task.uuid]"
|
||||||
:icon="(progress.exercises[exercise.uuid].tasks_completion[task.uuid] && progress.exercises[exercise.uuid].tasks_completion[task.uuid].first_completion) ? faCircleCheck : faCheck"
|
:icon="
|
||||||
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid] &&
|
||||||
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid].first_completion
|
||||||
|
? faCircleCheck
|
||||||
|
: faCheck
|
||||||
|
"
|
||||||
:class="`${compactGrid ? 'text-xs' : 'text-xl'} dark:text-green-400 text-green-600`"
|
:class="`${compactGrid ? 'text-xs' : 'text-xl'} dark:text-green-400 text-green-600`"
|
||||||
fixed-width
|
fixed-width
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
v-else-if="task.requirements?.inject_uuid !== undefined && !progress.exercises[exercise.uuid].tasks_completion[task.requirements.inject_uuid]"
|
v-else-if="
|
||||||
|
task.requirements?.inject_uuid !== undefined &&
|
||||||
|
!progress.exercises[exercise.uuid].tasks_completion[
|
||||||
|
task.requirements.inject_uuid
|
||||||
|
]
|
||||||
|
"
|
||||||
title="All requirements for that task haven't been fullfilled yet"
|
title="All requirements for that task haven't been fullfilled yet"
|
||||||
:icon="faHourglassHalf"
|
:icon="faHourglassHalf"
|
||||||
:class="`${compactGrid ? 'text-xs' : 'text-lg'} dark:text-slate-500 text-slate-400`"
|
:class="`${compactGrid ? 'text-xs' : 'text-lg'} dark:text-slate-500 text-slate-400`"
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from 'vue'
|
||||||
import { active_exercises as exercises, progresses, userCount, setCompletedState } from "@/socket";
|
import { active_exercises as exercises, progresses, userCount, setCompletedState } from '@/socket'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
import { faCheck, faTimes, faMedal, faHourglassHalf } from '@fortawesome/free-solid-svg-icons'
|
import { faCheck, faTimes, faMedal, faHourglassHalf } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faCircleCheck } from '@fortawesome/free-regular-svg-icons'
|
import { faCircleCheck } from '@fortawesome/free-regular-svg-icons'
|
||||||
import LiveLogsUserActivityGraph from "../LiveLogsUserActivityGraph.vue"
|
import LiveLogsUserActivityGraph from '../LiveLogsUserActivityGraph.vue'
|
||||||
|
|
||||||
const props = defineProps(['exercise', 'exercise_index'])
|
const props = defineProps(['exercise', 'exercise_index'])
|
||||||
const collapsed_panels = ref([])
|
const collapsed_panels = ref([])
|
||||||
|
@ -22,62 +21,76 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const compactTable = computed(() => { return userCount.value > 20 })
|
const compactTable = computed(() => {
|
||||||
|
return userCount.value > 20
|
||||||
|
})
|
||||||
const hasProgress = computed(() => Object.keys(progresses.value).length > 0)
|
const hasProgress = computed(() => Object.keys(progresses.value).length > 0)
|
||||||
const sortedProgress = computed(() => Object.values(progresses.value).sort((a, b) => {
|
const sortedProgress = computed(() =>
|
||||||
|
Object.values(progresses.value).sort((a, b) => {
|
||||||
if (a.email < b.email) {
|
if (a.email < b.email) {
|
||||||
return -1;
|
return -1
|
||||||
}
|
}
|
||||||
if (a.email > b.email) {
|
if (a.email > b.email) {
|
||||||
return 1;
|
return 1
|
||||||
}
|
}
|
||||||
return 0;
|
return 0
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const taskCompletionPercentages = computed(() => {
|
const taskCompletionPercentages = computed(() => {
|
||||||
const completions = {}
|
const completions = {}
|
||||||
Object.values(props.exercise.tasks).forEach(task => {
|
Object.values(props.exercise.tasks).forEach((task) => {
|
||||||
completions[task.uuid] = 0
|
completions[task.uuid] = 0
|
||||||
})
|
})
|
||||||
|
|
||||||
sortedProgress.value.forEach(progress => {
|
sortedProgress.value.forEach((progress) => {
|
||||||
for (const [taskUuid, taskCompletion] of Object.entries(progress.exercises[props.exercise.uuid].tasks_completion)) {
|
for (const [taskUuid, taskCompletion] of Object.entries(
|
||||||
|
progress.exercises[props.exercise.uuid].tasks_completion
|
||||||
|
)) {
|
||||||
if (taskCompletion !== false) {
|
if (taskCompletion !== false) {
|
||||||
completions[taskUuid] += 1
|
completions[taskUuid] += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
for (const [taskUuid, taskCompletionSum] of Object.entries(completions)) {
|
for (const [taskUuid, taskCompletionSum] of Object.entries(completions)) {
|
||||||
completions[taskUuid] = 100 * (taskCompletionSum / userCount.value)
|
completions[taskUuid] = 100 * (taskCompletionSum / userCount.value)
|
||||||
}
|
}
|
||||||
return completions
|
return completions
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<table
|
<table class="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full mb-4">
|
||||||
class="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full mb-4"
|
|
||||||
>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr @click="collapse(exercise_index)" class="cursor-pointer">
|
<tr @click="collapse(exercise_index)" class="cursor-pointer">
|
||||||
<th :colspan="2 + exercise.tasks.length" class="rounded-tl-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">
|
<th
|
||||||
|
:colspan="2 + exercise.tasks.length"
|
||||||
|
class="rounded-tl-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">
|
<div class="flex justify-between items-center">
|
||||||
<span class="dark:text-blue-200 text-slate-200"># {{ exercise_index + 1 }}</span>
|
<span class="dark:text-blue-200 text-slate-200"># {{ exercise_index + 1 }}</span>
|
||||||
<span class="text-lg">{{ exercise.name }}</span>
|
<span class="text-lg">{{ exercise.name }}</span>
|
||||||
<span class="">
|
<span class="">
|
||||||
Level: <span :class="{
|
Level:
|
||||||
|
<span
|
||||||
|
:class="{
|
||||||
'rounded-lg px-1 ml-2': true,
|
'rounded-lg px-1 ml-2': true,
|
||||||
'dark:bg-sky-400 bg-sky-400 text-neutral-950': exercise.level == 'beginner',
|
'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-orange-400 bg-orange-400 text-neutral-950': exercise.level == 'advanced',
|
||||||
'dark:bg-red-600 bg-red-600 text-neutral-950': exercise.level == 'expert',
|
'dark:bg-red-600 bg-red-600 text-neutral-950': exercise.level == 'expert'
|
||||||
}">{{ exercise.level }}</span>
|
}"
|
||||||
|
>{{ exercise.level }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr :class="`font-medium text-slate-600 dark:text-slate-200 ${collapsed_panels.includes(exercise_index) ? 'hidden' : ''}`">
|
<tr
|
||||||
|
:class="`font-medium text-slate-600 dark:text-slate-200 ${
|
||||||
|
collapsed_panels.includes(exercise_index) ? 'hidden' : ''
|
||||||
|
}`"
|
||||||
|
>
|
||||||
<th class="border-b border-slate-100 dark:border-slate-700 p-3 pl-6 text-left">User</th>
|
<th class="border-b border-slate-100 dark:border-slate-700 p-3 pl-6 text-left">User</th>
|
||||||
<th
|
<th
|
||||||
v-for="(task, task_index) in exercise.tasks"
|
v-for="(task, task_index) in exercise.tasks"
|
||||||
|
@ -86,12 +99,17 @@
|
||||||
:title="task.description"
|
:title="task.description"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-center font-normal text-sm dark:text-blue-200 text-slate-500 text-nowrap">Task {{ task_index + 1 }}</span>
|
<span
|
||||||
|
class="text-center font-normal text-sm dark:text-blue-200 text-slate-500 text-nowrap"
|
||||||
|
>Task {{ task_index + 1 }}</span
|
||||||
|
>
|
||||||
<i class="text-center">{{ task.name }}</i>
|
<i class="text-center">{{ task.name }}</i>
|
||||||
<div
|
<div
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
class="flex w-full h-1 bg-gray-200 rounded-full overflow-hidden dark:bg-neutral-600"
|
class="flex w-full h-1 bg-gray-200 rounded-full overflow-hidden dark:bg-neutral-600"
|
||||||
:aria-valuenow="taskCompletionPercentages[task.uuid]" :aria-valuemin="0" aria-valuemax="100"
|
:aria-valuenow="taskCompletionPercentages[task.uuid]"
|
||||||
|
:aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
:title="`${taskCompletionPercentages[task.uuid].toFixed(0)}%`"
|
:title="`${taskCompletionPercentages[task.uuid].toFixed(0)}%`"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -114,13 +132,31 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<tr v-for="(progress) in sortedProgress" :key="progress.user_id" class="bg-slate-100 dark:bg-slate-900">
|
<tr
|
||||||
<td class="border-b border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-0 pl-2 relative">
|
v-for="progress in sortedProgress"
|
||||||
|
:key="progress.user_id"
|
||||||
|
class="bg-slate-100 dark:bg-slate-900"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="border-b border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 p-0 pl-2 relative"
|
||||||
|
>
|
||||||
<span class="flex flex-col max-w-60">
|
<span class="flex flex-col max-w-60">
|
||||||
<span :title="progress.user_id" class="text-nowrap inline-block leading-5 truncate">
|
<span :title="progress.user_id" class="text-nowrap inline-block leading-5 truncate">
|
||||||
<FontAwesomeIcon v-if="progress.exercises[exercise.uuid].score / progress.exercises[exercise.uuid].max_score == 1" :icon="faMedal" class="mr-1 text-amber-300"></FontAwesomeIcon>
|
<FontAwesomeIcon
|
||||||
<span class="text-lg font-bold font-mono leading-5 tracking-tight">{{ progress.email.split('@')[0] }}</span>
|
v-if="
|
||||||
<span class="text-xs font-mono tracking-tight">@{{ progress.email.split('@')[1] }}</span>
|
progress.exercises[exercise.uuid].score /
|
||||||
|
progress.exercises[exercise.uuid].max_score ==
|
||||||
|
1
|
||||||
|
"
|
||||||
|
:icon="faMedal"
|
||||||
|
class="mr-1 text-amber-300"
|
||||||
|
></FontAwesomeIcon>
|
||||||
|
<span class="text-lg font-bold font-mono leading-5 tracking-tight">{{
|
||||||
|
progress.email.split('@')[0]
|
||||||
|
}}</span>
|
||||||
|
<span class="text-xs font-mono tracking-tight"
|
||||||
|
>@{{ progress.email.split('@')[1] }}</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<LiveLogsUserActivityGraph
|
<LiveLogsUserActivityGraph
|
||||||
:user_id="progress.user_id"
|
:user_id="progress.user_id"
|
||||||
|
@ -131,51 +167,117 @@
|
||||||
<td
|
<td
|
||||||
v-for="(task, task_index) in exercise.tasks"
|
v-for="(task, task_index) in exercise.tasks"
|
||||||
:key="task_index"
|
:key="task_index"
|
||||||
:class="`text-center border-b border-slate-200 dark:border-slate-700 text-slate-500 dark:text-slate-400 ${compactTable ? 'p-0' : 'p-2'}`"
|
:class="`text-center border-b border-slate-200 dark:border-slate-700 text-slate-500 dark:text-slate-400 ${
|
||||||
|
compactTable ? 'p-0' : 'p-2'
|
||||||
|
}`"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="select-none cursor-pointer flex justify-center content-center flex-wrap h-9"
|
class="select-none cursor-pointer flex justify-center content-center flex-wrap h-9"
|
||||||
@click="toggleCompleted(progress.exercises[exercise.uuid].tasks_completion[task.uuid], progress.user_id, exercise.uuid, task.uuid)"
|
@click="
|
||||||
|
toggleCompleted(
|
||||||
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid],
|
||||||
|
progress.user_id,
|
||||||
|
exercise.uuid,
|
||||||
|
task.uuid
|
||||||
|
)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span class="text-nowrap">
|
<span class="text-nowrap">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
v-if="progress.exercises[exercise.uuid].tasks_completion[task.uuid]"
|
v-if="progress.exercises[exercise.uuid].tasks_completion[task.uuid]"
|
||||||
:icon="progress.exercises[exercise.uuid].tasks_completion[task.uuid].first_completion ? faCircleCheck : faCheck"
|
:icon="
|
||||||
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid].first_completion
|
||||||
|
? faCircleCheck
|
||||||
|
: faCheck
|
||||||
|
"
|
||||||
:class="`
|
:class="`
|
||||||
${progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? 'dark:text-green-400 text-green-600' : 'dark:text-slate-500 text-slate-400'}
|
${
|
||||||
${progress.exercises[exercise.uuid].tasks_completion[task.uuid].first_completion ? 'text-lg' : 'text-xl'}
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid]
|
||||||
|
? 'dark:text-green-400 text-green-600'
|
||||||
|
: 'dark:text-slate-500 text-slate-400'
|
||||||
|
}
|
||||||
|
${
|
||||||
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid]
|
||||||
|
.first_completion
|
||||||
|
? 'text-lg'
|
||||||
|
: 'text-xl'
|
||||||
|
}
|
||||||
`"
|
`"
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
v-else-if="task.requirements?.inject_uuid !== undefined && !progress.exercises[exercise.uuid].tasks_completion[task.requirements.inject_uuid]"
|
v-else-if="
|
||||||
|
task.requirements?.inject_uuid !== undefined &&
|
||||||
|
!progress.exercises[exercise.uuid].tasks_completion[
|
||||||
|
task.requirements.inject_uuid
|
||||||
|
]
|
||||||
|
"
|
||||||
title="All requirements for that task haven't been fullfilled yet"
|
title="All requirements for that task haven't been fullfilled yet"
|
||||||
:icon="faHourglassHalf"
|
:icon="faHourglassHalf"
|
||||||
:class="`text-lg ${progress.exercises[exercise.uuid].tasks_completion[task.uuid] ? 'dark:text-green-400 text-green-600' : 'dark:text-slate-500 text-slate-400'}`"
|
:class="`text-lg ${
|
||||||
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid]
|
||||||
|
? 'dark:text-green-400 text-green-600'
|
||||||
|
: 'dark:text-slate-500 text-slate-400'
|
||||||
|
}`"
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
v-else
|
v-else
|
||||||
:icon="faTimes"
|
:icon="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'}`"
|
: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>
|
<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>
|
</span>
|
||||||
<span :class="['leading-3', !compactTable ? 'text-sm' : 'text-xs']">
|
<span :class="['leading-3', !compactTable ? 'text-sm' : 'text-xs']">
|
||||||
<span
|
<span
|
||||||
v-if="progress.exercises[exercise.uuid].tasks_completion[task.uuid].timestamp"
|
v-if="progress.exercises[exercise.uuid].tasks_completion[task.uuid].timestamp"
|
||||||
:class="progress.exercises[exercise.uuid].tasks_completion[task.uuid].first_completion ? 'font-bold' : 'font-extralight'"
|
:class="
|
||||||
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid].first_completion
|
||||||
|
? 'font-bold'
|
||||||
|
: 'font-extralight'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ (new Date(progress.exercises[exercise.uuid].tasks_completion[task.uuid].timestamp * 1000)).toTimeString().split(' ', 1)[0] }}
|
{{
|
||||||
|
new Date(
|
||||||
|
progress.exercises[exercise.uuid].tasks_completion[task.uuid].timestamp *
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
.toTimeString()
|
||||||
|
.split(' ', 1)[0]
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="border-b border-slate-200 dark:border-slate-700 text-slate-500 dark:text-slate-400 p-3">
|
<td
|
||||||
<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].score" :aria-valuemin="0" aria-valuemax="100">
|
class="border-b border-slate-200 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].score"
|
||||||
|
:aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
>
|
||||||
<div
|
<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"
|
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: ${100 * (progress.exercises[exercise.uuid].score / progress.exercises[exercise.uuid].max_score)}%`"
|
:style="`width: ${
|
||||||
|
100 *
|
||||||
|
(progress.exercises[exercise.uuid].score /
|
||||||
|
progress.exercises[exercise.uuid].max_score)
|
||||||
|
}%`"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
13
src/main.js
13
src/main.js
|
@ -1,10 +1,19 @@
|
||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
import VueApexCharts from "vue3-apexcharts";
|
import VueApexCharts from 'vue3-apexcharts'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp, ref } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
import Modal from '@/components/elements/Modal.vue'
|
||||||
|
import Loading from '@/components/elements/Loading.vue'
|
||||||
|
import Alert from '@/components/elements/Alert.vue'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
app.component('FontAwesomeIcon', FontAwesomeIcon)
|
||||||
|
app.component('Modal', Modal)
|
||||||
|
app.component('Loading', Loading)
|
||||||
|
app.component('Alert', Alert)
|
||||||
app.use(VueApexCharts)
|
app.use(VueApexCharts)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
59
src/router.js
Normal file
59
src/router.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { createWebHistory, createRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ path: '/', component: ScenarioList },
|
||||||
|
{
|
||||||
|
path: '/scenarios/index',
|
||||||
|
name: 'Scenario Index',
|
||||||
|
component: ScenarioList,
|
||||||
|
meta: { requiresScenarioSelection: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/scenarios/add',
|
||||||
|
name: 'New Scenario',
|
||||||
|
component: ScenarioNew,
|
||||||
|
meta: { requiresScenarioSelection: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/scenarios/overview/:uuid?',
|
||||||
|
name: 'Scenario Overview',
|
||||||
|
component: ScenarioOverview,
|
||||||
|
meta: { requiresScenarioSelection: true },
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/scenarios/designer/:uuid?',
|
||||||
|
name: 'Scenario Designer',
|
||||||
|
component: ScenarioDesigner,
|
||||||
|
meta: { requiresScenarioSelection: true },
|
||||||
|
props: true
|
||||||
|
},
|
||||||
|
{ path: '/injects/tester/:uuid?', name: 'Inject Tester', component: InjectTester, props: true }
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from) => {
|
||||||
|
if (to.path === '/') {
|
||||||
|
return { path: '/scenarios/index' }
|
||||||
|
}
|
||||||
|
if (!hasScenarios()) {
|
||||||
|
fetchScenarios()
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
from.name == undefined &&
|
||||||
|
['Scenario Overview', 'Scenario Designer'].includes(to.name) &&
|
||||||
|
to?.params?.uuid !== undefined
|
||||||
|
) {
|
||||||
|
store.selected_scenario = to.params.uuid
|
||||||
|
}
|
||||||
|
if (to?.meta?.requiresScenarioSelection === true && store.selected_scenario === null) {
|
||||||
|
return { path: '/scenarios/index' }
|
||||||
|
}
|
||||||
|
if (to.matched.length == 0) {
|
||||||
|
return { path: '/scenarios/index' }
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,9 +1,9 @@
|
||||||
import { reactive, computed } from "vue";
|
import { reactive, computed } from 'vue'
|
||||||
import { io } from "socket.io-client";
|
import { io } from 'socket.io-client'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
|
||||||
// "undefined" means the URL will be computed from the `window.location` object
|
// "undefined" means the URL will be computed from the `window.location` object
|
||||||
const URL = process.env.NODE_ENV === "production" ? undefined : "http://localhost:40001";
|
const URL = process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:40001'
|
||||||
const MAX_LIVE_LOG = 30
|
const MAX_LIVE_LOG = 30
|
||||||
|
|
||||||
const initial_state = {
|
const initial_state = {
|
||||||
|
@ -17,27 +17,27 @@ const initial_state = {
|
||||||
exercises: [],
|
exercises: [],
|
||||||
selected_exercises: [],
|
selected_exercises: [],
|
||||||
progresses: {},
|
progresses: {},
|
||||||
diagnostic: {},
|
diagnostic: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive({ ...initial_state });
|
const state = reactive({ ...initial_state })
|
||||||
const connectionState = reactive({
|
const connectionState = reactive({
|
||||||
connected: false,
|
connected: false,
|
||||||
zmq_last_time: false,
|
zmq_last_time: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const socket = io(URL, {
|
const socket = io(URL, {
|
||||||
autoConnect: true
|
autoConnect: true
|
||||||
});
|
})
|
||||||
|
|
||||||
|
|
||||||
/* Public */
|
/* Public */
|
||||||
/* ------ */
|
/* ------ */
|
||||||
|
|
||||||
export const exercises = computed(() => state.exercises)
|
export const exercises = computed(() => state.exercises)
|
||||||
export const selected_exercises = computed(() => state.selected_exercises)
|
export const selected_exercises = computed(() => state.selected_exercises)
|
||||||
export const active_exercises = computed(() => state.exercises.filter((exercise) => state.selected_exercises.includes(exercise.uuid)))
|
export const active_exercises = computed(() =>
|
||||||
|
state.exercises.filter((exercise) => state.selected_exercises.includes(exercise.uuid))
|
||||||
|
)
|
||||||
export const progresses = computed(() => state.progresses)
|
export const progresses = computed(() => state.progresses)
|
||||||
export const notifications = computed(() => state.notificationEvents)
|
export const notifications = computed(() => state.notificationEvents)
|
||||||
export const notificationCounter = computed(() => state.notificationCounter)
|
export const notificationCounter = computed(() => state.notificationCounter)
|
||||||
|
@ -52,7 +52,7 @@ export const socketConnected = computed(() => connectionState.connected)
|
||||||
export const zmqLastTime = computed(() => connectionState.zmq_last_time)
|
export const zmqLastTime = computed(() => connectionState.zmq_last_time)
|
||||||
|
|
||||||
export function resetState() {
|
export function resetState() {
|
||||||
Object.assign(state, initial_state);
|
Object.assign(state, initial_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fullReload() {
|
export function fullReload() {
|
||||||
|
@ -67,7 +67,7 @@ export function setCompletedState(completed, user_id, exec_uuid, task_uuid) {
|
||||||
const payload = {
|
const payload = {
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
exercise_uuid: exec_uuid,
|
exercise_uuid: exec_uuid,
|
||||||
task_uuid: task_uuid,
|
task_uuid: task_uuid
|
||||||
}
|
}
|
||||||
sendCompletedState(completed, payload)
|
sendCompletedState(completed, payload)
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ export function resetLiveLogs() {
|
||||||
export function changeExerciseSelection(exec_uuid, state_enabled) {
|
export function changeExerciseSelection(exec_uuid, state_enabled) {
|
||||||
const payload = {
|
const payload = {
|
||||||
exercise_uuid: exec_uuid,
|
exercise_uuid: exec_uuid,
|
||||||
selected: state_enabled,
|
selected: state_enabled
|
||||||
}
|
}
|
||||||
sendChangeExerciseSelection(payload)
|
sendChangeExerciseSelection(payload)
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,8 @@ export function toggleApiQueryMode(enabled) {
|
||||||
export function remediateSetting(setting) {
|
export function remediateSetting(setting) {
|
||||||
sendRemediateSetting(setting, (result) => {
|
sendRemediateSetting(setting, (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
state.diagnostic['settings'][setting].value = state.diagnostic['settings'][setting].expected_value
|
state.diagnostic['settings'][setting].value =
|
||||||
|
state.diagnostic['settings'][setting].expected_value
|
||||||
} else {
|
} else {
|
||||||
state.diagnostic['settings'][setting].error = true
|
state.diagnostic['settings'][setting].error = true
|
||||||
state.diagnostic['settings'][setting].errorMessage = result.message
|
state.diagnostic['settings'][setting].errorMessage = result.message
|
||||||
|
@ -114,76 +115,74 @@ export function remediateSetting(setting) {
|
||||||
export const debouncedGetProgress = debounce(getProgress, 200, { leading: true })
|
export const debouncedGetProgress = debounce(getProgress, 200, { leading: true })
|
||||||
export const debouncedGetDiangostic = debounce(getDiangostic, 1000, { leading: true })
|
export const debouncedGetDiangostic = debounce(getDiangostic, 1000, { leading: true })
|
||||||
|
|
||||||
|
|
||||||
/* Private */
|
/* Private */
|
||||||
/* ------- */
|
/* ------- */
|
||||||
|
|
||||||
function getExercises() {
|
function getExercises() {
|
||||||
socket.emit("get_exercises", (all_exercises) => {
|
socket.emit('get_exercises', (all_exercises) => {
|
||||||
state.exercises = all_exercises
|
state.exercises = all_exercises
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedExercises() {
|
function getSelectedExercises() {
|
||||||
socket.emit("get_selected_exercises", (all_selected_exercises) => {
|
socket.emit('get_selected_exercises', (all_selected_exercises) => {
|
||||||
state.selected_exercises = all_selected_exercises
|
state.selected_exercises = all_selected_exercises
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNotifications() {
|
function getNotifications() {
|
||||||
socket.emit("get_notifications", (all_notifications) => {
|
socket.emit('get_notifications', (all_notifications) => {
|
||||||
state.notificationEvents = all_notifications
|
state.notificationEvents = all_notifications
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProgress() {
|
function getProgress() {
|
||||||
socket.emit("get_progress", (all_progress) => {
|
socket.emit('get_progress', (all_progress) => {
|
||||||
state.progresses = all_progress
|
state.progresses = all_progress
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUsersActivity() {
|
function getUsersActivity() {
|
||||||
socket.emit("get_users_activity", (user_activity_bundle) => {
|
socket.emit('get_users_activity', (user_activity_bundle) => {
|
||||||
state.userActivity = user_activity_bundle.activity
|
state.userActivity = user_activity_bundle.activity
|
||||||
state.userActivityConfig = user_activity_bundle.config
|
state.userActivityConfig = user_activity_bundle.config
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getDiangostic() {
|
function getDiangostic() {
|
||||||
state.diagnostic = {}
|
state.diagnostic = {}
|
||||||
socket.emit("get_diagnostic", (diagnostic) => {
|
socket.emit('get_diagnostic', (diagnostic) => {
|
||||||
state.diagnostic = diagnostic
|
state.diagnostic = diagnostic
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendCompletedState(completed, payload) {
|
function sendCompletedState(completed, payload) {
|
||||||
const event_name = !completed ? "mark_task_completed": "mark_task_incomplete"
|
const event_name = !completed ? 'mark_task_completed' : 'mark_task_incomplete'
|
||||||
socket.emit(event_name, payload, () => {
|
socket.emit(event_name, payload, () => {
|
||||||
getProgress()
|
getProgress()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendResetAllExerciseProgress() {
|
function sendResetAllExerciseProgress() {
|
||||||
socket.emit("reset_all_exercise_progress", () => {
|
socket.emit('reset_all_exercise_progress', () => {
|
||||||
getProgress()
|
getProgress()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendResetAll() {
|
function sendResetAll() {
|
||||||
socket.emit("reset_all", () => {
|
socket.emit('reset_all', () => {
|
||||||
getProgress()
|
getProgress()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendResetLiveLogs() {
|
function sendResetLiveLogs() {
|
||||||
socket.emit("reset_notifications", () => {
|
socket.emit('reset_notifications', () => {
|
||||||
getNotifications()
|
getNotifications()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendChangeExerciseSelection(payload) {
|
function sendChangeExerciseSelection(payload) {
|
||||||
socket.emit("change_exercise_selection", payload, () => {
|
socket.emit('change_exercise_selection', payload, () => {
|
||||||
getSelectedExercises()
|
getSelectedExercises()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -192,64 +191,64 @@ function sendToggleVerboseMode(enabled) {
|
||||||
const payload = {
|
const payload = {
|
||||||
verbose: enabled
|
verbose: enabled
|
||||||
}
|
}
|
||||||
socket.emit("toggle_verbose_mode", payload, () => {})
|
socket.emit('toggle_verbose_mode', payload, () => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendToggleApiQueryMode(enabled) {
|
function sendToggleApiQueryMode(enabled) {
|
||||||
const payload = {
|
const payload = {
|
||||||
apiquery: enabled
|
apiquery: enabled
|
||||||
}
|
}
|
||||||
socket.emit("toggle_apiquery_mode", payload, () => {})
|
socket.emit('toggle_apiquery_mode', payload, () => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendRemediateSetting(setting, cb) {
|
function sendRemediateSetting(setting, cb) {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: setting
|
name: setting
|
||||||
}
|
}
|
||||||
socket.emit("remediate_setting", payload, (result) => {
|
socket.emit('remediate_setting', payload, (result) => {
|
||||||
cb(result)
|
cb(result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Event listener */
|
/* Event listener */
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on('connect', () => {
|
||||||
connectionState.connected = true;
|
connectionState.connected = true
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on('disconnect', () => {
|
||||||
connectionState.connected = false;
|
connectionState.connected = false
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.on("notification", (message) => {
|
socket.on('notification', (message) => {
|
||||||
state.notificationCounter += 1
|
state.notificationCounter += 1
|
||||||
if (message.is_api_request) {
|
if (message.is_api_request) {
|
||||||
state.notificationAPICounter += 1
|
state.notificationAPICounter += 1
|
||||||
}
|
}
|
||||||
addLimited(state.notificationEvents, message, MAX_LIVE_LOG)
|
addLimited(state.notificationEvents, message, MAX_LIVE_LOG)
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.on("new_user", (new_user) => {
|
socket.on('new_user', (new_user) => {
|
||||||
debouncedGetProgress()
|
debouncedGetProgress()
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.on("refresh_score", (new_user) => {
|
socket.on('refresh_score', (new_user) => {
|
||||||
debouncedGetProgress()
|
debouncedGetProgress()
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.on("keep_alive", (keep_alive) => {
|
socket.on('keep_alive', (keep_alive) => {
|
||||||
connectionState.zmq_last_time = keep_alive['zmq_last_time']
|
connectionState.zmq_last_time = keep_alive['zmq_last_time']
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.on("update_notification_history", (notification_history_bundle) => {
|
socket.on('update_notification_history', (notification_history_bundle) => {
|
||||||
state.notificationHistory = notification_history_bundle.history
|
state.notificationHistory = notification_history_bundle.history
|
||||||
state.notificationHistoryConfig = notification_history_bundle.config
|
state.notificationHistoryConfig = notification_history_bundle.config
|
||||||
});
|
})
|
||||||
|
|
||||||
socket.on("update_users_activity", (user_activity_bundle) => {
|
socket.on('update_users_activity', (user_activity_bundle) => {
|
||||||
state.userActivity = user_activity_bundle.activity
|
state.userActivity = user_activity_bundle.activity
|
||||||
state.userActivityConfig = user_activity_bundle.config
|
state.userActivityConfig = user_activity_bundle.config
|
||||||
});
|
})
|
||||||
|
|
||||||
function addLimited(target, message, maxCount) {
|
function addLimited(target, message, maxCount) {
|
||||||
target.unshift(message)
|
target.unshift(message)
|
||||||
|
|
20
src/utils.js
Normal file
20
src/utils.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
let toastID = 0
|
||||||
|
export const toastBuffer = ref([])
|
||||||
|
export const allToasts = computed(() => toastBuffer.value)
|
||||||
|
export function toast(toast) {
|
||||||
|
toastID += 1
|
||||||
|
toast.id = toastID
|
||||||
|
toastBuffer.value.push(toast)
|
||||||
|
}
|
||||||
|
export function removeToast(id) {
|
||||||
|
toastBuffer.value = toastBuffer.value.filter((toast) => toast.id != id)
|
||||||
|
}
|
||||||
|
export function ajaxFeedback(response) {
|
||||||
|
toast({
|
||||||
|
variant: response.success ? 'success' : 'danger',
|
||||||
|
message: String(response.message),
|
||||||
|
title: response.title
|
||||||
|
})
|
||||||
|
}
|
|
@ -8,6 +8,15 @@ export default {
|
||||||
{
|
{
|
||||||
pattern: /bg-blue+/, // Includes bg of all colors and shades
|
pattern: /bg-blue+/, // Includes bg of all colors and shades
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pattern: /bg-(red|green|blue|amber)-(100|200|300|800)/, // Includes bg of all colors and shades
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /text-(red|green|blue|amber)-(100|700|800)/, // Includes bg of all colors and shades
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /border-(red|green|blue|amber)-(700)/, // Includes bg of all colors and shades
|
||||||
|
},
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
@ -18,22 +27,16 @@ export default {
|
||||||
'3xl': '1800px',
|
'3xl': '1800px',
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
'xxs': '0.6rem',
|
'2xs': '0.66rem',
|
||||||
},
|
},
|
||||||
|
maxWidth: {
|
||||||
|
'8xl': '88rem',
|
||||||
|
'9xl': '96rem',
|
||||||
|
'10xl': '104rem',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require('daisyui'),
|
|
||||||
],
|
],
|
||||||
darkMode: ['selector'],
|
darkMode: ['selector'],
|
||||||
daisyui: {
|
|
||||||
themes: false, // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"]
|
|
||||||
darkTheme: "dark", // name of one of the included themes for dark mode
|
|
||||||
base: false, // applies background color and foreground color for root element by default
|
|
||||||
styled: true, // include daisyUI colors and design decisions for all components
|
|
||||||
utils: false, // adds responsive and modifier utility classes
|
|
||||||
prefix: "", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors)
|
|
||||||
logs: false, // Shows info about daisyUI version and used config in the console when building your CSS
|
|
||||||
themeRoot: ":root", // The element that receives theme color CSS variables
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue