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
@@ -105,7 +108,7 @@ onMounted(() => {
105108        forceScrollToBottom (); 
106109      } 
107110    }); 
108-     observer .observe (scrollViewportRef .value , { childList: true , subtree: true  }); 
111+     observer .observe (scrollViewportRef .value   as   unknown   as   Node , { childList: true , subtree: true  }); 
109112  } 
110113
111114  if  (props .logFilePath ) { 
@@ -117,15 +120,15 @@ onMounted(() => {
117120
118121        //  Set subscription as active when we receive data 
119122        state .isSubscriptionActive  =  true ; 
120-          
123+ 
121124        const =  prev .logFile ?.content  ||  ' '  
122125        const =  subscriptionData .data .logFile .content ; 
123-          
126+ 
124127        //  Update the local state with the new content 
125128        if  (newContent  &&  state .loadedContentChunks .length  >  0 ) { 
126129          const =  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 =  newResult .logFile ; 
163166    const =  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 =  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 =  computed (() =>  {
258-   const =  state .loadedContentChunks .map (chunk  =>  chunk .content ).join (' '  
258+   const =  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 =  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 =  props .logFilePath .split (' /' pop () ||  ' log.txt'  
303-      
303+ 
304304    //  Query for the entire log file content 
305305    const =  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 =  new  Blob ([result .data .logFile .content ], { type: ' text/plain'  
320-      
320+ 
321321    //  Create a download link 
322322    const =  URL .createObjectURL (blob ); 
323323    const =  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,8 +381,16 @@ 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 =" outline" 
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" span >
382395        </Button >
383396        <Button  variant =" outline" :disabled =" loadingLogContent" @click =" refreshLogContent" 
@@ -387,31 +400,43 @@ defineExpose({ refreshLogContent });
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