Beta version

This commit is too large to list all changes.
This commit is contained in:
2020-06-27 00:32:33 +01:00
parent d15c0cb12f
commit 5a8c080a31
135 changed files with 8534 additions and 12774 deletions

View File

@@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LogViewer - Created by ARCANEDEV</title>
<meta name="description" content="LogViewer">
<meta name="author" content="ARCANEDEV">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link href='https://fonts.googleapis.com/css?family=Montserrat:400,700|Source+Sans+Pro:400,600' rel='stylesheet' type='text/css'>
<style>
html {
position: relative;
min-height: 100%;
}
body {
padding-top: 50px;
/* Margin bottom by footer height */
margin-bottom: 50px;
font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, sans-serif;
font-weight: 600;
}
h1, h2, h3 {
font-family: 'Montserrat', 'Helvetica Neue', Helvetica, sans-serif;
}
.sub-header {
padding-bottom: 10px;
border-bottom: 1px solid #EEE;
}
.navbar-inverse {
background-color: #1a237e;
border-color: #1a237e;
}
.navbar-inverse .navbar-nav>.active>a,
.navbar-inverse .navbar-nav>.active>a:focus,
.navbar-inverse .navbar-nav>.active>a:hover {
background-color: #3949ab;
}
.navbar-inverse .navbar-brand {
color: #c5cae9;
}
.navbar-inverse .navbar-nav>li>a {
color: #c5cae9;
}
.navbar-fixed-top {
border: 0;
}
.main {
padding: 20px;
}
.main .page-header {
margin-top: 0;
}
footer.main-footer {
position: absolute;
padding: 10px 0;
bottom: 0;
width: 100%;
background-color: #e8eaf6;
font-weight: 600;
}
footer.main-footer p {
margin: 0;
}
footer.main-footer i.fa.fa-heart {
color: #C62828;
}
.pagination {
margin: 0;
}
.pagination > li > a,
.pagination > li > span {
padding: 4px 10px;
}
.table-condensed > tbody > tr > td.stack,
.table-condensed > tfoot > tr > td.stack,
.table-condensed > thead > tr > td.stack {
padding: 0;
border-top: none;
background-color: #F6F6F6;
border-top: 1px solid #D1D1D1;
max-width: 0;
overflow-x: auto;
}
.table-condensed > tbody > tr > td > p {
margin: 0;
}
.stack-content {
padding: 8px;
color: #AE0E0E;
font-family: consolas, Menlo, Courier, monospace;
font-size: 12px;
font-weight: 400;
white-space: pre-line;
}
.info-box.level {
display: block;
padding: 0;
margin-bottom: 15px;
min-height: 70px;
background: #fff;
width: 100%;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
border-radius: 2px;
}
.info-box.level .info-box-text,
.info-box.level .info-box-number,
.info-box.level .info-box-icon > i {
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.info-box.level .info-box-text {
display: block;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info-box.level .info-box-content {
padding: 5px 10px;
margin-left: 70px;
}
.info-box.level .info-box-number {
display: block;
font-weight: bold;
font-size: 18px;
}
.info-box.level .info-box-icon {
border-radius: 2px 0 0 2px;
display: block;
float: left;
height: 70px; width: 70px;
text-align: center;
font-size: 40px;
line-height: 70px;
background: rgba(0,0,0,0.2);
}
.info-box.level .progress {
background: rgba(0,0,0,0.2);
margin: 5px -10px 5px -10px;
height: 2px;
}
.info-box.level .progress .progress-bar {
background: #fff;
}
.info-box.level-empty {
opacity: .6;
-webkit-filter: grayscale(1);
-moz-filter: grayscale(1);
-ms-filter: grayscale(1);
filter: grayscale(1);
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
-webkit-transition-property: -webkit-filter, opacity;
-moz-transition-property: -moz-filter, opacity;
-o-transition-property: filter, opacity;
transition-property: -webkit-filter, -moz-filter, -o-filter, filter, opacity;
}
.info-box.level-empty:hover {
opacity: 1;
-webkit-filter: grayscale(0);
-moz-filter: grayscale(0);
-ms-filter: grayscale(0);
filter: grayscale(0);
}
.level {
padding: 2px 6px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
border-radius: 2px;
font-size: .9em;
font-weight: 600;
}
.badge.level-all,
.badge.level-emergency,
.badge.level-alert,
.badge.level-critical,
.badge.level-error,
.badge.level-warning,
.badge.level-notice,
.badge.level-info,
.badge.level-debug,
.level, .level i,
.info-box.level-all,
.info-box.level-emergency,
.info-box.level-alert,
.info-box.level-critical,
.info-box.level-error,
.info-box.level-warning,
.info-box.level-notice,
.info-box.level-info,
.info-box.level-debug {
color: #FFF;
}
.label-env {
font-size: .85em;
}
.badge.level-all, .level.level-all, .info-box.level-all {
background-color: {{ log_styler()->color('all') }};
}
.badge.level-emergency, .level.level-emergency, .info-box.level-emergency {
background-color: {{ log_styler()->color('emergency') }};
}
.badge.level-alert, .level.level-alert, .info-box.level-alert {
background-color: {{ log_styler()->color('alert') }};
}
.badge.level-critical, .level.level-critical, .info-box.level-critical {
background-color: {{ log_styler()->color('critical') }};
}
.badge.level-error, .level.level-error, .info-box.level-error {
background-color: {{ log_styler()->color('error') }};
}
.badge.level-warning, .level.level-warning, .info-box.level-warning {
background-color: {{ log_styler()->color('warning') }};
}
.badge.level-notice, .level.level-notice, .info-box.level-notice {
background-color: {{ log_styler()->color('notice') }};
}
.badge.level-info, .level.level-info, .info-box.level-info {
background-color: {{ log_styler()->color('info') }};
}
.badge.level-debug, .level.level-debug, .info-box.level-debug {
background-color: {{ log_styler()->color('debug') }};
}
.badge.level-empty, .level.level-empty {
background-color: {{ log_styler()->color('empty') }};
}
.badge.label-env, .label.label-env {
background-color: #6A1B9A;
}
</style>
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
{{-- Navbar --}}
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">@lang('Toggle navigation')</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="{{ route('log-viewer::dashboard') }}" class="navbar-brand">
<i class="fa fa-fw fa-book"></i> LogViewer
</a>
</div>
<div class="collapse navbar-collapse" id="navbar">
<ul class="nav navbar-nav">
<li class="{{ Route::is('log-viewer::dashboard') ? 'active' : '' }}">
<a href="{{ route('log-viewer::dashboard') }}">
<i class="fa fa-dashboard"></i> @lang('Dashboard')
</a>
</li>
<li class="{{ Route::is('log-viewer::logs.list') ? 'active' : '' }}">
<a href="{{ route('log-viewer::logs.list') }}">
<i class="fa fa-archive"></i> @lang('Logs')
</a>
</li>
</ul>
</div>
</div>
</nav>
{{-- Main container --}}
<main class="container-fluid">
@yield('content')
</main>
{{-- Footer --}}
<footer class="main-footer">
<div class="container">
<p class="text-muted pull-left">
LogViewer - <span class="label label-info">version {{ log_viewer()->version() }}</span>
</p>
<p class="text-muted pull-right">
Created with <i class="fa fa-heart"></i> by ARCANEDEV <sup>&copy;</sup>
</p>
</div>
</footer>
{{-- Scripts --}}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js"></script>
<script>
Chart.defaults.global.responsive = true;
Chart.defaults.global.scaleFontFamily = "'Source Sans Pro'";
Chart.defaults.global.animationEasing = "easeOutQuart";
</script>
@yield('modals')
@yield('scripts')
</body>
</html>

View File

@@ -0,0 +1,52 @@
@extends('log-viewer::bootstrap-3._master')
@section('content')
<h1 class="page-header">@lang('Dashboard')</h1>
<div class="row">
<div class="col-md-3">
<canvas id="stats-doughnut-chart" height="300"></canvas>
</div>
<div class="col-md-9">
<section class="box-body">
<div class="row">
@foreach($percents as $level => $item)
<div class="col-md-4">
<div class="info-box level level-{{ $level }} {{ $item['count'] === 0 ? 'level-empty' : '' }}">
<span class="info-box-icon">
{{ log_styler()->icon($level) }}
</span>
<div class="info-box-content">
<span class="info-box-text">{{ $item['name'] }}</span>
<span class="info-box-number">
{{ $item['count'] }} @lang('entries') - {!! $item['percent'] !!} %
</span>
<div class="progress">
<div class="progress-bar" style="width: {{ $item['percent'] }}%"></div>
</div>
</div>
</div>
</div>
@endforeach
</div>
</section>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function() {
new Chart($('canvas#stats-doughnut-chart'), {
type: 'doughnut',
data: {!! $chartData !!},
options: {
legend: {
position: 'bottom'
}
}
});
});
</script>
@endsection

View File

@@ -0,0 +1,152 @@
@extends('log-viewer::bootstrap-3._master')
<?php /** @var Illuminate\Pagination\LengthAwarePaginator $rows */ ?>
@section('content')
<h1 class="page-header">@lang('Logs')</h1>
{{ $rows->render() }}
<div class="table-responsive">
<table class="table table-condensed table-hover table-stats">
<thead>
<tr>
@foreach($headers as $key => $header)
<th class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="label label-info">{{ $header }}</span>
@else
<span class="level level-{{ $key }}">
{{ log_styler()->icon($key) }} {{ $header }}
</span>
@endif
</th>
@endforeach
<th class="text-right">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($rows as $date => $row)
<tr>
@foreach($row as $key => $value)
<td class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="label label-primary">{{ $value }}</span>
@elseif ($value == 0)
<span class="level level-empty">{{ $value }}</span>
@else
<a href="{{ route('log-viewer::logs.filter', [$date, $key]) }}">
<span class="level level-{{ $key }}">{{ $value }}</span>
</a>
@endif
</td>
@endforeach
<td class="text-right">
<a href="{{ route('log-viewer::logs.show', [$date]) }}" class="btn btn-xs btn-info">
<i class="fa fa-search"></i>
</a>
<a href="{{ route('log-viewer::logs.download', [$date]) }}" class="btn btn-xs btn-success">
<i class="fa fa-download"></i>
</a>
<a href="#delete-log-modal" class="btn btn-xs btn-danger" data-log-date="{{ $date }}">
<i class="fa fa-trash-o"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="11" class="text-center">
<span class="label label-default">{{ trans('log-viewer::general.empty-logs') }}</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{ $rows->render() }}
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div id="delete-log-modal" class="modal fade">
<div class="modal-dialog">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="date" value="">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">@lang('Delete log file')</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-default pull-left" data-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function () {
var deleteLogModal = $('div#delete-log-modal'),
deleteLogForm = $('form#delete-log-form'),
submitBtn = deleteLogForm.find('button[type=submit]');
$("a[href=#delete-log-modal]").on('click', function(event) {
event.preventDefault();
var date = $(this).data('log-date'),
message = "@lang('Are you sure you want to DELETE this log file: :date ?')";
deleteLogForm.find('input[name=date]').val(date);
deleteLogModal.find('.modal-body p').html(message.replace(':date', date));
deleteLogModal.modal('show');
});
deleteLogForm.on('submit', function(event) {
event.preventDefault();
submitBtn.button('loading');
$.ajax({
url: $(this).attr('action'),
type: $(this).attr('method'),
dataType: 'json',
data: $(this).serialize(),
success: function(data) {
submitBtn.button('reset');
if (data.result === 'success') {
deleteLogModal.modal('hide');
location.reload();
}
else {
alert('AJAX ERROR ! Check the console !');
console.error(data);
}
},
error: function(xhr, textStatus, errorThrown) {
alert('AJAX ERROR ! Check the console !');
console.error(errorThrown);
submitBtn.button('reset');
}
});
return false;
});
deleteLogModal.on('hidden.bs.modal', function() {
deleteLogForm.find('input[name=date]').val('');
deleteLogModal.find('.modal-body p').html('');
});
});
</script>
@endsection

