Skip to content

Feature / LogViewer defaults #447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 10, 2025
Merged
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
52 changes: 52 additions & 0 deletions config/log-viewer.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<?php

use Opcodes\LogViewer\Enums\FolderSortingMethod;
use Opcodes\LogViewer\Enums\SortingOrder;
use Opcodes\LogViewer\Enums\Theme;

return [

/*
Expand Down Expand Up @@ -241,4 +245,52 @@
*/

'per_page_options' => [10, 25, 50, 100, 250, 500],

/*
|--------------------------------------------------------------------------
| Default settings for Log Viewer
|--------------------------------------------------------------------------
| These settings determine the default behaviour of Log Viewer. Many of
| these can be persisted for the user in their browser's localStorage,
| if the `use_local_storage` option is set to true.
|
*/

'defaults' => [

// Whether to use browser's localStorage to store user preferences.
// If true, user preferences saved in the browser will take precedence over the defaults below.
'use_local_storage' => true,

// Method to sort the folders. Other options: `Alphabetical`, `ModifiedTime`
'folder_sorting_method' => FolderSortingMethod::ModifiedTime,

// Order to sort the folders. Other options: `Ascending`, `Descending`
'folder_sorting_order' => SortingOrder::Descending,

// Order to sort the logs. Other options: `Ascending`, `Descending`
'log_sorting_order' => SortingOrder::Descending,

// Number of results per page. Must be one of the above `per_page_options` values
'per_page' => 25,

// Color scheme for the Log Viewer. Other options: `System`, `Light`, `Dark`
'theme' => Theme::System,

// Whether to enable `Shorter Stack Traces` option by default
'shorter_stack_traces' => false,

],

/*
|--------------------------------------------------------------------------
| Root folder prefix
|--------------------------------------------------------------------------
| The prefix for log files inside Laravel's `storage/logs` folder.
| Log Viewer does not show the full path to these files in the UI,
| but only the filename prefixed with this value.
|
*/

'root_folder_prefix' => 'root',
];
2 changes: 1 addition & 1 deletion public/app.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"/app.js": "/app.js?id=c5e0f20ee42d437f446958f2c1001581",
"/app.js": "/app.js?id=aad92fca2262c03653db52125757e99d",
"/app.css": "/app.css?id=5593a0331dd40729ff41e32a6035d872",
"/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166",
"/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d",
Expand Down
6 changes: 4 additions & 2 deletions resources/js/components/FileList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@
<ChevronRightIcon :class="[fileStore.isOpen(folder) ? 'rotate-90' : '', 'transition duration-100']" />
</span>
<span class="file-name">
<span v-if="String(folder.clean_path || '').startsWith('root')">
<span class="text-gray-500 dark:text-gray-400">root</span>{{ String(folder.clean_path).substring(4) }}
<span v-if="String(folder.clean_path || '').startsWith(rootFolderPrefix)">
<span class="text-gray-500 dark:text-gray-400">{{ rootFolderPrefix }}</span>{{ String(folder.clean_path).substring(rootFolderPrefix.length) }}
</span>
<span v-else>{{ folder.clean_path }}</span>
</span>
Expand Down Expand Up @@ -255,6 +255,8 @@ const selectFile = (fileIdentifier) => {
}
};

const rootFolderPrefix = window.LogViewer?.root_folder_prefix || 'root';

onMounted(async () => {
hostStore.selectHost(route.query.host || null);
});
Expand Down
18 changes: 14 additions & 4 deletions resources/js/stores/logViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,24 @@ const defaultColumns = [
{ label: 'Message', data_key: 'message' },
]

const shouldUseLocalStorage = window.LogViewer?.defaults?.use_local_storage ?? true;

