Skip to content

Commit

Permalink
group files into folders
Browse files Browse the repository at this point in the history
  • Loading branch information
arukompas committed Sep 9, 2022
1 parent eb00851 commit 81e6f3c
Show file tree
Hide file tree
Showing 16 changed files with 247 additions and 47 deletions.
2 changes: 1 addition & 1 deletion public/app.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/app.js

Large diffs are not rendered by default.

45 changes: 33 additions & 12 deletions resources/css/app.scss
Expand Up @@ -398,29 +398,37 @@ html.dark {
.file-list {
@apply relative h-full overflow-y-auto pt-6 pb-6 pr-4;

.file-item-container {
@apply relative mb-2 text-gray-800 dark:text-gray-200 rounded-md transition duration-100 border cursor-pointer border-transparent bg-white dark:bg-gray-800;

&.active {
@apply border-emerald-500 bg-emerald-50 dark:border-emerald-900 dark:bg-emerald-900 dark:bg-opacity-50;
}

&:hover {
@apply border-emerald-600 dark:border-emerald-800;
}
.file-item-container,
.folder-item-container {
@apply relative mt-2 text-gray-800 dark:text-gray-200 rounded-md bg-white dark:bg-gray-800;

.file-item {
@apply relative flex justify-between items-center pl-4 pr-10 py-2;
@apply relative flex justify-between items-center pl-4 pr-10 py-2 rounded-md border cursor-pointer border-transparent transition duration-100;

.file-icon {
@apply mr-2 text-gray-400 dark:text-gray-500;
&>svg {
@apply w-4 h-4;
}
}

.file-name {
@apply text-sm mr-3;
@apply text-sm mr-3 w-full;
}

.file-size {
@apply text-xs text-gray-500 dark:text-gray-300 dark:opacity-90 whitespace-nowrap;
}
}

&.active .file-item {
@apply border-emerald-500 dark:border-emerald-900 bg-emerald-50 dark:bg-emerald-900 dark:bg-opacity-40;
}

&:hover .file-item {
@apply border-emerald-600 dark:border-emerald-800;
}

.file-dropdown-toggle {
@apply absolute top-0 right-0 bottom-0 w-8 flex rounded-r-md items-center justify-center border-l border-transparent text-gray-500 dark:text-gray-400 outline-emerald-500 dark:outline-emerald-700 transition duration-200;
&:hover {
Expand All @@ -431,6 +439,19 @@ html.dark {
}
}
}

.folder-container {
.folder-item-container {
&.sticky {
position: -webkit-sticky;
position: sticky;
}

& > .file-item {
@apply pr-4;
}
}
}
}

.menu-button {
Expand Down
59 changes: 59 additions & 0 deletions resources/js/app.js
Expand Up @@ -13,6 +13,65 @@ Alpine.plugin(Persist)

window.Alpine = Alpine

Alpine.store('fileViewer', {
foldersOpen: [],
foldersInView: [],
folderTops: {},
containerTop: 0,
isOpen(folder) {
return this.foldersOpen.includes(folder);
},
toggle(folder) {
if (this.isOpen(folder)) {
this.foldersOpen = this.foldersOpen.filter(f => f !== folder);
} else {
this.foldersOpen.push(folder);
}
this.onScroll();
},
shouldBeSticky(folder) {
return this.isOpen(folder) && this.foldersInView.includes(folder);
},
stickTopPosition(folder) {
let aboveFold = this.pixelsAboveFold(folder);

if (aboveFold < 0) {
return Math.max(0, -24 + aboveFold) + 'px';
}

return '-24px';
},
pixelsAboveFold(folder) {
let folderContainer = document.getElementById('folder-'+folder);
if (!folderContainer) return false;
let row = folderContainer.getClientRects()[0];
return (row.top + row.height) - this.containerTop;
},
isInViewport(index) {
return this.pixelsAboveFold(index) > -36;
},
onScroll() {
let vm = this;
this.foldersOpen.forEach(function (folder) {
if (vm.isInViewport(folder)) {
if (!vm.foldersInView.includes(folder)) { vm.foldersInView.push(folder); }
vm.folderTops[folder] = vm.stickTopPosition(folder);
} else {
vm.foldersInView = vm.foldersInView.filter(f => f !== folder);
delete vm.folderTops[folder];
}
})
},
reset() {
this.foldersOpen = [];
this.foldersInView = [];
this.folderTops = {};
const container = document.getElementById('file-list-container');
this.containerTop = container.getBoundingClientRect().top;
container.scrollTo(0, 0);
}
});

Alpine.store('logViewer', {
theme: Alpine.$persist(Theme.System).as('logViewer_theme'),
stacksOpen: [],
Expand Down
8 changes: 8 additions & 0 deletions resources/views/icons.blade.php
Expand Up @@ -104,5 +104,13 @@
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clip-rule="evenodd" />
</symbol>
<symbol id="icon-folder" viewBox="0 0 20 20">
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />
</symbol>
<symbol id="icon-folder-open" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z"
clip-rule="evenodd" />
<path d="M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z" />
</symbol>
</defs>
</svg>
9 changes: 5 additions & 4 deletions resources/views/index.blade.php
Expand Up @@ -15,7 +15,7 @@
</head>
<body class="h-full px-5 bg-gray-100 dark:bg-gray-900"
x-data="{
selectedFileIdentifier: @isset($selectedFileIdentifier) '{{ $selectedFileIdentifier }}' @else null @endisset,
selectedFileIdentifier: @isset($selectedFile) '{{ $selectedFile->identifier }}' @else null @endisset,
selectFile(name) {
if (name && name === this.selectedFileIdentifier) {
this.selectedFileIdentifier = null;
Expand All @@ -25,9 +25,10 @@
this.$dispatch('file-selected', this.selectedFileIdentifier);
}
}"
x-init="$nextTick(() => { $store.fileViewer.reset(); @if(isset($selectedFile)) $store.fileViewer.foldersOpen.push('{{ $selectedFile->subFolderIdentifier() }}') @endif })"
>
<div class="flex h-full max-h-screen max-w-full">
<div class="hidden md:flex md:w-80 md:flex-col md:fixed md:inset-y-0">
<div class="hidden md:flex md:w-88 md:flex-col md:fixed md:inset-y-0">
<nav class="flex flex-col h-full py-5">
<div class="mx-3 mb-4">
<h1 class="font-semibold text-emerald-800 dark:text-emerald-600 text-2xl flex items-center">
Expand All @@ -44,11 +45,11 @@
@endif
</div>

@livewire('log-viewer::file-list', ['selectedFileIdentifier' => $selectedFileIdentifier])
@livewire('log-viewer::file-list', ['selectedFileIdentifier' => $selectedFile?->identifier])
</nav>
</div>

<div class="md:pl-80 flex flex-col flex-1 min-h-screen max-h-screen max-w-full">
<div class="md:pl-88 flex flex-col flex-1 min-h-screen max-h-screen max-w-full">
@livewire('log-viewer::log-list')
</div>
</div>
Expand Down
31 changes: 27 additions & 4 deletions resources/views/livewire/file-list.blade.php
@@ -1,6 +1,6 @@
<div class="relative h-full overflow-hidden" x-cloak @if(!$shouldLoadFilesImmediately) wire:init="loadFiles" @endif>
<div id="file-list-container" class="relative h-full overflow-hidden" x-cloak @if(!$shouldLoadFilesImmediately) wire:init="loadFiles" @endif>
<div class="pointer-events-none absolute z-10 top-0 h-6 w-full bg-gradient-to-b from-gray-100 dark:from-gray-900 to-transparent"></div>
<div class="file-list" x-ref="list">
<div class="file-list" x-ref="list" x-on:scroll="(event) => $store.fileViewer.onScroll(event)">
@if(!$shouldLoadFilesImmediately)
<div class="w-full flex flex-col items-center justify-center text-gray-600 dark:text-gray-400 text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 opacity-70 spin" fill="currentColor"><use href="#icon-spinner" /></svg>
Expand All @@ -10,17 +10,40 @@
</div>
@endif

<div class="text-sm text-gray-600 mb-4 ml-1">
@if($shouldLoadFilesImmediately)
<div class="text-sm text-gray-600 dark:text-gray-400 mb-4 ml-1">
<label for="file-sort-direction" class="sr-only">Sort direction</label>
<select id="file-sort-direction" wire:model="direction" class="bg-gray-100 dark:bg-gray-900 px-2 font-normal mr-3 outline-none rounded focus:ring-2 focus:ring-emerald-500 dark:focus:ring-emerald-600">
<option value="desc">Newest first</option>
<option value="asc">Oldest first</option>
</select>
</div>
@endif
@php /** @var \Opcodes\LogViewer\LogFolder $folder */ @endphp
@foreach($filesGrouped as $folder)
<div x-data="{ folder: '{{ $folder->identifier }}' }" :id="'folder-'+folder"
class="relative @if(!$folder->isRoot()) folder-container @endif"
>
@if(!$folder->isRoot())
<div class="folder-item-container"
x-on:click="$store.fileViewer.toggle(folder)"
x-bind:class="[$store.fileViewer.isOpen(folder) ? 'active' : '', $store.fileViewer.shouldBeSticky(folder) ? 'sticky z-10' : '']"
x-bind:style="{ top: $store.fileViewer.isOpen(folder) ? ($store.fileViewer.folderTops[folder] || 0) : 0 }"
>
<div class="file-item">
@include('log-viewer::partials.folder-icon')
<div class="file-name">{{ $folder->cleanPath() }}</div>
</div>
</div>
@endif

@foreach($files as $logFile)
<div class="folder-files @if(!$folder->isRoot()) pl-3 ml-1 border-l border-gray-200 dark:border-gray-800 @endif" @if(!$folder->isRoot()) x-show="$store.fileViewer.isOpen(folder)" @endif>
@foreach($folder->files as $logFile)
@include('log-viewer::partials.file-list-item', ['logFile' => $logFile])
@endforeach
</div>
</div>
@endforeach
</div>
<div class="pointer-events-none absolute z-10 bottom-0 h-8 w-full bg-gradient-to-t from-gray-100 dark:from-gray-900 to-transparent"></div>
</div>
2 changes: 1 addition & 1 deletion resources/views/partials/file-list-item.blade.php
Expand Up @@ -24,7 +24,7 @@
x-id="['dropdown-button']"
>
<div class="file-item">
<p class="file-name"><span class="text-gray-400 dark:text-gray-500">{{ $logFile->subFolderFormatted() }}</span> {{ $logFile->name }}</p>
<p class="file-name">{{ $logFile->name }}</p>
<span class="file-size">{{ $logFile->sizeFormatted() }}</span>
<button type="button" class="file-dropdown-toggle"
x-ref="button" x-on:click.stop="toggle()" :aria-expanded="open" :aria-controls="$id('dropdown-button')"
Expand Down
4 changes: 4 additions & 0 deletions resources/views/partials/folder-icon.blade.php
@@ -0,0 +1,4 @@
<div class="file-icon">
<svg x-show="!$store.fileViewer.isOpen(folder)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><use href="#icon-folder" /></svg>
<svg x-show="$store.fileViewer.isOpen(folder)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><use href="#icon-folder-open" /></svg>
</div>
2 changes: 1 addition & 1 deletion routes/web.php
Expand Up @@ -15,7 +15,7 @@
return view('log-viewer::index', [
'jsPath' => __DIR__.'/../public/app.js',
'cssPath' => __DIR__.'/../public/app.css',
'selectedFileIdentifier' => $selectedFile?->identifier,
'selectedFile' => $selectedFile,
]);
})->name('blv.index');

Expand Down
1 change: 1 addition & 0 deletions src/Facades/LogViewer.php
Expand Up @@ -20,6 +20,7 @@
* @method static int maxLogSize()
* @method static string laravelRegexPattern()
* @method static string logMatchPattern()
* @method static string basePathForLogs()
*/
class LogViewer extends Facade
{
Expand Down
33 changes: 18 additions & 15 deletions src/Http/Livewire/FileList.php
Expand Up @@ -8,6 +8,7 @@
use Livewire\Component;
use Opcodes\LogViewer\Facades\LogViewer;
use Opcodes\LogViewer\LogFile;
use Opcodes\LogViewer\LogFolder;
use Opcodes\LogViewer\LogReader;

class FileList extends Component
Expand Down Expand Up @@ -62,29 +63,31 @@ public function render()
LogReader::clearInstance($file);
}

$files = $files->groupBy('subFolder')
$filesGrouped = $files->groupBy(fn ($file) => $file->subFolder)

->when($this->direction === self::OLDEST_FIRST, function ($groupedFiles) {
return $groupedFiles->sortBy(function ($group) {
return $group->min->earliestTimestamp();
->map(fn ($files, $subFolder) => new LogFolder($subFolder, $files))

// sort the folders
->when($this->direction === self::OLDEST_FIRST, function (Collection $folders) {
return $folders->sortBy(function (LogFolder $folder) {
return $folder->files->min->earliestTimestamp();
});
}, function ($groupedFiles) {
return $groupedFiles->sortByDesc(function ($group) {
return $group->max->latestTimestamp();
}, function (Collection $folders) {
return $folders->sortByDesc(function (LogFolder $folder) {
return $folder->files->max->latestTimestamp();
});
})

// Then individual log files by their latest or earliest timestamps
->map(function (Collection $group) {
->map(function (LogFolder $folder) {
if ($this->direction === self::OLDEST_FIRST) {
return $group->sortBy->earliestTimestamp();
$folder->files = $folder->files->sortBy->earliestTimestamp();
}

return $group->sortByDesc->latestTimestamp();
})
$folder->files = $folder->files->sortByDesc->latestTimestamp();

// And then bring back into a flat view after everything's sorted
->flatten();
return $folder;
});
} else {
// Otherwise, let's estimate the scan duration by sampling the speed of the first scan.
// For more accurate results, let's scan a file that's more than 10 MB in size.
Expand All @@ -102,14 +105,14 @@ public function render()
$totalFileSize -= $file->size();

$durationInMicroseconds = ($scanEnd - $scanStart) * 1000_000;
$microsecondsPerMB = $durationInMicroseconds / $file->sizeInMB() * 1.10; // 10% buffer just in case
$microsecondsPerMB = $durationInMicroseconds / $file->sizeInMB() * 1.20; // 20% buffer just in case
$totalFileSizeInMB = $totalFileSize / 1024 / 1024;

$estimatedSecondsToScan = ceil($totalFileSizeInMB * $microsecondsPerMB / 1000_000);
}

return view('log-viewer::livewire.file-list', [
'files' => $this->shouldLoadFilesImmediately && $files ? $files : [],
'filesGrouped' => $this->shouldLoadFilesImmediately && isset($filesGrouped) ? $filesGrouped : [],
'totalFileSize' => $totalFileSize,
'cacheRecentlyCleared' => $this->cacheRecentlyCleared ?? false,
'estimatedTimeToScan' => CarbonInterval::seconds($estimatedSecondsToScan)->cascade()->forHumans(),
Expand Down
7 changes: 4 additions & 3 deletions src/LogFile.php
Expand Up @@ -27,7 +27,8 @@ public function __construct(
$folder = str_replace(Str::finish(storage_path('logs'), DIRECTORY_SEPARATOR), '', $path);

// now we're left with something like `folderA/laravel.log`. Let's remove the file name because we already know it.
$this->subFolder = str_replace($name, '', $folder);
$this->subFolder = str_replace([LogViewer::basePathForLogs(), $name], ['', ''], $folder);
$this->subFolder = rtrim($this->subFolder, DIRECTORY_SEPARATOR);

$this->metaData = Cache::get($this->metaDataCacheKey(), []);
}
Expand Down Expand Up @@ -60,9 +61,9 @@ public function sizeFormatted(): string
return bytes_formatted($this->size());
}

public function subFolderFormatted(): string
public function subFolderIdentifier(): string
{
return str_replace(DIRECTORY_SEPARATOR, ' '.DIRECTORY_SEPARATOR.' ', $this->subFolder);
return Str::substr(md5($this->subFolder), -8, 8);
}

public function downloadUrl(): string
Expand Down

0 comments on commit 81e6f3c

Please sign in to comment.