11<script setup lang="ts">
22import { computed , nextTick , onMounted , onUnmounted , reactive , ref , watch } from ' vue' ;
3- import { useQuery , useApolloClient } from ' @vue/apollo-composable' ;
3+ import { useApolloClient , useQuery } from ' @vue/apollo-composable' ;
44import { vInfiniteScroll } from ' @vueuse/components' ;
5- import { ArrowPathIcon , ArrowDownTrayIcon } from ' @heroicons/vue/24/outline ' ;
6- import { Button , Tooltip , TooltipProvider , TooltipTrigger , TooltipContent } from ' @unraid/ui ' ;
7- import DOMPurify from ' isomorphic-dompurify ' ;
5+
6+ import { ArrowDownTrayIcon , ArrowPathIcon } from ' @heroicons/vue/24/outline ' ;
7+ import { Button , Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from ' @unraid/ui ' ;
88import hljs from ' highlight.js/lib/core' ;
9+ import DOMPurify from ' isomorphic-dompurify' ;
10+
911import ' highlight.js/styles/github-dark.css' ; // You can choose a different style
10- import { useThemeStore } from ' ~/store/theme' ;
1112
12- // Register the languages you want to support
13- import plaintext from ' highlight.js/lib/languages/plaintext' ;
13+ import apache from ' highlight.js/lib/languages/apache' ;
1414import bash from ' highlight.js/lib/languages/bash' ;
1515import ini from ' highlight.js/lib/languages/ini' ;
16- import xml from ' highlight.js/lib/languages/xml ' ;
16+ import javascript from ' highlight.js/lib/languages/javascript ' ;
1717import json from ' highlight.js/lib/languages/json' ;
18- import yaml from ' highlight.js/lib/languages/yaml' ;
1918import nginx from ' highlight.js/lib/languages/nginx' ;
20- import apache from ' highlight.js/lib/languages/apache' ;
21- import javascript from ' highlight.js/lib/languages/javascript' ;
2219import php from ' highlight.js/lib/languages/php' ;
20+ // Register the languages you want to support
21+ import plaintext from ' highlight.js/lib/languages/plaintext' ;
22+ import xml from ' highlight.js/lib/languages/xml' ;
23+ import yaml from ' highlight.js/lib/languages/yaml' ;
2324
2425import type { LogFileContentQuery , LogFileContentQueryVariables } from ' ~/composables/gql/graphql' ;
26+
27+ import { useThemeStore } from ' ~/store/theme' ;
2528import { GET_LOG_FILE_CONTENT } from ' ./log.query' ;
2629import { LOG_FILE_SUBSCRIPTION } from ' ./log.subscription' ;
2730
@@ -61,7 +64,7 @@ const state = reactive({
6164 canLoadMore: false ,
6265 initialLoadComplete: false ,
6366 isDownloading: false ,
64- isSubscriptionActive: false
67+ isSubscriptionActive: false ,
6568});
6669
6770// Get Apollo client for direct queries
@@ -117,15 +120,15 @@ onMounted(() => {
117120
118121 // Set subscription as active when we receive data
119122 state .isSubscriptionActive = true ;
120-
123+
121124 const existingContent = prev .logFile ?.content || ' ' ;
122125 const newContent = subscriptionData .data .logFile .content ;
123-
126+
124127 // Update the local state with the new content
125128 if (newContent && state .loadedContentChunks .length > 0 ) {
126129 const lastChunk = state .loadedContentChunks [state .loadedContentChunks .length - 1 ];
127130 lastChunk .content += newContent ;
128-
131+
129132 // Force scroll to bottom if auto-scroll is enabled
130133 if (props .autoScroll ) {
131134 nextTick (() => forceScrollToBottom ());
@@ -142,7 +145,7 @@ onMounted(() => {
142145 };
143146 },
144147 });
145-
148+
146149 // Set subscription as active
147150 state .isSubscriptionActive = true ;
148151 }
@@ -158,18 +161,18 @@ watch(
158161 logContentResult ,
159162 (newResult ) => {
160163 if (! newResult ?.logFile ) return ;
161-
164+
162165 const { content, startLine } = newResult .logFile ;
163166 const effectiveStartLine = startLine || 1 ;
164167
165168 if (state .isLoadingMore ) {
166169 state .loadedContentChunks .unshift ({ content , startLine: effectiveStartLine });
167170 state .isLoadingMore = false ;
168171
169- nextTick (() => state .canLoadMore = true );
172+ nextTick (() => ( state .canLoadMore = true ) );
170173 } else {
171174 state .loadedContentChunks = [{ content , startLine: effectiveStartLine }];
172-
175+
173176 nextTick (() => {
174177 forceScrollToBottom ();
175178 state .initialLoadComplete = true ;
@@ -190,29 +193,29 @@ const highlightLog = (content: string): string => {
190193 try {
191194 // Determine which language to use for highlighting
192195 const language = props .highlightLanguage || defaultLanguage ;
193-
196+
194197 // Apply syntax highlighting
195198 let highlighted = hljs .highlight (content , { language }).value ;
196-
199+
197200 // Apply additional custom highlighting for common log patterns
198-
201+
199202 // Highlight timestamps (various formats)
200203 highlighted = highlighted .replace (
201204 / \b (\d {4} -\d {2} -\d {2} [T ] \d {2} :\d {2} :\d {2} (?:\. \d + )? (?:Z| [+-] \d {2} :? \d {2} )? )\b / g ,
202205 ' <span class="hljs-timestamp">$1</span>'
203206 );
204-
207+
205208 // Highlight IP addresses
206209 highlighted = highlighted .replace (
207210 / \b (\d {1,3} \. \d {1,3} \. \d {1,3} \. \d {1,3} )\b / g ,
208211 ' <span class="hljs-ip">$1</span>'
209212 );
210-
213+
211214 // Split the content into lines
212215 let lines = highlighted .split (' \n ' );
213-
216+
214217 // Process each line to add error, warning, and success highlighting
215- lines = lines .map (line => {
218+ lines = lines .map (( line ) => {
216219 if (/ (error| exception| fail| failed| failure)/ i .test (line )) {
217220 // Highlight error keywords
218221 line = line .replace (
@@ -223,10 +226,7 @@ const highlightLog = (content: string): string => {
223226 return ` <span class="hljs-error">${line }</span> ` ;
224227 } else if (/ (warning| warn)/ i .test (line )) {
225228 // Highlight warning keywords
226- line = line .replace (
227- / \b (warning| warn)\b / gi ,
228- ' <span class="hljs-warning-keyword">$1</span>'
229- );
229+ line = line .replace (/ \b (warning| warn)\b / gi , ' <span class="hljs-warning-keyword">$1</span>' );
230230 // Wrap the entire line
231231 return ` <span class="hljs-warning">${line }</span> ` ;
232232 } else if (/ (success| successful| completed| done)/ i .test (line )) {
@@ -240,10 +240,10 @@ const highlightLog = (content: string): string => {
240240 }
241241 return line ;
242242 });
243-
243+
244244 // Join the lines back together
245245 highlighted = lines .join (' \n ' );
246-
246+
247247 // Sanitize the highlighted HTML
248248 return DOMPurify .sanitize (highlighted );
249249 } catch (error ) {
@@ -255,7 +255,7 @@ const highlightLog = (content: string): string => {
255255
256256// Computed properties
257257const logContent = computed (() => {
258- const rawContent = state .loadedContentChunks .map (chunk => chunk .content ).join (' ' );
258+ const rawContent = state .loadedContentChunks .map (( chunk ) => chunk .content ).join (' ' );
259259 return highlightLog (rawContent );
260260});
261261
@@ -294,13 +294,13 @@ const loadMoreContent = async () => {
294294// Download log file
295295const downloadLogFile = async () => {
296296 if (! props .logFilePath || state .isDownloading ) return ;
297-
297+
298298 try {
299299 state .isDownloading = true ;
300-
300+
301301 // Get the filename from the path
302302 const fileName = props .logFilePath .split (' /' ).pop () || ' log.txt' ;
303-
303+
304304 // Query for the entire log file content
305305 const result = await client .query ({
306306 query: GET_LOG_FILE_CONTENT ,
@@ -310,24 +310,24 @@ const downloadLogFile = async () => {
310310 },
311311 fetchPolicy: ' network-only' ,
312312 });
313-
313+
314314 if (! result .data ?.logFile ?.content ) {
315315 throw new Error (' Failed to fetch log content' );
316316 }
317-
317+
318318 // Create a blob with the content
319319 const blob = new Blob ([result .data .logFile .content ], { type: ' text/plain' });
320-
320+
321321 // Create a download link
322322 const url = URL .createObjectURL (blob );
323323 const link = document .createElement (' a' );
324324 link .href = url ;
325325 link .download = fileName ;
326-
326+
327327 // Trigger the download
328328 document .body .appendChild (link );
329329 link .click ();
330-
330+
331331 // Clean up
332332 document .body .removeChild (link );
333333 URL .revokeObjectURL (url );
@@ -348,7 +348,7 @@ const refreshLogContent = () => {
348348 state .initialLoadComplete = false ;
349349 state .isLoadingMore = false ;
350350 refetchLogContent ();
351-
351+
352352 nextTick (() => {
353353 forceScrollToBottom ();
354354 });
@@ -360,13 +360,18 @@ defineExpose({ refreshLogContent });
360360
361361<template >
362362 <div class =" flex flex-col h-full max-h-full overflow-hidden" >
363- <div class =" flex justify-between px-4 py-2 bg-muted text-xs text-muted-foreground shrink-0 items-center" >
363+ <div
364+ class =" flex justify-between px-4 py-2 bg-muted text-xs text-muted-foreground shrink-0 items-center"
365+ >
364366 <div class =" flex items-center gap-2" >
365367 <span >Total lines: {{ totalLines }}</span >
366368 <TooltipProvider v-if =" state.isSubscriptionActive" >
367369 <Tooltip :delay-duration =" 300" >
368370 <TooltipTrigger as-child >
369- <div class =" w-2 h-2 rounded-full bg-green-500 animate-pulse cursor-help" aria-hidden =" true" ></div >
371+ <div
372+ class =" w-2 h-2 rounded-full bg-green-500 animate-pulse cursor-help"
373+ aria-hidden =" true"
374+ ></div >
370375 </TooltipTrigger >
371376 <TooltipContent >
372377 <p >Watching log file</p >
@@ -376,42 +381,62 @@ defineExpose({ refreshLogContent });
376381 </div >
377382 <span >{{ state.isAtTop ? 'Showing all available lines' : 'Scroll up to load more' }}</span >
378383 <div class =" flex gap-2" >
379- <Button variant =" outline" :disabled =" loadingLogContent || state.isDownloading" @click =" downloadLogFile" >
380- <ArrowDownTrayIcon class =" h-3 w-3 mr-1" :class =" { 'animate-pulse': state.isDownloading }" aria-hidden =" true" />
384+ <Button
385+ variant =" secondary"
386+ :disabled =" loadingLogContent || state.isDownloading"
387+ @click =" downloadLogFile"
388+ >
389+ <ArrowDownTrayIcon
390+ class =" h-3 w-3 mr-1"
391+ :class =" { 'animate-pulse': state.isDownloading }"
392+ aria-hidden =" true"
393+ />
381394 <span class =" text-sm" >{{ state.isDownloading ? 'Downloading...' : 'Download' }}</span >
382395 </Button >
383- <Button variant =" outline " :disabled =" loadingLogContent" @click =" refreshLogContent" >
396+ <Button variant =" secondary " :disabled =" loadingLogContent" @click =" refreshLogContent" >
384397 <ArrowPathIcon class =" h-3 w-3 mr-1" aria-hidden =" true" />
385398 <span class =" text-sm" >Refresh</span >
386399 </Button >
387400 </div >
388401 </div >
389402
390- <div v-if =" loadingLogContent && !state.isLoadingMore" class =" flex items-center justify-center flex-1 p-4 text-muted-foreground" >
403+ <div
404+ v-if =" loadingLogContent && !state.isLoadingMore"
405+ class =" flex items-center justify-center flex-1 p-4 text-muted-foreground"
406+ >
391407 Loading log content...
392408 </div >
393409
394- <div v-else-if =" logContentError" class =" flex items-center justify-center flex-1 p-4 text-destructive" >
410+ <div
411+ v-else-if =" logContentError"
412+ class =" flex items-center justify-center flex-1 p-4 text-destructive"
413+ >
395414 Error loading log content: {{ logContentError.message }}
396415 </div >
397416
398417 <div
399418 v-else
400419 ref =" scrollViewportRef"
401- v-infinite-scroll =" [loadMoreContent, { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore }]"
420+ v-infinite-scroll =" [
421+ loadMoreContent,
422+ { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore },
423+ ]"
402424 class =" flex-1 overflow-y-auto"
403425 :class =" { 'theme-dark': isDarkMode, 'theme-light': !isDarkMode }"
404426 >
405427 <!-- Loading indicator for loading more content -->
406- <div v-if =" state.isLoadingMore" class =" sticky top-0 z-10 bg-muted/80 backdrop-blur-sm border-b border-border rounded-md mx-2 mt-2" >
428+ <div
429+ v-if =" state.isLoadingMore"
430+ class =" sticky top-0 z-10 bg-muted/80 backdrop-blur-sm border-b border-border rounded-md mx-2 mt-2"
431+ >
407432 <div class =" flex items-center justify-center p-2 text-xs text-primary-foreground" >
408433 <ArrowPathIcon class =" h-3 w-3 mr-2 animate-spin" aria-hidden =" true" />
409434 Loading more lines...
410435 </div >
411436 </div >
412-
413- <pre
414- class =" font-mono whitespace-pre-wrap p-4 m-0 text-xs leading-6 hljs"
437+
438+ <pre
439+ class =" font-mono whitespace-pre-wrap p-4 m-0 text-xs leading-6 hljs"
415440 :class =" { 'theme-dark': isDarkMode, 'theme-light': !isDarkMode }"
416441 v-html =" logContent"
417442 ></pre >
0 commit comments