View File

@@ -0,0 +1,287 @@
<?php
/**
* @var Arcanedev\LogViewer\Entities\Log $log
* @var Illuminate\Pagination\LengthAwarePaginator $entries
* @var string|null $query
*/
?>
@extends('log-viewer::bootstrap-3._master')
@section('content')
<h1 class="page-header">@lang('Log') [{{ $log->date }}]</h1>
<div class="row">
<div class="col-md-2">
<div class="panel panel-default">
<div class="panel-heading"><i class="fa fa-fw fa-flag"></i> @lang('Levels')</div>
<ul class="list-group">
@foreach($log->menu() as $levelKey => $item)
@if ($item['count'] === 0)
<a href="#" class="list-group-item disabled">
<span class="badge">
{{ $item['count'] }}
</span>
{!! $item['icon'] !!} {{ $item['name'] }}
</a>
@else
<a href="{{ $item['url'] }}" class="list-group-item {{ $levelKey }}">
<span class="badge level-{{ $levelKey }}">
{{ $item['count'] }}
</span>
<span class="level level-{{ $levelKey }}">
{!! $item['icon'] !!} {{ $item['name'] }}
</span>
</a>
@endif
@endforeach
</ul>
</div>
</div>
<div class="col-md-10">
{{-- Log Details --}}
<div class="panel panel-default">
<div class="panel-heading">
@lang('Log info') :
<div class="group-btns pull-right">
<a href="{{ route('log-viewer::logs.download', [$log->date]) }}" class="btn btn-xs btn-success">
<i class="fa fa-download"></i> @lang('Download')
</a>
<a href="#delete-log-modal" class="btn btn-xs btn-danger" data-toggle="modal">
<i class="fa fa-trash-o"></i> @lang('Delete')
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td>@lang('File path') :</td>
<td colspan="5">{{ $log->getPath() }}</td>
</tr>
</thead>
<tbody>
<tr>
<td>@lang('Log entries') :</td>
<td>
<span class="label label-primary">{{ $entries->total() }}</span>
</td>
<td>@lang('Size') :</td>
<td>
<span class="label label-primary">{{ $log->size() }}</span>
</td>
<td>@lang('Created at') :</td>
<td>
<span class="label label-primary">{{ $log->createdAt() }}</span>
</td>
<td>@lang('Updated at') :</td>
<td>
<span class="label label-primary">{{ $log->updatedAt() }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-footer">
{{-- Search --}}
<form action="{{ route('log-viewer::logs.search', [$log->date, $level]) }}" method="GET">
<div class=form-group">
<div class="input-group">
<input id="query" name="query" class="form-control" value="{{ $query }}" placeholder="@lang('Type here to search')">
<span class="input-group-btn">
@unless (is_null($query))
<a href="{{ route('log-viewer::logs.show', [$log->date]) }}" class="btn btn-default">
(@lang(':count results', ['count' => $entries->count()])) <span class="glyphicon glyphicon-remove"></span>
</a>
@endunless
<button id="search-btn" class="btn btn-primary">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
</div>
</form>
</div>
</div>
{{-- Log Entries --}}
<div class="panel panel-default">
@if ($entries->hasPages())
<div class="panel-heading">
{{ $entries->appends(compact('query'))->render() }}
<span class="label label-info pull-right">
{{ __('Page :current of :last', ['current' => $entries->currentPage(), 'last' => $entries->lastPage()]) }}
</span>
</div>
@endif
<div class="table-responsive">
<table id="entries" class="table table-condensed">
<thead>
<tr>
<th>@lang('ENV')</th>
<th style="width: 120px;">@lang('Level')</th>
<th style="width: 65px;">@lang('Time')</th>
<th>@lang('Header')</th>
<th class="text-right">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($entries as $key => $entry)
<?php /** @var Arcanedev\LogViewer\Entities\LogEntry $entry */ ?>
<tr>
<td>
<span class="label label-env">{{ $entry->env }}</span>
</td>
<td>
<span class="level level-{{ $entry->level }}">{!! $entry->level() !!}</span>
</td>
<td>
<span class="label label-default">
{{ $entry->datetime->format('H:i:s') }}
</span>
</td>
<td>
<p>{{ $entry->header }}</p>
</td>
<td class="text-right">
@if ($entry->hasStack())
<a class="btn btn-xs btn-default" role="button" data-toggle="collapse"
href="#log-stack-{{ $key }}" aria-expanded="false" aria-controls="log-stack-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Stack')
</a>
@endif
@if ($entry->hasContext())
<a class="btn btn-xs btn-default" role="button" data-toggle="collapse"
href="#log-context-{{ $key }}" aria-expanded="false" aria-controls="log-context-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Context')
</a>
@endif
</td>
</tr>
@if ($entry->hasStack() || $entry->hasContext())
<tr>
<td colspan="5" class="stack">
@if ($entry->hasStack())
<div class="stack-content collapse" id="log-stack-{{ $key }}">
{!! $entry->stack() !!}
</div>
@endif
@if ($entry->hasContext())
<div class="stack-content collapse" id="log-context-{{ $key }}">
<pre>{{ $entry->context() }}</pre>
</div>
@endif
</td>
</tr>
@endif
@empty
<tr>
<td colspan="5" class="text-center">
<span class="label label-default">@lang('The list of logs is empty!')</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if ($entries->hasPages())
<div class="panel-footer">
{!! $entries->appends(compact('query'))->render() !!}
<span class="label label-info pull-right">
@lang('Page :current of :last', ['current' => $entries->currentPage(), 'last' => $entries->lastPage()])
</span>
</div>
@endif
</div>
</div>
</div>
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div id="delete-log-modal" class="modal fade">
<div class="modal-dialog">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="date" value="{{ $log->date }}">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">@lang('Delete log file')</h4>
</div>
<div class="modal-body">
<p>@lang('Are you sure you want to delete this log file: :date ?', ['date' => $log->date])</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-default pull-left" data-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function () {
var deleteLogModal = $('div#delete-log-modal'),
deleteLogForm = $('form#delete-log-form'),
submitBtn = deleteLogForm.find('button[type=submit]');
deleteLogForm.on('submit', function(event) {
event.preventDefault();
submitBtn.button('loading');
$.ajax({
url: $(this).attr('action'),
type: $(this).attr('method'),
dataType: 'json',
data: $(this).serialize(),
success: function(data) {
submitBtn.button('reset');
if (data.result === 'success') {
deleteLogModal.modal('hide');
location.replace("{{ route('log-viewer::logs.list') }}");
}
else {
alert('OOPS ! This is a lack of coffee exception !')
}
},
error: function(xhr, textStatus, errorThrown) {
alert('AJAX ERROR ! Check the console !');
console.error(errorThrown);
submitBtn.button('reset');
}
});
return false;
});
@unless (empty(log_styler()->toHighlight()))
@php
$htmlHighlight = version_compare(PHP_VERSION, '7.4.0') >= 0
? join('|', log_styler()->toHighlight())
: join(log_styler()->toHighlight(), '|');
@endphp
$('.stack-content').each(function() {
var $this = $(this);
var html = $this.html().trim()
.replace(/({!! $htmlHighlight !!})/gm, '<strong>$1</strong>');
$this.html(html);
});
@endunless
});
</script>
@endsection

