Logs wireup (#116)

* wireup

* Added live logs

* Finish up wireup

* Run linter

* finish most

* Fix most issues

* Finish page

* run lint

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Adrian O.V
2023-05-17 16:05:20 -04:00
committed by GitHub
parent c6e2133e15
commit 0801d7a145
10 changed files with 336 additions and 192 deletions

View File

@@ -15,7 +15,7 @@
"dependencies": {
"@tauri-apps/api": "^1.2.0",
"ofetch": "^1.0.1",
"omorphia": "^0.4.10",
"omorphia": "^0.4.13",
"pinia": "^2.0.33",
"vite-svg-loader": "^4.0.0",
"vue": "^3.2.45",

View File

@@ -14,8 +14,17 @@ pub struct Logs {
/// Get all Logs for a profile, sorted by datetime
#[tauri::command]
pub async fn logs_get_logs(profile_uuid: Uuid) -> Result<Vec<Logs>> {
Ok(logs::get_logs(profile_uuid).await?)
pub async fn logs_get_logs(
profile_uuid: Uuid,
clear_contents: Option<bool>,
) -> Result<Vec<Logs>> {
use std::time::Instant;
let now = Instant::now();
let val = logs::get_logs(profile_uuid, clear_contents).await?;
let elapsed = now.elapsed();
println!("Elapsed: {:.2?}", elapsed);
Ok(val)
}
/// Get a Log struct for a profile by profile id and datetime string

View File

@@ -40,6 +40,10 @@ a {
}
}
input {
border: none;
}
.multiselect {
color: var(--color-base) !important;
outline: 2px solid transparent;

View File

@@ -17,8 +17,8 @@ pub struct Logs {
/// Get all logs that exist for a given profile
/// This is returned as an array of Log objects, sorted by datetime_string (the folder name, when the log was created)
export async function get_logs(profileUuid) {
return await invoke('logs_get_logs', { profileUuid })
export async function get_logs(profileUuid, clearContents) {
return await invoke('logs_get_logs', { profileUuid, clearContents })
}
/// Get a profile's log by datetime_string (the folder name, when the log was created)

View File

@@ -63,7 +63,13 @@
</div>
<div class="content">
<Promotion />
<router-view :instance="instance" />
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense @pending="loadingBar.startLoading()" @resolve="loadingBar.stopLoading()">
<component :is="Component" :instance="instance"></component>
</Suspense>
</template>
</RouterView>
</div>
</div>
</template>
@@ -81,7 +87,7 @@ import { useRoute } from 'vue-router'
import { ref, onUnmounted } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { open } from '@tauri-apps/api/dialog'
import { useBreadcrumbs, useSearch } from '@/store/state'
import { useBreadcrumbs, useLoading, useSearch } from '@/store/state'
const route = useRoute()
const searchStore = useSearch()
@@ -96,6 +102,8 @@ breadcrumbs.setContext({
link: route.path,
})
const loadingBar = useLoading()
const uuid = ref(null)
const playing = ref(false)
const loading = ref(false)
@@ -240,15 +248,18 @@ Button {
width: 100%;
color: var(--color-primary);
padding: var(--gap-md);
box-shadow: none;
&.router-link-exact-active {
box-shadow: var(--shadow-inset-lg);
background: var(--color-button-bg);
color: var(--color-contrast);
}
&:hover {
background-color: var(--color-button-bg);
color: var(--color-contrast);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
box-shadow: var(--shadow-inset-lg);
text-decoration: none;
}

View File

@@ -1,92 +1,177 @@
<template>
<Card class="log-card">
<div class="button-row">
<DropdownSelect :options="['logs/latest.log']" />
<DropdownSelect
name="Log date"
:model-value="logs[selectedLogIndex]"
:options="logs"
:display-name="(option) => option?.name"
:disabled="logs.length === 0"
@change="(value) => (selectedLogIndex = value.index)"
/>
<div class="button-group">
<Button>
<ClipboardCopyIcon />
Copy
<Button :disabled="!logs[selectedLogIndex]" @click="copyLog()">
<ClipboardCopyIcon v-if="!copied" />
<CheckIcon v-else />
{{ copied ? 'Copied' : 'Copy' }}
</Button>
<Button color="primary">
<Button disabled color="primary">
<SendIcon />
Share
</Button>
<Button color="danger">
<Button
:disabled="!logs[selectedLogIndex] || logs[selectedLogIndex].live === true"
color="danger"
@click="deleteLog()"
>
<TrashIcon />
Delete
</Button>
</div>
</div>
<div class="log-text">
<div v-for="(line, index) in fileContents.value.split('\n')" :key="index">{{ line }}</div>
<div ref="logContainer" class="log-text">
<!-- {{ logs[1] }}-->
<div v-for="line in logs[selectedLogIndex]?.stdout.split('\n')" :key="line" class="no-wrap">
{{ line }}
</div>
</div>
</Card>
</template>
<script setup>
import { Card, Button, TrashIcon, SendIcon, ClipboardCopyIcon, DropdownSelect } from 'omorphia'
</script>
<script>
export default {
data() {
return {
fileContents: {
value:
"'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' +
'[22:13:02] [Server thread/INFO]: venashial left the game\n' +
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' +
'[22:13:02] [Server thread/INFO]: Stopping server\n' +
'[22:13:02] [Server thread/INFO]: Saving players\n' +
'[22:13:02] [Server thread/INFO]: Saving worlds\n' +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n' +
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' +
'[22:13:07] [Render thread/INFO]: Stopping!\n' +
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...\n' +
"'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' +
'[22:13:02] [Server thread/INFO]: venashial left the game\n' +
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' +
'[22:13:02] [Server thread/INFO]: Stopping server\n' +
'[22:13:02] [Server thread/INFO]: Saving players\n' +
'[22:13:02] [Server thread/INFO]: Saving worlds\n' +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n' +
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' +
'[22:13:07] [Render thread/INFO]: Stopping!\n' +
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...\n' +
"'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' +
'[22:13:02] [Server thread/INFO]: venashial left the game\n' +
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' +
'[22:13:02] [Server thread/INFO]: Stopping server\n' +
'[22:13:02] [Server thread/INFO]: Saving players\n' +
'[22:13:02] [Server thread/INFO]: Saving worlds\n' +
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether\n" +
"[22:13:05] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end\n" +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (New World): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved\n' +
'[22:13:05] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved\n' +
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' +
'[22:13:07] [Render thread/INFO]: Stopping!\n' +
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...',
},
}
import {
Button,
Card,
CheckIcon,
ClipboardCopyIcon,
DropdownSelect,
SendIcon,
TrashIcon,
} from 'omorphia'
import { delete_logs_by_datetime, get_logs, get_stdout_by_datetime } from '@/helpers/logs.js'
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
import dayjs from 'dayjs'
import calendar from 'dayjs/plugin/calendar'
import { get_stdout_by_uuid, get_uuids_by_profile_path } from '@/helpers/process.js'
import { useRoute } from 'vue-router'
import { process_listener } from '@/helpers/events.js'
dayjs.extend(calendar)
const route = useRoute()
const props = defineProps({
instance: {
type: Object,
required: true,
},
})
async function getLiveLog() {
const uuids = await get_uuids_by_profile_path(route.params.id)
let returnValue
if (uuids.length === 0) {
returnValue = 'No live game detected. \nStart your game to proceed'
} else {
returnValue = await get_stdout_by_uuid(uuids[0])
}
return { name: 'Live Log', stdout: returnValue, live: true }
}
async function getLogs() {
return (await get_logs(props.instance.uuid, true)).reverse().map((log) => {
log.name = dayjs(
log.datetime_string.slice(0, 8) + 'T' + log.datetime_string.slice(9)
).calendar()
log.stdout = 'Loading...'
return log
})
}
async function setLogs() {
const [liveLog, allLogs] = await Promise.all([getLiveLog(), getLogs()])
logs.value = [liveLog, ...allLogs]
}
const logs = ref([])
await setLogs()
const selectedLogIndex = ref(0)
const copied = ref(false)
const copyLog = () => {
if (logs.value[selectedLogIndex.value]) {
navigator.clipboard.writeText(logs.value[selectedLogIndex.value].stdout)
copied.value = true
}
}
watch(selectedLogIndex, async (newIndex) => {
copied.value = false
userScrolled.value = false
if (newIndex !== 0) {
logs.value[newIndex].stdout = 'Loading...'
logs.value[newIndex].stdout = await get_stdout_by_datetime(
props.instance.uuid,
logs.value[newIndex].datetime_string
)
}
})
const deleteLog = async () => {
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
let deleteIndex = selectedLogIndex.value
selectedLogIndex.value = deleteIndex - 1
await delete_logs_by_datetime(props.instance.uuid, logs.value[deleteIndex].datetime_string)
await setLogs()
}
}
const logContainer = ref(null)
const interval = ref(null)
const userScrolled = ref(false)
const isAutoScrolling = ref(false)
function handleUserScroll() {
if (!isAutoScrolling.value) {
userScrolled.value = true
}
}
interval.value = setInterval(async () => {
if (logs.value.length > 0) {
logs.value[0] = await getLiveLog()
if (selectedLogIndex.value === 0 && !userScrolled.value) {
await nextTick()
isAutoScrolling.value = true
logContainer.value.scrollTop =
logContainer.value.scrollHeight - logContainer.value.offsetHeight
setTimeout(() => (isAutoScrolling.value = false), 50)
}
}
}, 250)
const unlistenProcesses = await process_listener(async (e) => {
if (e.event === 'finished') {
userScrolled.value = false
await setLogs()
}
})
onMounted(() => {
logContainer.value.addEventListener('scroll', handleUserScroll)
})
onBeforeUnmount(() => {
logContainer.value.removeEventListener('scroll', handleUserScroll)
})
onUnmounted(() => {
clearInterval(interval.value)
unlistenProcesses()
})
</script>
<style scoped lang="scss">
@@ -94,11 +179,14 @@ export default {
display: flex;
flex-direction: column;
gap: 1rem;
height: calc(100vh - 11rem);
}
.button-row {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 0.5rem;
}
.button-group {
@@ -109,7 +197,7 @@ export default {
.log-text {
width: 100%;
aspect-ratio: 2/1;
height: 100%;
font-family: var(--mono-font);
background-color: var(--color-accent-contrast);
color: var(--color-contrast);
@@ -117,5 +205,6 @@ export default {
padding: 1.5rem;
overflow: auto;
white-space: normal;
color-scheme: dark;
}
</style>

View File

@@ -1149,10 +1149,10 @@ ofetch@^1.0.1:
node-fetch-native "^1.0.2"
ufo "^1.1.0"
omorphia@^0.4.10:
version "0.4.10"
resolved "https://registry.yarnpkg.com/omorphia/-/omorphia-0.4.10.tgz#93c0e6a08a233f27d76587286e42450af44bb55d"
integrity sha512-WgSFosOqoM0IRpzGNYyprfZSRyBLgqs6sTmKRuWo96ZpzrHRWAom2upIm/HAxAC+YBwFni5sgUeBemXYI7wmuw==
omorphia@^0.4.13:
version "0.4.13"
resolved "https://registry.yarnpkg.com/omorphia/-/omorphia-0.4.13.tgz#6141886b9c332e4a28afe31a743f0c85d4a09efe"
integrity sha512-Yb76WoM4e42aAq3G/OPxQS6whCu+WIHVBhJxSzmUUycF1Pvf6tJZov+LefneSkk4xcQAjDZsgK8VOVD7q/siig==
dependencies:
dayjs "^1.11.7"
floating-vue "^2.0.0-beta.20"