Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,761 changes: 3,760 additions & 1 deletion public/app.css

Large diffs are not rendered by default.

62,732 changes: 62,730 additions & 2 deletions public/app.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"/app.js": "/app.js?id=b5eb6497b80ecd00237a857b35fcc1d6",
"/app.css": "/app.css?id=bf9e77abce3da8caacd004d57e4e8429",
"/app.js": "/app.js?id=aaada6e0203e63f49ca33aba8d105a7d",
"/app.css": "/app.css?id=788227d4e2fca0983985fbc19b1ca543",
"/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166",
"/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d",
"/img/log-viewer-64.png": "/img/log-viewer-64.png?id=8902d596fc883ca9eb8105bb683568c6"
Expand Down
3 changes: 3 additions & 0 deletions resources/js/components/FileList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<host-selector class="mb-8 mt-6" />
</template>

<severity-stats-cards class="mt-6" />

<template v-if="fileStore.fileTypesAvailable && fileStore.fileTypesAvailable.length > 1">
<file-type-selector class="mb-8 mt-6" />
</template>
Expand Down Expand Up @@ -218,6 +220,7 @@ import SiteSettingsDropdown from './SiteSettingsDropdown.vue';
import HostSelector from './HostSelector.vue';
import { handleKeyboardFileNavigation, handleKeyboardFileSettingsNavigation } from '../keyboardNavigation';
import FileTypeSelector from './FileTypeSelector.vue';
import SeverityStatsCards from './SeverityStatsCards.vue';
import DownloadLink from "./DownloadLink.vue";

const router = useRouter();
Expand Down
37 changes: 35 additions & 2 deletions resources/js/components/LevelButtons.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
<template>
<div class="flex items-center">
<Menu as="div" class="mr-5 relative log-levels-selector">
<div class="flex items-center" :class="verbose ? 'flex-wrap w-full' : ''">
<!-- Verbose mode: Individual buttons -->
<template v-if="verbose">
<!-- All button -->
<button
@click.stop.prevent="severityStore.selectAllLevels()"
class="badge none"
:class="severityStore.levelsSelected.length === severityStore.levelsFound.length ? 'active' : ''"
>
<span class="font-semibold">All</span>
<span class="ml-2 opacity-90">{{ severityStore.totalResults.toLocaleString() + (logViewerStore.hasMoreResults ? '+' : '') }}</span>
</button>

<!-- Individual level buttons -->
<button
v-for="levelCount in severityStore.levelsFound"
:key="levelCount.level"
@click.stop.prevent="severityStore.selectOnlyLevel(levelCount.level)"
class="badge"
:class="[levelCount.level_class, levelCount.selected ? 'active' : '']"
>
<span class="font-semibold">{{ levelCount.level_name }}</span>
<span class="ml-2 opacity-90">{{ Number(levelCount.count).toLocaleString() + (logViewerStore.hasMoreResults ? '+' : '') }}</span>
</button>
</template>

<!-- Compact mode: Dropdown -->
<Menu v-else as="div" class="relative log-levels-selector">

<MenuButton as="button" id="severity-dropdown-toggle" class="dropdown-toggle badge none" :class="severityStore.levelsSelected.length > 0 ? 'active' : ''">
<template v-if="severityStore.levelsSelected.length > 2">
Expand Down Expand Up @@ -76,6 +102,13 @@ import { useLogViewerStore } from '../stores/logViewer.js';
import { useSeverityStore } from '../stores/severity.js';
import { watch } from 'vue';

const props = defineProps({
verbose: {
type: Boolean,
default: false
}
});

const logViewerStore = useLogViewerStore();
const severityStore = useSeverityStore();

Expand Down
11 changes: 7 additions & 4 deletions resources/js/components/LogList.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<template>
<div class="h-full w-full py-5 log-list">
<div class="flex flex-col h-full w-full md:mx-3 mb-4">
<div class="md:px-4 mb-4 flex flex-col-reverse lg:flex-row items-start">
<div class="flex items-center mr-5 mt-3 md:mt-0" v-if="showLevelsDropdown">
<LevelButtons />
<div class="md:px-4 mb-4" :class="logViewerStore.verboseLogCount ? 'flex flex-col items-start' : 'flex flex-col-reverse lg:flex-row items-start'">
<!-- Level buttons - to the left in normal mode, separate row when verbose mode -->
<div class="flex items-center mt-3 md:mt-0" :class="logViewerStore.verboseLogCount ? 'w-full order-2' : 'mr-5'" v-if="showLevelsDropdown">
<LevelButtons :verbose="logViewerStore.verboseLogCount" />
</div>
<div class="w-full lg:w-auto flex-1 flex justify-end min-h-[38px]">

