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

@@ -138,6 +138,8 @@ pub async fn find_java17_jres() -> crate::Result<Vec<JavaVersion>> {
} }
pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> { pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
Box::pin(
async move {
let state = State::get().await?; let state = State::get().await?;
let loading_bar = init_loading( let loading_bar = init_loading(
@@ -217,6 +219,8 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
)).into()) )).into())
} }
} }
).await
}
// Get all JREs that exist on the system // Get all JREs that exist on the system
pub async fn get_all_jre() -> crate::Result<Vec<JavaVersion>> { pub async fn get_all_jre() -> crate::Result<Vec<JavaVersion>> {

View File

@@ -5,29 +5,46 @@ use tokio::fs::read_to_string;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Logs { pub struct Logs {
pub datetime_string: String, pub datetime_string: String,
pub stdout: String, pub stdout: Option<String>,
pub stderr: String, pub stderr: Option<String>,
} }
impl Logs { impl Logs {
async fn build( async fn build(
profile_uuid: uuid::Uuid, profile_uuid: uuid::Uuid,
datetime_string: String, datetime_string: String,
clear_contents: Option<bool>,
) -> crate::Result<Self> { ) -> crate::Result<Self> {
Ok(Self { Ok(Self {
stdout: get_stdout_by_datetime(profile_uuid, &datetime_string) stdout: if clear_contents.unwrap_or(false) {
None
} else {
Some(
get_stdout_by_datetime(profile_uuid, &datetime_string)
.await?, .await?,
stderr: get_stderr_by_datetime(profile_uuid, &datetime_string) )
},
stderr: if clear_contents.unwrap_or(false) {
None
} else {
Some(
get_stderr_by_datetime(profile_uuid, &datetime_string)
.await?, .await?,
)
},
datetime_string, datetime_string,
}) })
} }
} }
#[tracing::instrument] #[tracing::instrument]
pub async fn get_logs(profile_uuid: uuid::Uuid) -> crate::Result<Vec<Logs>> { pub async fn get_logs(
profile_uuid: uuid::Uuid,
clear_contents: Option<bool>,
) -> crate::Result<Vec<Logs>> {
let state = State::get().await?; let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(profile_uuid); let logs_folder = state.directories.profile_logs_dir(profile_uuid);
let mut logs = Vec::new(); let mut logs = Vec::new();
if logs_folder.exists() {
for entry in std::fs::read_dir(logs_folder)? { for entry in std::fs::read_dir(logs_folder)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
@@ -37,12 +54,15 @@ pub async fn get_logs(profile_uuid: uuid::Uuid) -> crate::Result<Vec<Logs>> {
Logs::build( Logs::build(
profile_uuid, profile_uuid,
datetime_string.to_string_lossy().to_string(), datetime_string.to_string_lossy().to_string(),
clear_contents,
) )
.await, .await,
); );
} }
} }
} }
}
let mut logs = logs.into_iter().collect::<crate::Result<Vec<Logs>>>()?; let mut logs = logs.into_iter().collect::<crate::Result<Vec<Logs>>>()?;
logs.sort_by_key(|x| x.datetime_string.clone()); logs.sort_by_key(|x| x.datetime_string.clone());
Ok(logs) Ok(logs)
@@ -54,8 +74,12 @@ pub async fn get_logs_by_datetime(
datetime_string: String, datetime_string: String,
) -> crate::Result<Logs> { ) -> crate::Result<Logs> {
Ok(Logs { Ok(Logs {
stdout: get_stdout_by_datetime(profile_uuid, &datetime_string).await?, stdout: Some(
stderr: get_stderr_by_datetime(profile_uuid, &datetime_string).await?, get_stdout_by_datetime(profile_uuid, &datetime_string).await?,
),
stderr: Some(
get_stderr_by_datetime(profile_uuid, &datetime_string).await?,
),
datetime_string, datetime_string,
}) })
} }

View File

@@ -187,6 +187,7 @@ pub async fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
#[tracing::instrument] #[tracing::instrument]
async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError> async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
{ {
Box::pin(async move {
let state = State::get().await.map_err(|_| JREError::StateError)?; let state = State::get().await.map_err(|_| JREError::StateError)?;
let mut jre_paths = HashSet::new(); let mut jre_paths = HashSet::new();
@@ -205,6 +206,8 @@ async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, JREError>
} }
Ok(jre_paths) Ok(jre_paths)
})
.await
} }
// Gets all JREs from the PATH env variable // Gets all JREs from the PATH env variable

View File

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

View File