export const useLogViewerStore = defineStore({
id: 'logViewer',

state: () => ({
theme: useLocalStorage('logViewerTheme', Theme.System),
shorterStackTraces: useLocalStorage('logViewerShorterStackTraces', false),
direction: useLocalStorage('logViewerDirection', 'desc'),
resultsPerPage: useLocalStorage('logViewerResultsPerPage', 25),
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)
: (window.LogViewer?.defaults?.shorter_stack_traces ?? false),
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')
: (window.LogViewer?.defaults?.log_sorting_order || 'desc'),
helpSlideOverOpen: false,

// Log data
Expand Down
9 changes: 9 additions & 0 deletions src/Enums/FolderSortingMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Opcodes\LogViewer\Enums;

class FolderSortingMethod
{
public const Alphabetical = 'Alphabetical';
public const ModifiedTime = 'ModifiedTime';
}
9 changes: 9 additions & 0 deletions src/Enums/SortingOrder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Opcodes\LogViewer\Enums;

class SortingOrder
{
public const Ascending = 'asc';
public const Descending = 'desc';
}
10 changes: 10 additions & 0 deletions src/Enums/Theme.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Opcodes\LogViewer\Enums;

class Theme
{
public const System = 'System';
public const Light = 'Light';
public const Dark = 'Dark';
}
32 changes: 28 additions & 4 deletions src/Http/Controllers/FoldersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
use Opcodes\LogViewer\Enums\FolderSortingMethod;
use Opcodes\LogViewer\Enums\SortingOrder;
use Opcodes\LogViewer\Facades\LogViewer;
use Opcodes\LogViewer\Http\Resources\LogFolderResource;
use Opcodes\LogViewer\LogFile;
Expand All @@ -15,10 +17,32 @@ public function index(Request $request)
{
$folders = LogViewer::getFilesGroupedByFolder();

if ($request->query('direction', 'desc') === 'asc') {
$folders = $folders->sortByEarliestFirstIncludingFiles();
} else {
$folders = $folders->sortByLatestFirstIncludingFiles();
$sortingMethod = config('log-viewer.defaults.folder_sorting_method', FolderSortingMethod::ModifiedTime);
$sortingOrder = config('log-viewer.defaults.folder_sorting_order', SortingOrder::Descending);

$fileSortingOrder = $request->query('direction', 'desc');

if ($sortingMethod === FolderSortingMethod::Alphabetical) {
if ($sortingOrder === SortingOrder::Ascending) {
$folders = $folders->sortAlphabeticallyAsc();
} else {
$folders = $folders->sortAlphabeticallyDesc();
}

// Still sort files inside folders by direction param
$folders->each(function ($folder) use ($fileSortingOrder) {
if ($fileSortingOrder === 'asc') {
$folder->files()->sortByEarliestFirst();
} else {
$folder->files()->sortByLatestFirst();
}
});
} else { // ModifiedTime
if ($fileSortingOrder === 'asc') {
$folders = $folders->sortByEarliestFirstIncludingFiles();
} else {
$folders = $folders->sortByLatestFirstIncludingFiles();
}
}

return LogFolderResource::collection($folders->values());
Expand Down
9 changes: 9 additions & 0 deletions src/Http/Controllers/IndexController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Opcodes\LogViewer\Http\Controllers;

use Opcodes\LogViewer\Facades\LogViewer;
use Opcodes\LogViewer\LogFolder;
use Opcodes\LogViewer\Utils\Utils;

class IndexController
Expand All @@ -28,6 +29,14 @@ public function __invoke()
'supports_hosts' => LogViewer::supportsHostsFeature(),
'hosts' => LogViewer::getHosts(),
'per_page_options' => config('log-viewer.per_page_options') ?? [10, 25, 50, 100, 250, 500],
'defaults' => [
'use_local_storage' => config('log-viewer.defaults.use_local_storage'),
'log_sorting_order' => config('log-viewer.defaults.log_sorting_order'),
'per_page' => config('log-viewer.defaults.per_page'),
'theme' => config('log-viewer.defaults.theme'),
'shorter_stack_traces' => config('log-viewer.defaults.shorter_stack_traces'),
],
'root_folder_prefix' => LogFolder::rootPrefix(),
],
]);
}
Expand Down
17 changes: 15 additions & 2 deletions src/LogFolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class LogFolder
{
public string $identifier;
protected mixed $files;
protected static string $rootPrefix;

public function __construct(
public string $path,
Expand Down Expand Up @@ -38,15 +39,27 @@ public function isRoot(): bool
|| $this->path === rtrim(LogViewer::basePathForLogs(), DIRECTORY_SEPARATOR);
}

/**
* Returns the prefix string used to represent the root folder.
*/
public static function rootPrefix(): string
{
if (! isset(self::$rootPrefix)) {
self::$rootPrefix = config('log-viewer.root_folder_prefix', 'root');
}

return self::$rootPrefix;
}

public function cleanPath(): string
{
if ($this->isRoot()) {
return 'root';
return self::rootPrefix();
}

$folder = $this->path;

$folder = str_replace(LogViewer::basePathForLogs(), 'root'.DIRECTORY_SEPARATOR, $folder);
$folder = str_replace(LogViewer::basePathForLogs(), self::rootPrefix().DIRECTORY_SEPARATOR, $folder);

if ($unixHomePath = getenv('HOME')) {
$folder = str_replace($unixHomePath, '~', $folder);
Expand Down
38 changes: 38 additions & 0 deletions src/LogFolderCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,42 @@ public function sortByLatestFirstIncludingFiles(): self

return $this;
}

public function sortAlphabeticallyAsc(): self
{
$this->items = collect($this->items)
->sort(function (LogFolder $a, LogFolder $b) {
if ($a->isRoot() && ! $b->isRoot()) {
return -1;
}
if (! $a->isRoot() && $b->isRoot()) {
return 1;
}

return strcmp($a->cleanPath(), $b->cleanPath());
})
->values()
->toArray();

return $this;
}

public function sortAlphabeticallyDesc(): self
{
$this->items = collect($this->items)
->sort(function (LogFolder $a, LogFolder $b) {
if ($a->isRoot() && ! $b->isRoot()) {
return -1;
}
if (! $a->isRoot() && $b->isRoot()) {
return 1;
}

return strcmp($b->cleanPath(), $a->cleanPath());
})
->values()
->toArray();

return $this;
}
}
6 changes: 3 additions & 3 deletions tests/Feature/Authorization/CanDownloadFoldersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function assertCannotDownloadFolder(string $folderName): void
generateLogFiles([$fileName = 'laravel.log']);
$folder = LogViewer::getFolder('');

assertCanDownloadFolder($folder->identifier, 'root.zip');
assertCanDownloadFolder($folder->identifier, LogFolder::rootPrefix().'.zip');
});