View File

@@ -0,0 +1,266 @@
<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="LogViewer">
<meta name="author" content="ARCANEDEV">
<title>LogViewer - Created by ARCANEDEV</title>
{{-- Styles --}}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
<link href='https://fonts.googleapis.com/css?family=Montserrat:400,700|Source+Sans+Pro:400,600' rel='stylesheet' type='text/css'>
<style>
html {
position: relative;
min-height: 100%;
}
body {
font-size: .875rem;
margin-bottom: 60px;
}
.main-footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px;
line-height: 60px;
background-color: #E8EAF6;
}
.main-footer p {
margin-bottom: 0;
}
.main-footer .fa.fa-heart {
color: #C62828;
}
.page-header {
border-bottom: 1px solid #8a8a8a;
}
/*
* Navbar
*/
.navbar-brand {
padding: .75rem 1rem;
font-size: 1rem;
}
.navbar-nav .nav-link {
padding-right: .5rem;
padding-left: .5rem;
}
/*
* Boxes
*/
.box {
display: block;
padding: 0;
min-height: 70px;
background: #fff;
width: 100%;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
border-radius: .25rem;
}
.box > .box-icon > i,
.box .box-content .box-text,
.box .box-content .box-number {
color: #FFF;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.box > .box-icon {
border-radius: 2px 0 0 2px;
display: block;
float: left;
height: 70px; width: 70px;
text-align: center;
font-size: 40px;
line-height: 70px;
background: rgba(0,0,0,0.2);
}
.box .box-content {
padding: 5px 10px;
margin-left: 70px;
}
.box .box-content .box-text {
display: block;
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.box .box-content .box-number {
display: block;
}
.box .box-content .progress {
background: rgba(0,0,0,0.2);
margin: 5px -10px 5px -10px;
}
.box .box-content .progress .progress-bar {
background-color: #FFF;
}
/*
* Log Menu
*/
.log-menu .list-group-item.disabled {
cursor: not-allowed;
}
.log-menu .list-group-item.disabled .level-name {
color: #D1D1D1;
}
/*
* Log Entry
*/
.stack-content {
color: #AE0E0E;
font-family: consolas, Menlo, Courier, monospace;
white-space: pre-line;
font-size: .8rem;
}
/*
* Colors: Badge & Infobox
*/
.badge.badge-env,
.badge.badge-level-all,
.badge.badge-level-emergency,
.badge.badge-level-alert,
.badge.badge-level-critical,
.badge.badge-level-error,
.badge.badge-level-warning,
.badge.badge-level-notice,
.badge.badge-level-info,
.badge.badge-level-debug,
.badge.empty {
color: #FFF;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.badge.badge-level-all,
.box.level-all {
background-color: {{ log_styler()->color('all') }};
}
.badge.badge-level-emergency,
.box.level-emergency {
background-color: {{ log_styler()->color('emergency') }};
}
.badge.badge-level-alert,
.box.level-alert {
background-color: {{ log_styler()->color('alert') }};
}
.badge.badge-level-critical,
.box.level-critical {
background-color: {{ log_styler()->color('critical') }};
}
.badge.badge-level-error,
.box.level-error {
background-color: {{ log_styler()->color('error') }};
}
.badge.badge-level-warning,
.box.level-warning {
background-color: {{ log_styler()->color('warning') }};
}
.badge.badge-level-notice,
.box.level-notice {
background-color: {{ log_styler()->color('notice') }};
}
.badge.badge-level-info,
.box.level-info {
background-color: {{ log_styler()->color('info') }};
}
.badge.badge-level-debug,
.box.level-debug {
background-color: {{ log_styler()->color('debug') }};
}
.badge.empty,
.box.empty {
background-color: {{ log_styler()->color('empty') }};
}
.badge.badge-env {
background-color: #6A1B9A;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark sticky-top bg-dark p-0">
<a href="{{ route('log-viewer::dashboard') }}" class="navbar-brand mr-0">
<i class="fa fa-fw fa-book"></i> LogViewer
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item {{ Route::is('log-viewer::dashboard') ? 'active' : '' }}">
<a href="{{ route('log-viewer::dashboard') }}" class="nav-link">
<i class="fa fa-dashboard"></i> @lang('Dashboard')
</a>
</li>
<li class="nav-item {{ Route::is('log-viewer::logs.list') ? 'active' : '' }}">
<a href="{{ route('log-viewer::logs.list') }}" class="nav-link">
<i class="fa fa-archive"></i> @lang('Logs')
</a>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<main role="main" class="pt-3">
@yield('content')
</main>
</div>
{{-- Footer --}}
<footer class="main-footer">
<div class="container-fluid">
<p class="text-muted pull-left">
LogViewer - <span class="badge badge-info">version {{ log_viewer()->version() }}</span>
</p>
<p class="text-muted pull-right">
Created with <i class="fa fa-heart"></i> by ARCANEDEV <sup>&copy;</sup>
</p>
</div>
</footer>
{{-- Scripts --}}
<script src="https://code.jquery.com/jquery-3.2.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
@yield('modals')
@yield('scripts')
</body>
</html>

View File

@@ -0,0 +1,53 @@
@extends('log-viewer::bootstrap-4._master')
@section('content')
<div class="page-header mb-4">
<h1>@lang('Dashboard')</h1>
</div>
<div class="row">
<div class="col-md-6 col-lg-3">
<canvas id="stats-doughnut-chart" height="300" class="mb-3"></canvas>
</div>
<div class="col-md-6 col-lg-9">
<div class="row">
@foreach($percents as $level => $item)
<div class="col-sm-6 col-md-12 col-lg-4 mb-3">
<div class="box level-{{ $level }} {{ $item['count'] === 0 ? 'empty' : '' }}">
<div class="box-icon">
{!! log_styler()->icon($level) !!}
</div>
<div class="box-content">
<span class="box-text">{{ $item['name'] }}</span>
<span class="box-number">
{{ $item['count'] }} @lang('entries') - {!! $item['percent'] !!} %
</span>
<div class="progress" style="height: 3px;">
<div class="progress-bar" style="width: {{ $item['percent'] }}%"></div>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function() {
new Chart(document.getElementById("stats-doughnut-chart"), {
type: 'doughnut',
data: {!! $chartData !!},
options: {
legend: {
position: 'bottom'
}
}
});
});
</script>
@endsection

View File

@@ -0,0 +1,152 @@
@extends('log-viewer::bootstrap-4._master')
<?php /** @var Illuminate\Pagination\LengthAwarePaginator $rows */ ?>
@section('content')
<div class="page-header mb-4">
<h1>@lang('Logs')</h1>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
@foreach($headers as $key => $header)
<th scope="col" class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="badge badge-info">{{ $header }}</span>
@else
<span class="badge badge-level-{{ $key }}">
{{ log_styler()->icon($key) }} {{ $header }}
</span>
@endif
</th>
@endforeach
<th scope="col" class="text-right">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($rows as $date => $row)
<tr>
@foreach($row as $key => $value)
<td class="{{ $key == 'date' ? 'text-left' : 'text-center' }}">
@if ($key == 'date')
<span class="badge badge-primary">{{ $value }}</span>
@elseif ($value == 0)
<span class="badge empty">{{ $value }}</span>
@else
<a href="{{ route('log-viewer::logs.filter', [$date, $key]) }}">
<span class="badge badge-level-{{ $key }}">{{ $value }}</span>
</a>
@endif
</td>
@endforeach
<td class="text-right">
<a href="{{ route('log-viewer::logs.show', [$date]) }}" class="btn btn-sm btn-info">
<i class="fa fa-search"></i>
</a>
<a href="{{ route('log-viewer::logs.download', [$date]) }}" class="btn btn-sm btn-success">
<i class="fa fa-download"></i>
</a>
<a href="#delete-log-modal" class="btn btn-sm btn-danger" data-log-date="{{ $date }}">
<i class="fa fa-trash-o"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="11" class="text-center">
<span class="badge badge-secondary">@lang('The list of logs is empty!')</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{ $rows->render() }}
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div id="delete-log-modal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="date" value="">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@lang('Delete log file')</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary mr-auto" data-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function () {
var deleteLogModal = $('div#delete-log-modal'),
deleteLogForm = $('form#delete-log-form'),
submitBtn = deleteLogForm.find('button[type=submit]');
$("a[href='#delete-log-modal']").on('click', function(event) {
event.preventDefault();
var date = $(this).data('log-date'),
message = "{{ __('Are you sure you want to delete this log file: :date ?') }}";
deleteLogForm.find('input[name=date]').val(date);
deleteLogModal.find('.modal-body p').html(message.replace(':date', date));
deleteLogModal.modal('show');
});
deleteLogForm.on('submit', function(event) {
event.preventDefault();
submitBtn.button('loading');
$.ajax({
url: $(this).attr('action'),
type: $(this).attr('method'),
dataType: 'json',
data: $(this).serialize(),
success: function(data) {
submitBtn.button('reset');
if (data.result === 'success') {
deleteLogModal.modal('hide');
location.reload();
}
else {
alert('AJAX ERROR ! Check the console !');
console.error(data);
}
},
error: function(xhr, textStatus, errorThrown) {
alert('AJAX ERROR ! Check the console !');
console.error(errorThrown);
submitBtn.button('reset');
}
});
return false;
});
deleteLogModal.on('hidden.bs.modal', function() {
deleteLogForm.find('input[name=date]').val('');
deleteLogModal.find('.modal-body p').html('');
});
});
</script>
@endsection

