Skip to content

Commit

Permalink
[photon-client] Log Viewer Improvements (#1385)
Browse files Browse the repository at this point in the history
Fixes the following issues with the client log viewer:
- Inconsistent and excessive spacing between log entries
- Lack of responsiveness to window size or scaling

Adds the following features to the log viewer:
- Auto-scroll if scrolled to the bottom
- Ability to clear logs on button click
- Search function to filter logs
- Displays the time the frontend captured a log and displays that timestamp in hh::mm::ss in the log viewer
- Allows logs to be filtered to be after a certain time
- General styling refinements to increase usability

---------

Co-authored-by: Sriman Achanta <68172138+srimanachanta@users.noreply.github.com>
  • Loading branch information
DevonRD and srimanachanta authored Aug 31, 2024
1 parent 169595e commit c38b509
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 76 deletions.
6 changes: 6 additions & 0 deletions photon-client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions photon-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"three": "^0.160.0",
"vue": "^2.7.14",
"vue-router": "^3.6.5",
"vue-virtual-scroll-list": "^2.3.5",
"vuetify": "^2.7.1"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion photon-client/src/components/app/photon-camera-stream.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ onBeforeUnmount(() => {

<template>
<div class="stream-container">
<img :id="id" crossorigin="anonymous" :src="streamSrc" :alt="streamDesc" :style="streamStyle" ref="mjpgStream" />
<img :id="id" ref="mjpgStream" crossorigin="anonymous" :src="streamSrc" :alt="streamDesc" :style="streamStyle" />
<div class="stream-overlay" :style="overlayStyle">
<pv-icon
icon-name="mdi-camera-image"
Expand Down
24 changes: 24 additions & 0 deletions photon-client/src/components/app/photon-log-entry.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
import { computed } from "vue";
const props = defineProps<{ source: LogMessage }>();
const logColorClass = computed<string>(() => {
switch (props.source.level) {
case LogLevel.ERROR:
return "red--text";
case LogLevel.WARN:
return "yellow--text";
case LogLevel.INFO:
return "light-blue--text";
case LogLevel.DEBUG:
return "white--text";
}
return "";
});
</script>

<template>
<div :class="logColorClass">[{{ source.timestamp.toTimeString().split(" ")[0] }}] {{ source.message }}</div>
</template>
214 changes: 145 additions & 69 deletions photon-client/src/components/app/photon-log-view.vue
Original file line number Diff line number Diff line change
@@ -1,40 +1,68 @@
<script setup lang="ts">
import { computed, inject, ref } from "vue";
import { computed, inject, ref, watch } from "vue";
import { LogLevel, type LogMessage } from "@/types/SettingTypes";
import { useStateStore } from "@/stores/StateStore";
import LogEntry from "@/components/app/photon-log-entry.vue";
import VirtualList from "vue-virtual-scroll-list";
const selectedLogLevels = ref<LogLevel[]>([LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO]);
const backendHost = inject<string>("backendHost");
const searchQuery = ref("");
const timeInput = ref<string>();
const autoScroll = ref(true);
const logList = ref();
const logKeeps = ref(40);
const exportLogFile = ref();
const selectedLogLevels = ref({
[LogLevel.ERROR]: true,
[LogLevel.WARN]: true,
[LogLevel.INFO]: true,
[LogLevel.DEBUG]: false
});
const logs = computed<LogMessage[]>(() =>
useStateStore().logMessages.filter((message) => selectedLogLevels.value.includes(message.level))
useStateStore()
.logMessages.filter(
(message) =>
selectedLogLevels.value[message.level] &&
message.message.toLowerCase().includes(searchQuery.value?.toLowerCase() || "") &&
(timeInput.value === undefined ||
message.timestamp >=
new Date().setHours(
parseInt(timeInput.value.substring(0, 2)),
parseInt(timeInput.value.substring(3, 5)),
parseInt(timeInput.value.substring(6, 8))
))
)
.map((item, index) => ({ ...item, index: index }))
);
const backendHost = inject<string>("backendHost");
watch(logs, () => {
if (!logList.value) return;
const getLogColor = (level: LogLevel): string => {
switch (level) {
case LogLevel.ERROR:
return "red";
case LogLevel.WARN:
return "yellow";
case LogLevel.INFO:
return "green";
case LogLevel.DEBUG:
return "white";
}
return "";
};
// Dynamic list render size based on console size
logKeeps.value = Math.ceil(logList.value.$el.clientHeight / 17.5) + 20;
const bottomOffset = Math.abs(
logList.value.$el.scrollHeight - logList.value.$el.scrollTop - logList.value.$el.clientHeight
);
autoScroll.value = bottomOffset < 50;
if (autoScroll.value) logList.value.scrollToBottom();
});
const getLogLevelFromIndex = (index: number): string => {
return LogLevel[index];
};
const exportLogFile = ref();
const handleLogExport = () => {
exportLogFile.value.click();
};
const handleLogClear = () => {
useStateStore().logMessages = [];
};
document.addEventListener("keydown", (e) => {
switch (e.key) {
case "`":
Expand All @@ -46,20 +74,16 @@ document.addEventListener("keydown", (e) => {

<template>
<v-dialog v-model="useStateStore().showLogModal" width="1500" dark>
<v-card dark class="pt-3" color="primary" flat>
<v-row class="heading-container pl-6 pr-6">
<v-col>
<v-card-title>View Program Logs</v-card-title>
<v-card dark class="dialog-container pa-6" color="primary" flat>
<!-- Logs header -->
<v-row class="no-gutters pb-3">
<v-col cols="4">
<v-card-title class="pa-0">Program Logs</v-card-title>
</v-col>
<v-col class="align-self-center">
<v-btn
color="secondary"
style="margin-left: auto; max-width: 500px; width: 100%"
depressed
@click="handleLogExport"
>
<v-icon left class="open-icon"> mdi-download </v-icon>
<span class="open-label">Download Current Log</span>
<v-col class="align-self-center pl-3" style="text-align: right">
<v-btn text color="white" @click="handleLogExport">
<v-icon left class="menu-icon"> mdi-download </v-icon>
<span class="menu-label">Download</span>

<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
<a
Expand All @@ -70,58 +94,110 @@ document.addEventListener("keydown", (e) => {
target="_blank"
/>
</v-btn>
<v-btn text color="white" @click="handleLogClear">
<v-icon left class="menu-icon"> mdi-trash-can-outline </v-icon>
<span class="menu-label">Clear Client Logs</span>
</v-btn>
<v-btn text color="white" @click="() => (useStateStore().showLogModal = false)">
<v-icon left class="menu-icon"> mdi-close </v-icon>
<span class="menu-label">Close</span>
</v-btn>
</v-col>
</v-row>
<div class="pr-6 pl-6">
<v-btn-toggle v-model="selectedLogLevels" dark multiple class="fill mb-4 overflow-x-auto">
<v-btn v-for="level in [0, 1, 2, 3]" :key="level" color="secondary" class="fill">
{{ getLogLevelFromIndex(level) }}
</v-btn>
</v-btn-toggle>
<v-card-text v-if="logs.length === 0" style="font-size: 18px; font-weight: 600">
There are no logs to show
</v-card-text>
<v-virtual-scroll v-else :items="logs" item-height="50" height="600">
<template #default="{ item }">
<div :class="[getLogColor(item.level) + '--text', 'log-item']">
{{ item.message }}
</div>
</template>
</v-virtual-scroll>
</div>

<v-divider />

<v-card-actions>
<v-spacer />
<v-btn color="white" text @click="() => (useStateStore().showLogModal = false)"> Close </v-btn>
</v-card-actions>
<div class="dialog-data">
<!-- Log view options -->
<v-row class="pt-4 pt-md-0 no-gutters">
<v-col cols="12" md="5" class="align-self-center">
<v-text-field
v-model="searchQuery"
dark
dense
clearable
hide-details="auto"
prepend-icon="mdi-magnify"
color="accent"
label="Search"
/>
</v-col>
<v-col cols="12" md="2" style="display: flex; align-items: center">
<input v-model="timeInput" type="time" step="1" class="white--text pl-0 pl-md-8" />
<v-btn icon class="ml-3" @click="timeInput = undefined">
<v-icon>mdi-close-circle-outline</v-icon>
</v-btn>
</v-col>
<v-col cols="12" md="5" class="pr-3">
<v-row class="no-gutters">
<v-col v-for="level in [0, 1, 2, 3]" :key="level">
<v-row dense align="center">
<v-col cols="6" md="8" style="text-align: right">
{{ getLogLevelFromIndex(level) }}
</v-col>
<v-col cols="6" md="4">
<v-switch v-model="selectedLogLevels[level]" dark color="#ffd843" />
</v-col>
</v-row>
</v-col>
</v-row>
</v-col>
</v-row>

<!-- Log entry list display -->
<div class="log-display">
<v-card-text v-if="!logs.length" style="font-size: 18px; font-weight: 150; height: 100%; text-align: center">
No available logs
</v-card-text>
<virtual-list
v-else
ref="logList"
style="height: 100%; overflow-y: auto"
data-key="index"
:data-sources="logs"
:data-component="LogEntry"
:estimate-size="35"
:keeps="logKeeps"
/>
</div>
</div>
</v-card>
</v-dialog>
</template>

<style scoped>
.v-btn-toggle.fill {
width: 100%;
height: 100%;
<style scoped lang="scss">
.dialog-container {
height: 90vh;
min-height: 300px !important;
}
.v-btn-toggle.fill > .v-btn {
width: 25%;
height: 100%;
.dialog-data {
/* Dialog size - dialog padding - header - divider */
height: calc(max(90vh, 300px) - 48px - 48px - 1px);
}
@media only screen and (max-width: 512px) {
.heading-container {
flex-direction: column;
padding-bottom: 14px;
}
.log-display {
/* Dialog data size - options */
height: calc(100% - 66px);
padding: 10px;
background-color: #232c37 !important;
border-radius: 5px;
}
@media only screen and (max-width: 312px) {
.open-icon {
margin: 0 !important;
@media only screen and (max-width: 960px) {
.log-display {
/* Dialog data size - options */
height: calc(100% - 118px);
}
.open-label {
}
@media only screen and (max-width: 700px) {
.menu-label {
display: none;
}
.menu-icon {
margin: 0 !important;
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ const downloadCalibBoard = () => {
charucoImage.src = CharucoImage;
doc.addImage(charucoImage, "PNG", 0.25, 1.5, 8, 8);
doc.text(`8 x 8 | 1in & 0.75in`, paperWidth - 1, 1.0, {
doc.text("8 x 8 | 1in & 0.75in", paperWidth - 1, 1.0, {
maxWidth: (paperWidth - 2.0) / 2,
align: "right"
});
Expand Down Expand Up @@ -274,8 +274,8 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:disabled="isCalibrating"
/>
<pv-select
v-model="tagFamily"
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="tagFamily"
label="Tag Family"
tooltip="Dictionary of aruco markers on the charuco board"
:select-cols="7"
Expand All @@ -291,8 +291,8 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:label-cols="5"
/>
<pv-number-input
v-model="markerSizeIn"
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="markerSizeIn"
label="Marker Size (in)"
tooltip="Size of the tag markers in inches must be smaller than pattern spacing"
:disabled="isCalibrating"
Expand All @@ -316,8 +316,8 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
:label-cols="5"
/>
<pv-switch
v-model="useOldPattern"
v-show="boardType == CalibrationBoardTypes.Charuco"
v-model="useOldPattern"
label="Old OpenCV Pattern"
:disabled="isCalibrating"
tooltip="If enabled, Photon will use the old OpenCV pattern for calibration."
Expand Down
4 changes: 2 additions & 2 deletions photon-client/src/components/settings/DeviceControlCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ const handleSettingsImport = () => {
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="openExportLogsPrompt">
<v-icon left class="open-icon"> mdi-download </v-icon>
<span class="open-label">Download Current Log</span>
<span class="open-label">Download logs</span>

<!-- Special hidden link that gets 'clicked' when the user exports journalctl logs -->
<a
Expand All @@ -318,7 +318,7 @@ const handleSettingsImport = () => {
<v-col cols="12" sm="6">
<v-btn color="secondary" @click="useStateStore().showLogModal = true">
<v-icon left class="open-icon"> mdi-eye </v-icon>
<span class="open-label">Show log viewer</span>
<span class="open-label">View program logs</span>
</v-btn>
</v-col>
</v-row>
Expand Down
1 change: 1 addition & 0 deletions photon-client/src/types/SettingTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export enum LogLevel {
export interface LogMessage {
level: LogLevel;
message: string;
timestamp: Date;
}

export interface Resolution {
Expand Down

0 comments on commit c38b509

Please sign in to comment.