<!-- Search bar and controls - shares row with dropdown in normal mode, full width when verbose mode -->
<div class="flex justify-end min-h-[38px]" :class="logViewerStore.verboseLogCount ? 'w-full order-1 mb-3' : 'w-full lg:w-auto flex-1'">
<SearchInput />
<div class="hidden md:block ml-5">
<button @click="logViewerStore.loadLogs()" id="reload-logs-button" title="Reload current results" class="menu-button">
Expand Down
221 changes: 221 additions & 0 deletions resources/js/components/SeverityStatsCards.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<template>
<div
v-if="logViewerStore.showSeverityStats && levelCounts.length > 0"
class="mb-6"
>
<div class="ml-1 block text-sm text-gray-500 dark:text-gray-400 mb-3">Aggregate Statistics</div>
<div class="grid grid-cols-1 gap-2">
<div
v-for="level in levelCounts"
:key="level.level"
:class="getLevelCardClass(level.level_class, level.level)"
class="severity-stat-card"
>
<div class="severity-icon">
<component
:is="getLevelIcon(level.level)"
:class="['w-6 h-6', getLevelIconClass(level.level)]"
/>
</div>
<div class="severity-content">
<div class="severity-name">{{ level.level_name }}</div>
<div class="severity-stats">
<span class="severity-count">{{ Number(level.count).toLocaleString() }} entries</span>
<span class="severity-percentage">{{ level.percentage }}%</span>
</div>
</div>
</div>
</div>
</div>
<div
v-else-if="logViewerStore.showSeverityStats && loading"
class="mb-6 text-center text-sm text-gray-500 dark:text-gray-400 py-4"
>
<div class="flex items-center justify-center">
<SpinnerIcon class="w-5 h-5 mr-2" />
Loading statistics...
</div>
</div>
</template>

<script setup>
import {
Bars3BottomLeftIcon,
BellAlertIcon,
BugAntIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
LightBulbIcon,
MegaphoneIcon,
ShieldExclamationIcon,
XCircleIcon,
} from '@heroicons/vue/24/outline'
import axios from 'axios'
import { onMounted, ref, watch } from 'vue'
import { useFileStore } from '../stores/files.js'
import { useHostStore } from '../stores/hosts.js'
import { useLogViewerStore } from '../stores/logViewer.js'
import SpinnerIcon from './SpinnerIcon.vue'

const fileStore = useFileStore()
const hostStore = useHostStore()
const logViewerStore = useLogViewerStore()
const levelCounts = ref([])
const loading = ref(false)

const levelIcons = {
all: Bars3BottomLeftIcon,
emergency: MegaphoneIcon,
alert: BellAlertIcon,
critical: ShieldExclamationIcon,
error: XCircleIcon,
warning: ExclamationTriangleIcon,
notice: LightBulbIcon,
info: InformationCircleIcon,
debug: BugAntIcon,
}

const getLevelIcon = (level) => {
const normalizedLevel = level?.toLowerCase()
return levelIcons[normalizedLevel] || InformationCircleIcon
}

const getLevelIconClass = (levelName) => {
const normalizedLevel = levelName?.toLowerCase()

if (normalizedLevel === 'debug') {
return 'text-orange-500'
}
if (normalizedLevel === 'info') {
return 'text-blue-500'
}
if (normalizedLevel === 'notice') {
return 'text-emerald-500'
}
if (normalizedLevel === 'warning') {
return 'text-yellow-500'
}
if (
normalizedLevel === 'error' ||
normalizedLevel === 'critical' ||
normalizedLevel === 'alert' ||
normalizedLevel === 'emergency'
) {
return 'text-red-500'
}

return 'text-gray-500'
}

const getLevelCardClass = (levelClass, levelName) => {
const baseClasses = 'rounded border-l-4 p-3 flex items-center transition-colors duration-150'

// Normalize level name for comparison
const normalizedLevel = levelName?.toLowerCase()

// Check specific level names first for custom colors
if (normalizedLevel === 'debug') {
// Debug - Orange
return `${baseClasses} border-l-orange-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`
}

if (normalizedLevel === 'info') {
// Info - Blue
return `${baseClasses} border-l-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`
}

// Then use level class for general categorization
switch (levelClass) {
case 'none':
// All - Neutral gray
return `${baseClasses} border-l-gray-400 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`

case 'info':
// Fallback for info class - Blue
return `${baseClasses} border-l-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`

case 'notice':
case 'success':
// Notice - Green
return `${baseClasses} border-l-emerald-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`

case 'warning':
// Warning - Yellow
return `${baseClasses} border-l-yellow-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`

case 'danger':
// Danger (includes error, critical, alert, emergency) - Red
return `${baseClasses} border-l-red-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`

default:
return `${baseClasses} border-l-gray-400 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100`
}
}

const loadLevelStats = async () => {
// Don't load if the feature is disabled
if (!logViewerStore.showSeverityStats) {
return
}

loading.value = true

try {
const params = {
host: fileStore.hostQueryParam,
exclude_file_types: fileStore.fileTypesExcluded,
}

const response = await axios.get(`${window.LogViewer.basePath}/api/level-stats`, { params })
levelCounts.value = response.data.levelCounts || []
} catch (error) {
console.error('Error loading level statistics:', error)
levelCounts.value = []
} finally {
loading.value = false
}
}