@@ -14,8 +14,17 @@ pub struct Logs {
/// Get all Logs for a profile, sorted by datetime /// Get all Logs for a profile, sorted by datetime
#[tauri::command] #[tauri::command]
pub async fn logs_get_logs(profile_uuid: Uuid) -> Result<Vec<Logs>> { pub async fn logs_get_logs(
Ok(logs::get_logs(profile_uuid).await?) 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 /// Get a Log struct for a profile by profile id and datetime string

View File

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

View File

@@ -17,8 +17,8 @@ pub struct Logs {
/// Get all logs that exist for a given profile /// 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) /// 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) { export async function get_logs(profileUuid, clearContents) {
return await invoke('logs_get_logs', { profileUuid }) return await invoke('logs_get_logs', { profileUuid, clearContents })
} }
/// Get a profile's log by datetime_string (the folder name, when the log was created) /// Get a profile's log by datetime_string (the folder name, when the log was created)

View File

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

View File

@@ -1,92 +1,177 @@
<template> <template>
<Card class="log-card"> <Card class="log-card">
<div class="button-row"> <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"> <div class="button-group">
<Button> <Button :disabled="!logs[selectedLogIndex]" @click="copyLog()">
<ClipboardCopyIcon /> <ClipboardCopyIcon v-if="!copied" />
Copy <CheckIcon v-else />
{{ copied ? 'Copied' : 'Copy' }}
</Button> </Button>
<Button color="primary"> <Button disabled color="primary">
<SendIcon /> <SendIcon />
Share Share
</Button> </Button>
<Button color="danger"> <Button
:disabled="!logs[selectedLogIndex] || logs[selectedLogIndex].live === true"
color="danger"
@click="deleteLog()"
>
<TrashIcon /> <TrashIcon />
Delete Delete
</Button> </Button>
</div> </div>
</div> </div>
<div class="log-text"> <div ref="logContainer" class="log-text">
<div v-for="(line, index) in fileContents.value.split('\n')" :key="index">{{ line }}</div> <!-- {{ logs[1] }}-->
<div v-for="line in logs[selectedLogIndex]?.stdout.split('\n')" :key="line" class="no-wrap">
{{ line }}
</div>
</div> </div>
</Card> </Card>
</template> </template>
<script setup> <script setup>
import { Card, Button, TrashIcon, SendIcon, ClipboardCopyIcon, DropdownSelect } from 'omorphia' import {
</script> Button,
<script> Card,
export default { CheckIcon,
data() { ClipboardCopyIcon,
return { DropdownSelect,
fileContents: { SendIcon,
value: TrashIcon,
"'ServerLevel[New World]'/minecraft:the_end\n" + } from 'omorphia'
'[22:13:02] [Server thread/INFO]: venashial lost connection: Disconnected\n' + import { delete_logs_by_datetime, get_logs, get_stdout_by_datetime } from '@/helpers/logs.js'
'[22:13:02] [Server thread/INFO]: venashial left the game\n' + import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
'[22:13:02] [Server thread/INFO]: Stopping singleplayer server as player logged out\n' + import dayjs from 'dayjs'
'[22:13:02] [Server thread/INFO]: Stopping server\n' + import calendar from 'dayjs/plugin/calendar'
'[22:13:02] [Server thread/INFO]: Saving players\n' + import { get_stdout_by_uuid, get_uuids_by_profile_path } from '@/helpers/process.js'
'[22:13:02] [Server thread/INFO]: Saving worlds\n' + import { useRoute } from 'vue-router'
"[22:13:02] [Server thread/INFO]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld\n" + import { process_listener } from '@/helpers/events.js'
"[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" + dayjs.extend(calendar)
'[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' + const route = useRoute()
'[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' + const props = defineProps({
'[22:13:06] [Render thread/INFO]: Stopping worker threads\n' + instance: {
'[22:13:07] [Render thread/INFO]: Stopping!\n' + type: Object,
'[22:13:07] [CraftPresence-ShutDown-Handler/INFO]: Shutting down CraftPresence...\n' + required: true,
"'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...',
}, },
})
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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -94,11 +179,14 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
height: calc(100vh - 11rem);
} }
.button-row { .button-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem;
} }
.button-group { .button-group {
@@ -109,7 +197,7 @@ export default {
.log-text { .log-text {
width: 100%; width: 100%;
aspect-ratio: 2/1; height: 100%;
font-family: var(--mono-font); font-family: var(--mono-font);
background-color: var(--color-accent-contrast); background-color: var(--color-accent-contrast);
color: var(--color-contrast); color: var(--color-contrast);
@@ -117,5 +205,6 @@ export default {
padding: 1.5rem; padding: 1.5rem;
overflow: auto; overflow: auto;
white-space: normal; white-space: normal;
color-scheme: dark;
} }
</style> </style>

View File

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