test('cannot download a folder that\'s not found', function () {
Expand All @@ -47,7 +47,7 @@ function assertCannotDownloadFolder(string $folderName): void
// now let's allow access again
Gate::define('downloadLogFolder', fn (mixed $user) => true);

assertCanDownloadFolder($folder->identifier, 'root.zip');
assertCanDownloadFolder($folder->identifier, LogFolder::rootPrefix().'.zip');
});

test('"downloadLogFolder" gate is supplied with a log folder object', function () {
Expand All @@ -63,7 +63,7 @@ function assertCannotDownloadFolder(string $folderName): void
return true;
});

assertCanDownloadFolder($expectedFolder->identifier, 'root.zip');
assertCanDownloadFolder($expectedFolder->identifier, LogFolder::rootPrefix().'.zip');

expect($gateChecked)->toBeTrue();
});
34 changes: 32 additions & 2 deletions tests/Feature/LogFoldersControllerTest.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
<?php

use Opcodes\LogViewer\Enums\FolderSortingMethod;
use Opcodes\LogViewer\Enums\SortingOrder;
use Opcodes\LogViewer\LogFolder;

use function Pest\Laravel\getJson;

beforeEach(function () {
config(['log-viewer.include_files' => ['*/**.log']]);
});

it('can get the log files', function () {
config(['log-viewer.defaults.folder_sorting_method' => FolderSortingMethod::ModifiedTime]);
config(['log-viewer.defaults.folder_sorting_order' => SortingOrder::Descending]);

generateLogFiles([
'one/1.one.log',
'one/2.two.log',
Expand All @@ -17,6 +24,29 @@

expect($response->json())->not->toHaveKey('data');
$response->assertJsonCount(2)
->assertJsonFragment(['clean_path' => 'root'.DIRECTORY_SEPARATOR.'one'])
->assertJsonFragment(['clean_path' => 'root'.DIRECTORY_SEPARATOR.'two']);
->assertJsonFragment(['clean_path' => LogFolder::rootPrefix().DIRECTORY_SEPARATOR.'one'])
->assertJsonFragment(['clean_path' => LogFolder::rootPrefix().DIRECTORY_SEPARATOR.'two']);
});

it('folders are sorted alphabetically descending when configured', function () {
config(['log-viewer.include_files' => ['*.log', '*/**.log']]);
config(['log-viewer.defaults.folder_sorting_method' => FolderSortingMethod::Alphabetical]);
config(['log-viewer.defaults.folder_sorting_order' => SortingOrder::Ascending]);

generateLogFiles([
'one/1.one.log',
'one/2.two.log',
'two/3.three.log',
'alpha/4.alpha.log',
'laravel.log',
], randomContent: true);

$response = getJson(route('log-viewer.folders'));
$folders = $response->json();
// Should be sorted: 'root', 'alpha', 'one', 'two'
$response->assertJsonCount(4);
expect($folders[0]['clean_path'])->toBe(LogFolder::rootPrefix());
expect($folders[1]['clean_path'])->toBe(LogFolder::rootPrefix().DIRECTORY_SEPARATOR.'alpha');
expect($folders[2]['clean_path'])->toBe(LogFolder::rootPrefix().DIRECTORY_SEPARATOR.'one');
expect($folders[3]['clean_path'])->toBe(LogFolder::rootPrefix().DIRECTORY_SEPARATOR.'two');
});
Loading
Loading