onMounted(() => {
loadLevelStats()
})

// Reload stats when host, file types, or the setting changes
watch(
() => [hostStore.selectedHost, fileStore.selectedFileTypes, logViewerStore.showSeverityStats],
() => {
loadLevelStats()
},
{ deep: true },
)
</script>

<style scoped>
.severity-stat-card {
min-height: 68px;
}

.severity-icon {
@apply flex-shrink-0 mr-3;
}

.severity-content {
@apply flex-1 min-w-0;
}

.severity-name {
@apply font-semibold text-sm mb-1 text-gray-900 dark:text-gray-100;
}

.severity-stats {
@apply flex justify-between items-center text-xs font-medium;
}

.severity-count {
@apply truncate text-gray-600 dark:text-gray-400;
}

.severity-percentage {
@apply flex-shrink-0 ml-2 font-semibold text-gray-700 dark:text-gray-300;
}
</style>
14 changes: 14 additions & 0 deletions resources/js/components/SiteSettingsDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@
</button>
</MenuItem>

<MenuItem v-slot="{ active }">
<button :class="[active ? 'active' : '']" @click.stop.prevent="logViewerStore.verboseLogCount = !logViewerStore.verboseLogCount">
<Checkmark :checked="logViewerStore.verboseLogCount" />
<span class="ml-3">Verbose log count</span>
</button>
</MenuItem>

<MenuItem v-slot="{ active }">
<button :class="[active ? 'active' : '']" @click.stop.prevent="logViewerStore.showSeverityStats = !logViewerStore.showSeverityStats">
<Checkmark :checked="logViewerStore.showSeverityStats" />
<span class="ml-3">Show aggregate statistics</span>
</button>
</MenuItem>

<div class="divider"></div>
<div class="label">Actions</div>

Expand Down
20 changes: 13 additions & 7 deletions resources/js/stores/logViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,23 @@ export const useLogViewerStore = defineStore({
id: 'logViewer',

state: () => ({
theme: shouldUseLocalStorage
? useLocalStorage('logViewerTheme', window.LogViewer?.defaults?.theme || Theme.System)
theme: shouldUseLocalStorage
? useLocalStorage('logViewerTheme', window.LogViewer?.defaults?.theme || Theme.System)
: (window.LogViewer?.defaults?.theme || Theme.System),
shorterStackTraces: shouldUseLocalStorage
? useLocalStorage('logViewerShorterStackTraces', window.LogViewer?.defaults?.shorter_stack_traces ?? false)
shorterStackTraces: shouldUseLocalStorage
? useLocalStorage('logViewerShorterStackTraces', window.LogViewer?.defaults?.shorter_stack_traces ?? false)
: (window.LogViewer?.defaults?.shorter_stack_traces ?? false),
resultsPerPage: shouldUseLocalStorage
? useLocalStorage('logViewerResultsPerPage', window.LogViewer?.defaults?.per_page ?? 25)
verboseLogCount: shouldUseLocalStorage
? useLocalStorage('logViewerVerboseLogCount', window.LogViewer?.defaults?.verbose_log_count ?? true)
: (window.LogViewer?.defaults?.verbose_log_count ?? true),
showSeverityStats: shouldUseLocalStorage
? useLocalStorage('logViewerShowSeverityStats', window.LogViewer?.defaults?.show_severity_stats ?? true)
: (window.LogViewer?.defaults?.show_severity_stats ?? true),
resultsPerPage: shouldUseLocalStorage
? useLocalStorage('logViewerResultsPerPage', window.LogViewer?.defaults?.per_page ?? 25)
: (window.LogViewer?.defaults?.per_page ?? 25),
direction: shouldUseLocalStorage
? useLocalStorage('logViewerDirection', window.LogViewer?.defaults?.log_sorting_order || 'desc')
? useLocalStorage('logViewerDirection', window.LogViewer?.defaults?.log_sorting_order || 'desc')
: (window.LogViewer?.defaults?.log_sorting_order || 'desc'),
helpSlideOverOpen: false,

Expand Down
10 changes: 10 additions & 0 deletions resources/js/stores/severity.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,15 @@ export const useSeverityStore = defineStore({
levelCount.selected = false;
}
},

selectOnlyLevel(level) {
// Set excluded levels to all levels except the selected one
this.excludedLevels = this.allLevels.filter(l => l !== level);

// Update selected state for all levels
this.levelCounts.forEach(levelCount => {
levelCount.selected = levelCount.level === level;
});
},
},
})
1 change: 1 addition & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
Route::post('delete-multiple-files', 'FilesController@deleteMultipleFiles')->name('log-viewer.files.delete-multiple-files');

Route::get('logs', 'LogsController@index')->name('log-viewer.logs');
Route::get('level-stats', 'LogsController@levelStats')->name('log-viewer.level-stats');
});

Route::get('folders/{folderIdentifier}/download', 'FoldersController@download')
Expand Down
Loading