View File

@@ -0,0 +1,274 @@
<?php
/**
* @var Arcanedev\LogViewer\Entities\Log $log
* @var Illuminate\Pagination\LengthAwarePaginator $entries
* @var string|null $query
*/
?>
@extends('log-viewer::bootstrap-4._master')
@section('content')
<div class="page-header mb-4">
<h1>@lang('Log') [{{ $log->date }}]</h1>
</div>
<div class="row">
<div class="col-lg-2">
{{-- Log Menu --}}
<div class="card mb-4">
<div class="card-header"><i class="fa fa-fw fa-flag"></i> @lang('Levels')</div>
<div class="list-group list-group-flush log-menu">
@foreach($log->menu() as $levelKey => $item)
@if ($item['count'] === 0)
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center disabled">
<span class="level-name">{!! $item['icon'] !!} {{ $item['name'] }}</span>
<span class="badge empty">{{ $item['count'] }}</span>
</a>
@else
<a href="{{ $item['url'] }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center level-{{ $levelKey }}{{ $level === $levelKey ? ' active' : ''}}">
<span class="level-name">{!! $item['icon'] !!} {{ $item['name'] }}</span>
<span class="badge badge-level-{{ $levelKey }}">{{ $item['count'] }}</span>
</a>
@endif
@endforeach
</div>
</div>
</div>
<div class="col-lg-10">
{{-- Log Details --}}
<div class="card mb-4">
<div class="card-header">
@lang('Log info') :
<div class="group-btns pull-right">
<a href="{{ route('log-viewer::logs.download', [$log->date]) }}" class="btn btn-sm btn-success">
<i class="fa fa-download"></i> @lang('Download')
</a>
<a href="#delete-log-modal" class="btn btn-sm btn-danger" data-toggle="modal">
<i class="fa fa-trash-o"></i> @lang('Delete')
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-condensed mb-0">
<tbody>
<tr>
<td>@lang('File path') :</td>
<td colspan="7">{{ $log->getPath() }}</td>
</tr>
<tr>
<td>@lang('Log entries') :</td>
<td>
<span class="badge badge-primary">{{ $entries->total() }}</span>
</td>
<td>@lang('Size') :</td>
<td>
<span class="badge badge-primary">{{ $log->size() }}</span>
</td>
<td>@lang('Created at') :</td>
<td>
<span class="badge badge-primary">{{ $log->createdAt() }}</span>
</td>
<td>@lang('Updated at') :</td>
<td>
<span class="badge badge-primary">{{ $log->updatedAt() }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer">
{{-- Search --}}
<form action="{{ route('log-viewer::logs.search', [$log->date, $level]) }}" method="GET">
<div class="form-group">
<div class="input-group">
<input id="query" name="query" class="form-control" value="{{ $query }}" placeholder="@lang('Type here to search')">
<div class="input-group-append">
@unless (is_null($query))
<a href="{{ route('log-viewer::logs.show', [$log->date]) }}" class="btn btn-secondary">
(@lang(':count results', ['count' => $entries->count()])) <i class="fa fa-fw fa-times"></i>
</a>
@endunless
<button id="search-btn" class="btn btn-primary">
<span class="fa fa-fw fa-search"></span>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
{{-- Log Entries --}}
<div class="card mb-4">
@if ($entries->hasPages())
<div class="card-header">
<span class="badge badge-info float-right">
{{ __('Page :current of :last', ['current' => $entries->currentPage(), 'last' => $entries->lastPage()]) }}
</span>
</div>
@endif
<div class="table-responsive">
<table id="entries" class="table mb-0">
<thead>
<tr>
<th>@lang('ENV')</th>
<th style="width: 120px;">@lang('Level')</th>
<th style="width: 65px;">@lang('Time')</th>
<th>@lang('Header')</th>
<th class="text-right">@lang('Actions')</th>
</tr>
</thead>
<tbody>
@forelse($entries as $key => $entry)
<?php /** @var Arcanedev\LogViewer\Entities\LogEntry $entry */ ?>
<tr>
<td>
<span class="badge badge-env">{{ $entry->env }}</span>
</td>
<td>
<span class="badge badge-level-{{ $entry->level }}">
{!! $entry->level() !!}
</span>
</td>
<td>
<span class="badge badge-secondary">
{{ $entry->datetime->format('H:i:s') }}
</span>
</td>
<td>
{{ $entry->header }}
</td>
<td class="text-right">
@if ($entry->hasStack())
<a class="btn btn-sm btn-light" role="button" data-toggle="collapse"
href="#log-stack-{{ $key }}" aria-expanded="false" aria-controls="log-stack-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Stack')
</a>
@endif
@if ($entry->hasContext())
<a class="btn btn-sm btn-light" role="button" data-toggle="collapse"
href="#log-context-{{ $key }}" aria-expanded="false" aria-controls="log-context-{{ $key }}">
<i class="fa fa-toggle-on"></i> @lang('Context')
</a>
@endif
</td>
</tr>
@if ($entry->hasStack() || $entry->hasContext())
<tr>
<td colspan="5" class="stack py-0">
@if ($entry->hasStack())
<div class="stack-content collapse" id="log-stack-{{ $key }}">
{!! $entry->stack() !!}
</div>
@endif
@if ($entry->hasContext())
<div class="stack-content collapse" id="log-context-{{ $key }}">
<pre>{{ $entry->context() }}</pre>
</div>
@endif
</td>
</tr>
@endif
@empty
<tr>
<td colspan="5" class="text-center">
<span class="badge badge-secondary">@lang('The list of logs is empty!')</span>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
{!! $entries->appends(compact('query'))->render() !!}
</div>
</div>
@endsection
@section('modals')
{{-- DELETE MODAL --}}
<div id="delete-log-modal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<form id="delete-log-form" action="{{ route('log-viewer::logs.delete') }}" method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<input type="hidden" name="date" value="{{ $log->date }}">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">@lang('Delete log file')</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>@lang('Are you sure you want to delete this log file: :date ?', ['date' => $log->date])</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary mr-auto" data-dismiss="modal">@lang('Cancel')</button>
<button type="submit" class="btn btn-sm btn-danger" data-loading-text="@lang('Loading')&hellip;">@lang('Delete')</button>
</div>
</div>
</form>
</div>
</div>
@endsection
@section('scripts')
<script>
$(function () {
var deleteLogModal = $('div#delete-log-modal'),
deleteLogForm = $('form#delete-log-form'),
submitBtn = deleteLogForm.find('button[type=submit]');
deleteLogForm.on('submit', function(event) {
event.preventDefault();
submitBtn.button('loading');
$.ajax({
url: $(this).attr('action'),
type: $(this).attr('method'),
dataType: 'json',
data: $(this).serialize(),
success: function(data) {
submitBtn.button('reset');
if (data.result === 'success') {
deleteLogModal.modal('hide');
location.replace("{{ route('log-viewer::logs.list') }}");
}
else {
alert('OOPS ! This is a lack of coffee exception !')
}
},
error: function(xhr, textStatus, errorThrown) {
alert('AJAX ERROR ! Check the console !');
console.error(errorThrown);
submitBtn.button('reset');
}
});
return false;
});
@unless (empty(log_styler()->toHighlight()))
@php
$htmlHighlight = version_compare(PHP_VERSION, '7.4.0') >= 0
? join('|', log_styler()->toHighlight())
: join(log_styler()->toHighlight(), '|');
@endphp
$('.stack-content').each(function() {
var $this = $(this);
var html = $this.html().trim()
.replace(/({!! $htmlHighlight !!})/gm, '<strong>$1</strong>');
$this.html(html);
});
@endunless
});
</script>
@endsection