init
This commit is contained in:
10 files changed
+842
No files matched your search
@@ -0,0 +1,8 @@
|
||||
# Use the lightest official nginx image
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy static files to the nginx server directory
|
||||
COPY . /usr/share/nginx/html
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,43 @@
|
||||
/* jetbrains-mono-regular - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../fonts/jetbrains-mono-v24-latin-regular.woff2') format('woff2');
|
||||
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* jetbrains-mono-500 - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('../fonts/jetbrains-mono-v24-latin-500.woff2') format('woff2');
|
||||
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* dm-sans-300 - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'DM Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('../fonts/dm-sans-v17-latin-300.woff2') format('woff2');
|
||||
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* dm-sans-regular - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'DM Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../fonts/dm-sans-v17-latin-regular.woff2') format('woff2');
|
||||
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+776
@@ -0,0 +1,776 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Log Viewer</title>
|
||||
<link rel="stylesheet" href="css/fonts.css">
|
||||
<script src="js/chart.umd.min.js"></script>
|
||||
<script src="js/chartjs-plugin-zoom.min.js"></script>
|
||||
<script src="js/hammer.min.js"></script>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#0d0f12;--surface:#131619;--surface2:#191c21;--surface3:#1e2128;
|
||||
--border:#ffffff0f;--border2:#ffffff18;--border3:#ffffff28;
|
||||
--text:#e8eaf0;--text2:#8b8f9a;--text3:#555966;
|
||||
--accent:#4f8ef7;--accent-dim:#4f8ef720;
|
||||
--debug:#555966;--info:#4f8ef7;--warn:#f5a623;--error:#e8504a;--fatal:#c72b2b;
|
||||
--debug-bg:#55596610;--info-bg:#4f8ef710;--warn-bg:#f5a62310;--error-bg:#e8504a10;--fatal-bg:#c72b2b18;
|
||||
--radius:6px;--radius-lg:10px;
|
||||
--font-mono:'JetBrains Mono',monospace;
|
||||
--font-sans:'DM Sans',sans-serif;
|
||||
}
|
||||
html,body{height:100%;overflow:hidden}
|
||||
body{background:var(--bg);color:var(--text);font-family:var(--font-sans);font-size:13px;display:flex;flex-direction:column}
|
||||
|
||||
/* Drop overlay */
|
||||
#drop-overlay{
|
||||
position:fixed;inset:0;z-index:9999;
|
||||
background:#0d0f12f0;backdrop-filter:blur(8px);
|
||||
display:none;align-items:center;justify-content:center;
|
||||
flex-direction:column;gap:20px;
|
||||
border:2px dashed var(--accent);
|
||||
pointer-events:none;
|
||||
}
|
||||
#drop-overlay.active{display:flex;pointer-events:all}
|
||||
#drop-overlay .drop-icon{width:64px;height:64px;opacity:.6;animation:pulse 1.4s ease-in-out infinite}
|
||||
@keyframes pulse{0%,100%{transform:scale(1);opacity:.6}50%{transform:scale(1.08);opacity:1}}
|
||||
#drop-overlay .drop-label{font-size:18px;font-weight:500;color:var(--text);letter-spacing:.01em}
|
||||
#drop-overlay .drop-sub{font-size:13px;color:var(--text2)}
|
||||
|
||||
/* Header */
|
||||
#header{
|
||||
display:flex;align-items:center;gap:16px;
|
||||
padding:10px 20px;border-bottom:1px solid var(--border);
|
||||
background:var(--surface);flex-shrink:0;
|
||||
}
|
||||
#logo{font-family:var(--font-mono);font-size:15px;font-weight:500;color:var(--text);letter-spacing:-.02em}
|
||||
#logo span{color:var(--accent)}
|
||||
#file-info{font-family:var(--font-mono);font-size:11px;color:var(--text3);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
#file-btn{
|
||||
display:flex;align-items:center;gap:7px;
|
||||
background:var(--surface3);border:1px solid var(--border2);border-radius:var(--radius);
|
||||
color:var(--text2);font-family:var(--font-sans);font-size:12px;font-weight:500;
|
||||
padding:6px 12px;cursor:pointer;transition:all .15s;white-space:nowrap;
|
||||
}
|
||||
#file-btn:hover{border-color:var(--border3);color:var(--text);background:var(--surface2)}
|
||||
#file-input{display:none}
|
||||
.hdr-sep{width:1px;height:20px;background:var(--border)}
|
||||
|
||||
/* Filter bar */
|
||||
#filter-bar{
|
||||
display:flex;align-items:center;gap:10px;
|
||||
padding:8px 20px;border-bottom:1px solid var(--border);
|
||||
background:var(--surface);flex-shrink:0;
|
||||
}
|
||||
#search-wrap{position:relative;flex:1;max-width:420px}
|
||||
#search-wrap svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);opacity:.4;pointer-events:none}
|
||||
#search{
|
||||
width:100%;background:var(--surface3);border:1px solid var(--border2);border-radius:var(--radius);
|
||||
color:var(--text);font-family:var(--font-mono);font-size:12px;
|
||||
padding:6px 10px 6px 32px;outline:none;transition:border-color .15s;
|
||||
}
|
||||
#search:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-dim)}
|
||||
#search.invalid{border-color:var(--error);box-shadow:0 0 0 3px #e8504a18}
|
||||
#search::placeholder{color:var(--text3)}
|
||||
#regex-badge{
|
||||
position:absolute;right:8px;top:50%;transform:translateY(-50%);
|
||||
font-family:var(--font-mono);font-size:10px;padding:2px 5px;
|
||||
border-radius:3px;background:var(--accent-dim);color:var(--accent);opacity:.7;
|
||||
}
|
||||
|
||||
/* Level pills */
|
||||
.level-pills{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.level-pill{
|
||||
display:flex;align-items:center;gap:6px;
|
||||
padding:5px 10px;border-radius:20px;
|
||||
border:1px solid var(--border2);
|
||||
background:transparent;cursor:pointer;font-family:var(--font-sans);font-size:11px;
|
||||
font-weight:500;color:var(--text2);transition:all .15s;white-space:nowrap;
|
||||
}
|
||||
.level-pill:hover{border-color:var(--border3);color:var(--text)}
|
||||
.level-pill.active{border-color:currentColor}
|
||||
.level-pill[data-level="DEBUG"]{--lc:var(--debug)}
|
||||
.level-pill[data-level="INFO"]{--lc:var(--info)}
|
||||
.level-pill[data-level="WARN"]{--lc:var(--warn)}
|
||||
.level-pill[data-level="ERROR"]{--lc:var(--error)}
|
||||
.level-pill[data-level="FATAL"]{--lc:var(--fatal)}
|
||||
.level-pill.active{color:var(--lc);border-color:var(--lc);background:color-mix(in srgb,var(--lc) 10%,transparent)}
|
||||
.pill-dot{width:6px;height:6px;border-radius:50%;background:var(--lc,var(--text3));flex-shrink:0}
|
||||
.pill-count{font-family:var(--font-mono);font-size:10px;opacity:.7}
|
||||
|
||||
.filter-actions{display:flex;gap:6px;margin-left:auto}
|
||||
#clear-filters{
|
||||
padding:5px 10px;border-radius:var(--radius);border:1px solid var(--border2);
|
||||
background:transparent;color:var(--text3);font-size:11px;font-family:var(--font-sans);
|
||||
cursor:pointer;transition:all .15s;
|
||||
}
|
||||
#clear-filters:hover{color:var(--text);border-color:var(--border3)}
|
||||
#time-badge{
|
||||
display:none;align-items:center;gap:5px;
|
||||
padding:5px 10px;border-radius:var(--radius);
|
||||
background:var(--accent-dim);border:1px solid var(--accent);
|
||||
color:var(--accent);font-size:11px;font-family:var(--font-mono);cursor:pointer;
|
||||
}
|
||||
#time-badge.visible{display:flex}
|
||||
#time-badge:hover{background:#4f8ef730}
|
||||
#time-badge .tx-close{opacity:.6;margin-left:2px}
|
||||
|
||||
/* Main layout */
|
||||
#main{display:flex;flex-direction:column;flex:1;overflow:hidden}
|
||||
|
||||
/* Chart panel */
|
||||
#chart-panel{
|
||||
flex-shrink:0;padding:16px 20px 12px;
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
#chart-wrap{position:relative;height:100px}
|
||||
#chart-hint{position:absolute;top:4px;right:0;font-size:10px;color:var(--text3);font-family:var(--font-mono)}
|
||||
|
||||
/* Table area */
|
||||
#table-area{flex:1;overflow:hidden;display:flex;flex-direction:column}
|
||||
|
||||
/* DataTables custom styles */
|
||||
#dt-wrap{flex:1;overflow:auto;padding:0 20px 20px}
|
||||
table#log-table{width:100%;border-collapse:collapse;font-family:var(--font-mono);font-size:11.5px}
|
||||
table#log-table thead th{
|
||||
position:sticky;top:0;z-index:10;
|
||||
background:var(--surface);border-bottom:1px solid var(--border2);
|
||||
padding:10px 12px;text-align:left;font-weight:500;
|
||||
color:var(--text2);font-size:11px;letter-spacing:.05em;text-transform:uppercase;
|
||||
white-space:nowrap;cursor:pointer;user-select:none;
|
||||
}
|
||||
table#log-table thead th:hover{color:var(--text)}
|
||||
table#log-table thead th.sort-asc::after{content:' ↑'}
|
||||
table#log-table thead th.sort-desc::after{content:' ↓'}
|
||||
table#log-table tbody tr{border-bottom:1px solid var(--border);cursor:pointer;transition:background .1s}
|
||||
table#log-table tbody tr:hover>td{background:var(--surface2)}
|
||||
table#log-table tbody td{padding:7px 12px;vertical-align:top}
|
||||
.col-time{width:180px;color:var(--text3);white-space:nowrap}
|
||||
.col-level{width:72px}
|
||||
.col-msg{word-break:break-word}
|
||||
|
||||
/* Level badges */
|
||||
.lv{display:inline-block;padding:2px 7px;border-radius:3px;font-size:10px;font-weight:500;letter-spacing:.04em}
|
||||
.lv-DEBUG{background:var(--debug-bg);color:var(--debug)}
|
||||
.lv-INFO{background:var(--info-bg);color:var(--info)}
|
||||
.lv-WARN{background:var(--warn-bg);color:var(--warn)}
|
||||
.lv-ERROR{background:var(--error-bg);color:var(--error)}
|
||||
.lv-FATAL{background:var(--fatal-bg);color:var(--fatal)}
|
||||
|
||||
/* Row details (expanded) */
|
||||
.row-detail{background:var(--surface2)!important;border-left:2px solid var(--accent)!important}
|
||||
.row-detail td{padding:0!important}
|
||||
.detail-inner{padding:12px 16px 16px;display:flex;flex-direction:column;gap:12px}
|
||||
.detail-section{display:flex;flex-direction:column;gap:6px}
|
||||
.detail-label{font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--text3)}
|
||||
.detail-val{
|
||||
background:var(--surface3);border:1px solid var(--border);border-radius:var(--radius);
|
||||
padding:10px 12px;font-family:var(--font-mono);font-size:11px;color:var(--text2);
|
||||
white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;
|
||||
line-height:1.6;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
#empty-state{
|
||||
flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:16px;color:var(--text3);
|
||||
}
|
||||
#empty-state svg{opacity:.3}
|
||||
#empty-state .es-title{font-size:15px;font-weight:500;color:var(--text2)}
|
||||
#empty-state .es-sub{font-size:12px;text-align:center;max-width:280px;line-height:1.6}
|
||||
#empty-state .es-btn{
|
||||
margin-top:8px;padding:8px 20px;border-radius:var(--radius);
|
||||
background:var(--accent-dim);border:1px solid var(--accent);
|
||||
color:var(--accent);font-size:13px;font-family:var(--font-sans);cursor:pointer;
|
||||
transition:all .15s;
|
||||
}
|
||||
#empty-state .es-btn:hover{background:#4f8ef730}
|
||||
|
||||
/* Table status bar */
|
||||
#status-bar{
|
||||
display:flex;align-items:center;gap:12px;
|
||||
padding:7px 20px;border-top:1px solid var(--border);
|
||||
background:var(--surface);flex-shrink:0;font-size:11px;color:var(--text3);
|
||||
font-family:var(--font-mono);
|
||||
}
|
||||
#status-bar .status-count{color:var(--text2)}
|
||||
.sb-sep{color:var(--border3)}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar{width:6px;height:6px}
|
||||
::-webkit-scrollbar-track{background:transparent}
|
||||
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
|
||||
::-webkit-scrollbar-thumb:hover{background:var(--border3)}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
|
||||
.fade-in{animation:fadeIn .2s ease forwards}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Drop Overlay -->
|
||||
<div id="drop-overlay">
|
||||
<svg class="drop-icon" width="64" height="64" viewBox="0 0 64 64" fill="none">
|
||||
<rect x="8" y="8" width="48" height="48" rx="8" stroke="#4f8ef7" stroke-width="2" stroke-dasharray="6 4"/>
|
||||
<path d="M32 20v24M22 34l10 10 10-10" stroke="#4f8ef7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div class="drop-label">Drop log file here</div>
|
||||
<div class="drop-sub">Supports .log, .txt, .json and plain text</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div id="header">
|
||||
<div id="logo">log<span>scope</span></div>
|
||||
<div class="hdr-sep"></div>
|
||||
<div id="file-info">No file loaded — drag & drop or select a file</div>
|
||||
<button id="file-btn" onclick="document.getElementById('file-input').click()">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><path d="M2 12V4a1 1 0 011-1h5l2 2h3a1 1 0 011 1v6a1 1 0 01-1 1H3a1 1 0 01-1-1z" stroke="currentColor" stroke-width="1.4"/></svg>
|
||||
Open File
|
||||
</button>
|
||||
<input type="file" id="file-input" accept=".log,.txt,.json,text/plain">
|
||||
</div>
|
||||
|
||||
<!-- Filter bar -->
|
||||
<div id="filter-bar">
|
||||
<div id="search-wrap">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"><circle cx="6.5" cy="6.5" r="4.5" stroke="currentColor" stroke-width="1.4"/><path d="M10.5 10.5L14 14" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
<input type="text" id="search" placeholder="Filter messages… (regex supported)" autocomplete="off">
|
||||
<span id="regex-badge">.*</span>
|
||||
</div>
|
||||
<div class="hdr-sep"></div>
|
||||
<div class="level-pills" id="level-pills">
|
||||
<button class="level-pill" data-level="DEBUG"><span class="pill-dot"></span>Debug<span class="pill-count" id="cnt-DEBUG">0</span></button>
|
||||
<button class="level-pill" data-level="INFO"><span class="pill-dot"></span>Info<span class="pill-count" id="cnt-INFO">0</span></button>
|
||||
<button class="level-pill" data-level="WARN"><span class="pill-dot"></span>Warn<span class="pill-count" id="cnt-WARN">0</span></button>
|
||||
<button class="level-pill" data-level="ERROR"><span class="pill-dot"></span>Error<span class="pill-count" id="cnt-ERROR">0</span></button>
|
||||
<button class="level-pill" data-level="FATAL"><span class="pill-dot"></span>Fatal<span class="pill-count" id="cnt-FATAL">0</span></button>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div id="time-badge">
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.4"/><path d="M8 5v3l2 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
<span id="time-badge-label">time range</span>
|
||||
<span class="tx-close">✕</span>
|
||||
</div>
|
||||
<button id="clear-filters">Clear filters</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div id="main">
|
||||
<!-- Chart -->
|
||||
<div id="chart-panel">
|
||||
<div id="chart-wrap">
|
||||
<canvas id="intensity-chart"></canvas>
|
||||
<span id="chart-hint">drag to zoom · right-click to reset</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state / Table area -->
|
||||
<div id="empty-state">
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<rect x="6" y="6" width="36" height="36" rx="6" stroke="currentColor" stroke-width="1.5" stroke-dasharray="5 3"/>
|
||||
<path d="M16 20h16M16 24h10M16 28h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="es-title">No log file loaded</div>
|
||||
<div class="es-sub">Drag & drop a log file anywhere on the page or click below to browse</div>
|
||||
<button class="es-btn" onclick="document.getElementById('file-input').click()">Select log file</button>
|
||||
</div>
|
||||
|
||||
<div id="table-area" style="display:none">
|
||||
<div id="dt-wrap">
|
||||
<table id="log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-time" data-col="time">Time</th>
|
||||
<th class="col-level" data-col="level">Level</th>
|
||||
<th class="col-msg" data-col="message">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="log-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status bar -->
|
||||
<div id="status-bar" style="display:none">
|
||||
<span class="status-count" id="status-showing">0 entries</span>
|
||||
<span class="sb-sep">·</span>
|
||||
<span id="status-total">0 total</span>
|
||||
<span class="sb-sep">·</span>
|
||||
<span id="status-file">no file</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ---- State ----
|
||||
let allLogs = [];
|
||||
let filteredLogs = [];
|
||||
let levelFilter = null;
|
||||
let textFilter = '';
|
||||
let timeFilter = null; // {start, end} timestamps
|
||||
let sortCol = 'time';
|
||||
let sortDir = 'asc';
|
||||
let expandedRows = new Set();
|
||||
let chart = null;
|
||||
let chartBuckets = [];
|
||||
|
||||
// ---- Demo data generator ----
|
||||
function generateDemoLogs() {
|
||||
const levels = ['DEBUG','DEBUG','DEBUG','INFO','INFO','INFO','INFO','WARN','WARN','ERROR','ERROR','FATAL'];
|
||||
const msgs = [
|
||||
{level:'INFO', msg:'Server started on port 3000', props:{pid:1234,env:'production'}},
|
||||
{level:'DEBUG', msg:'Database connection pool initialized', props:{pool_size:10,timeout:5000}},
|
||||
{level:'INFO', msg:'Request received GET /api/users', props:{method:'GET',path:'/api/users',ip:'10.0.0.5'}},
|
||||
{level:'DEBUG', msg:'Cache hit for key user:session:8f2a', props:{ttl:3540}},
|
||||
{level:'INFO', msg:'User authentication successful', props:{user_id:'u_9f3a',roles:['admin','viewer']}},
|
||||
{level:'WARN', msg:'Response time exceeded threshold 500ms', props:{actual_ms:621,endpoint:'/api/reports'}},
|
||||
{level:'ERROR', msg:'Failed to connect to Redis', props:{host:'redis:6379',attempt:3}, exception:'RedisConnectionError: Connection refused\n at connect (redis.js:42)\n at retry (pool.js:87)'},
|
||||
{level:'DEBUG', msg:'Worker thread spawned', props:{worker_id:'w_003',queue:'email'}},
|
||||
{level:'WARN', msg:'Memory usage above 80%', props:{used_mb:812,total_mb:1024,percent:79.3}},
|
||||
{level:'INFO', msg:'Scheduled job completed: nightly-backup', props:{duration_ms:12430,records:8912}},
|
||||
{level:'ERROR', msg:'Unhandled exception in payment processor', props:{order_id:'ord_7f8a',amount:149.99}, exception:'TypeError: Cannot read property \'charge\' of undefined\n at PaymentService.process (payment.js:133)\n at OrderController.checkout (orders.js:78)'},
|
||||
{level:'FATAL', msg:'Out of memory — process terminating', props:{heap_used_mb:2048,heap_limit_mb:2048}, exception:'FatalError: ENOMEM\n at allocate (heap.js:21)'},
|
||||
{level:'INFO', msg:'Health check passed', props:{db:'ok',cache:'ok',queue:'ok'}},
|
||||
{level:'DEBUG', msg:'Config reloaded from environment', props:{changed_keys:['LOG_LEVEL','DB_POOL']}},
|
||||
{level:'WARN', msg:'Deprecated API endpoint /v1/search called', props:{new_endpoint:'/v2/search',caller_ip:'192.168.1.55'}},
|
||||
];
|
||||
const now = Date.now();
|
||||
const logs = [];
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const template = msgs[Math.floor(Math.random() * msgs.length)];
|
||||
logs.push({
|
||||
time: new Date(now - (200 - i) * 18000 + Math.random() * 15000),
|
||||
level: template.level,
|
||||
message: template.msg,
|
||||
props: template.props || null,
|
||||
exception: template.exception || null,
|
||||
});
|
||||
}
|
||||
return logs.sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
// ---- Log parsing ----
|
||||
function parseLogs(text) {
|
||||
const lines = text.split('\n').filter(l => l.trim());
|
||||
const logs = [];
|
||||
|
||||
// Try JSON Lines
|
||||
const jsonLogs = tryParseJSONLines(lines);
|
||||
if (jsonLogs.length > lines.length * 0.5) return jsonLogs;
|
||||
|
||||
// Try common log formats
|
||||
const patterns = [
|
||||
// 2024-01-15T10:23:45.123Z INFO message
|
||||
/^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\s+(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|TRACE)\s+(.+)$/i,
|
||||
// [2024-01-15 10:23:45] [INFO] message
|
||||
/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]\s*\[(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|TRACE)\]\s*(.+)$/i,
|
||||
// Jan 15 10:23:45 INFO message
|
||||
/^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL)\s+(.+)$/i,
|
||||
// INFO 2024-01-15 10:23:45 message
|
||||
/^(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL)\s+(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(.+)$/i,
|
||||
];
|
||||
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
const line = lines[i].trim();
|
||||
let matched = false;
|
||||
for (const pat of patterns) {
|
||||
const m = line.match(pat);
|
||||
if (m) {
|
||||
const [, g1, g2, g3] = m;
|
||||
let timeStr, levelStr, msg;
|
||||
if (/^(DEBUG|INFO|WARN|ERROR|FATAL)/i.test(g1)) {
|
||||
timeStr = g2; levelStr = g1.toUpperCase().replace('WARNING','WARN'); msg = g3;
|
||||
} else {
|
||||
timeStr = g1; levelStr = g2.toUpperCase().replace('WARNING','WARN'); msg = g3;
|
||||
}
|
||||
const time = new Date(timeStr);
|
||||
if (!isNaN(time)) {
|
||||
// Collect continuation lines (stack traces etc.)
|
||||
let exception = '';
|
||||
let j = i + 1;
|
||||
while (j < lines.length && !/\d{4}-\d{2}-\d{2}/.test(lines[j]) && !patterns.some(p => p.test(lines[j]))) {
|
||||
const cl = lines[j].trim();
|
||||
if (cl) exception += (exception ? '\n' : '') + cl;
|
||||
j++;
|
||||
}
|
||||
i = j - 1;
|
||||
logs.push({ time, level: levelStr, message: msg.trim(), props: null, exception: exception || null });
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
// Plain line, treat as INFO
|
||||
if (line.length > 0) {
|
||||
logs.push({ time: new Date(), level: 'INFO', message: line, props: null, exception: null });
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return logs.sort((a, b) => a.time - b.time);
|
||||
}
|
||||
|
||||
function tryParseJSONLines(lines) {
|
||||
const logs = [];
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const obj = JSON.parse(line.trim());
|
||||
const timeKeys = ['timestamp','time','ts','date','@timestamp'];
|
||||
const levelKeys = ['level','severity','lvl','log_level'];
|
||||
const msgKeys = ['message','msg','text','body','log'];
|
||||
const timeVal = timeKeys.map(k => obj[k]).find(v => v);
|
||||
const levelRaw = levelKeys.map(k => obj[k]).find(v => v) || 'INFO';
|
||||
const msgVal = msgKeys.map(k => obj[k]).find(v => v) || JSON.stringify(obj);
|
||||
const level = String(levelRaw).toUpperCase().replace('WARNING','WARN');
|
||||
const time = timeVal ? new Date(timeVal) : new Date();
|
||||
const usedKeys = new Set([...timeKeys, ...levelKeys, ...msgKeys].filter(k => obj[k] !== undefined));
|
||||
const props = Object.fromEntries(Object.entries(obj).filter(([k]) => !usedKeys.has(k)));
|
||||
const exception = obj.exception || obj.error || obj.stack || null;
|
||||
logs.push({ time, level: ['DEBUG','INFO','WARN','ERROR','FATAL'].includes(level) ? level : 'INFO', message: String(msgVal), props: Object.keys(props).length ? props : null, exception: exception ? String(exception) : null });
|
||||
} catch {}
|
||||
}
|
||||
return logs;
|
||||
}
|
||||
|
||||
// ---- Chart ----
|
||||
function buildChart(logs) {
|
||||
if (!logs.length) return;
|
||||
const canvas = document.getElementById('intensity-chart');
|
||||
const BUCKETS = 60;
|
||||
const times = logs.map(l => l.time.getTime());
|
||||
const minT = Math.min(...times), maxT = Math.max(...times);
|
||||
const span = maxT - minT || 1;
|
||||
const bucketSize = span / BUCKETS;
|
||||
|
||||
const buckets = Array.from({length: BUCKETS}, (_, i) => ({
|
||||
t: new Date(minT + i * bucketSize),
|
||||
DEBUG: 0, INFO: 0, WARN: 0, ERROR: 0, FATAL: 0, total: 0
|
||||
}));
|
||||
|
||||
for (const log of logs) {
|
||||
const idx = Math.min(Math.floor((log.time.getTime() - minT) / bucketSize), BUCKETS - 1);
|
||||
buckets[idx][log.level] = (buckets[idx][log.level] || 0) + 1;
|
||||
buckets[idx].total++;
|
||||
}
|
||||
chartBuckets = buckets;
|
||||
|
||||
if (chart) chart.destroy();
|
||||
|
||||
const colorMap = {
|
||||
DEBUG: '#55596640', INFO: '#4f8ef760', WARN: '#f5a62370', ERROR: '#e8504a80', FATAL: '#c72b2b90'
|
||||
};
|
||||
|
||||
chart = new Chart(canvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: buckets.map(b => b.t),
|
||||
datasets: ['FATAL','ERROR','WARN','INFO','DEBUG'].map(level => ({
|
||||
label: level,
|
||||
data: buckets.map(b => b[level]),
|
||||
backgroundColor: colorMap[level],
|
||||
borderColor: colorMap[level].replace(/[0-9a-f]{2}$/, 'ff'),
|
||||
borderWidth: 0,
|
||||
borderRadius: 1,
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: {display: false},
|
||||
tooltip: {
|
||||
backgroundColor: '#131619',
|
||||
borderColor: '#ffffff18',
|
||||
borderWidth: 1,
|
||||
titleColor: '#8b8f9a',
|
||||
bodyColor: '#e8eaf0',
|
||||
titleFont: {family: 'JetBrains Mono', size: 10},
|
||||
bodyFont: {family: 'JetBrains Mono', size: 11},
|
||||
callbacks: {
|
||||
title: items => {
|
||||
const d = new Date(items[0].label);
|
||||
return d.toLocaleString();
|
||||
}
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
zoom: {
|
||||
drag: {enabled: true, backgroundColor: '#4f8ef718', borderColor: '#4f8ef760', borderWidth: 1},
|
||||
mode: 'x',
|
||||
onZoomComplete({chart}) {
|
||||
const {min, max} = chart.scales.x;
|
||||
const minT = chart.data.labels[Math.round(min)]?.getTime();
|
||||
const maxT = chart.data.labels[Math.min(Math.round(max), chart.data.labels.length-1)]?.getTime();
|
||||
if (minT && maxT) applyTimeFilter(minT, maxT);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'category',
|
||||
stacked: true,
|
||||
ticks: {display: false},
|
||||
grid: {color: '#ffffff08', drawBorder: false},
|
||||
border: {color: '#ffffff10'}
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
ticks: {color: '#555966', font: {family: 'JetBrains Mono', size: 10}, maxTicksLimit: 4},
|
||||
grid: {color: '#ffffff08', drawBorder: false},
|
||||
border: {display: false}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('contextmenu', e => {
|
||||
e.preventDefault();
|
||||
clearTimeFilter();
|
||||
});
|
||||
}
|
||||
|
||||
function updateChartHighlight() {
|
||||
if (!chart || !timeFilter) return;
|
||||
// highlight handled by zoom plugin selection
|
||||
}
|
||||
|
||||
// ---- Filtering ----
|
||||
function applyTimeFilter(start, end) {
|
||||
timeFilter = {start, end};
|
||||
const startD = new Date(start), endD = new Date(end);
|
||||
document.getElementById('time-badge-label').textContent =
|
||||
`${fmtShort(startD)} – ${fmtShort(endD)}`;
|
||||
document.getElementById('time-badge').classList.add('visible');
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function clearTimeFilter() {
|
||||
timeFilter = null;
|
||||
document.getElementById('time-badge').classList.remove('visible');
|
||||
if (chart) chart.resetZoom();
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
let regex = null;
|
||||
const searchVal = document.getElementById('search').value;
|
||||
const searchEl = document.getElementById('search');
|
||||
if (searchVal) {
|
||||
try {
|
||||
regex = new RegExp(searchVal, 'i');
|
||||
searchEl.classList.remove('invalid');
|
||||
} catch {
|
||||
searchEl.classList.add('invalid');
|
||||
regex = null;
|
||||
}
|
||||
} else {
|
||||
searchEl.classList.remove('invalid');
|
||||
}
|
||||
|
||||
filteredLogs = allLogs.filter(log => {
|
||||
if (levelFilter && log.level !== levelFilter) return false;
|
||||
if (timeFilter && (log.time.getTime() < timeFilter.start || log.time.getTime() > timeFilter.end)) return false;
|
||||
if (regex && !regex.test(log.message)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
renderTable();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
// ---- Table rendering ----
|
||||
function renderTable() {
|
||||
const sorted = [...filteredLogs].sort((a, b) => {
|
||||
let av = a[sortCol], bv = b[sortCol];
|
||||
if (sortCol === 'time') { av = av.getTime(); bv = bv.getTime(); }
|
||||
else { av = String(av).toLowerCase(); bv = String(bv).toLowerCase(); }
|
||||
return sortDir === 'asc' ? (av > bv ? 1 : -1) : (av < bv ? 1 : -1);
|
||||
});
|
||||
|
||||
expandedRows.clear();
|
||||
const tbody = document.getElementById('log-tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
sorted.forEach((log, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'fade-in';
|
||||
tr.dataset.idx = idx;
|
||||
tr.innerHTML = `
|
||||
<td class="col-time">${fmtTime(log.time)}</td>
|
||||
<td class="col-level"><span class="lv lv-${log.level}">${log.level}</span></td>
|
||||
<td class="col-msg">${esc(log.message)}${log.exception || log.props ? ' <span style="color:var(--text3);font-size:10px">▸ details</span>' : ''}</td>
|
||||
`;
|
||||
tr.addEventListener('click', () => toggleDetail(tr, log, idx));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Sort indicators
|
||||
document.querySelectorAll('thead th').forEach(th => {
|
||||
th.classList.remove('sort-asc','sort-desc');
|
||||
if (th.dataset.col === sortCol) th.classList.add(sortDir === 'asc' ? 'sort-asc' : 'sort-desc');
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDetail(tr, log, idx) {
|
||||
if (expandedRows.has(idx)) {
|
||||
expandedRows.delete(idx);
|
||||
const detailRow = tr.nextSibling;
|
||||
if (detailRow && detailRow.classList.contains('row-detail')) detailRow.remove();
|
||||
return;
|
||||
}
|
||||
expandedRows.add(idx);
|
||||
const detailTr = document.createElement('tr');
|
||||
detailTr.className = 'row-detail';
|
||||
let sections = '';
|
||||
if (log.exception) {
|
||||
sections += `<div class="detail-section"><div class="detail-label">Exception / Stack Trace</div><div class="detail-val" style="color:var(--error)">${esc(log.exception)}</div></div>`;
|
||||
}
|
||||
if (log.props) {
|
||||
sections += `<div class="detail-section"><div class="detail-label">Properties</div><div class="detail-val">${esc(JSON.stringify(log.props, null, 2))}</div></div>`;
|
||||
}
|
||||
if (!sections) return;
|
||||
detailTr.innerHTML = `<td colspan="3"><div class="detail-inner">${sections}</div></td>`;
|
||||
tr.after(detailTr);
|
||||
}
|
||||
|
||||
// ---- Level counts ----
|
||||
function updateLevelCounts() {
|
||||
const counts = {DEBUG:0,INFO:0,WARN:0,ERROR:0,FATAL:0};
|
||||
allLogs.forEach(l => { if (counts[l.level] !== undefined) counts[l.level]++; });
|
||||
Object.entries(counts).forEach(([lv, cnt]) => {
|
||||
document.getElementById('cnt-' + lv).textContent = cnt.toLocaleString();
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Status bar ----
|
||||
function updateStatus() {
|
||||
const s = document.getElementById('status-showing');
|
||||
const t = document.getElementById('status-total');
|
||||
s.textContent = `${filteredLogs.length.toLocaleString()} entries`;
|
||||
t.textContent = `${allLogs.length.toLocaleString()} total`;
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
function fmtTime(d) {
|
||||
return d.toISOString().replace('T',' ').replace(/\.\d+Z$/,'');
|
||||
}
|
||||
function fmtShort(d) {
|
||||
return d.toLocaleTimeString('en', {hour:'2-digit', minute:'2-digit', second:'2-digit'});
|
||||
}
|
||||
function esc(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
// ---- Load file ----
|
||||
function loadFile(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const text = e.target.result;
|
||||
allLogs = parseLogs(text);
|
||||
if (!allLogs.length) { allLogs = generateDemoLogs(); }
|
||||
initView(file.name);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function loadDemo() {
|
||||
allLogs = generateDemoLogs();
|
||||
initView('demo.log');
|
||||
}
|
||||
|
||||
function initView(filename) {
|
||||
levelFilter = null;
|
||||
textFilter = '';
|
||||
timeFilter = null;
|
||||
expandedRows.clear();
|
||||
document.getElementById('search').value = '';
|
||||
document.getElementById('time-badge').classList.remove('visible');
|
||||
document.querySelectorAll('.level-pill').forEach(p => p.classList.remove('active'));
|
||||
|
||||
document.getElementById('file-info').textContent = filename + ' — ' + allLogs.length.toLocaleString() + ' entries';
|
||||
document.getElementById('status-file').textContent = filename;
|
||||
document.getElementById('empty-state').style.display = 'none';
|
||||
document.getElementById('table-area').style.display = 'flex';
|
||||
document.getElementById('status-bar').style.display = 'flex';
|
||||
|
||||
updateLevelCounts();
|
||||
filteredLogs = [...allLogs];
|
||||
buildChart(allLogs);
|
||||
renderTable();
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
// ---- Events ----
|
||||
document.getElementById('file-input').addEventListener('change', e => {
|
||||
if (e.target.files[0]) loadFile(e.target.files[0]);
|
||||
});
|
||||
|
||||
document.getElementById('search').addEventListener('input', () => applyFilters());
|
||||
|
||||
document.querySelectorAll('.level-pill').forEach(pill => {
|
||||
pill.addEventListener('click', () => {
|
||||
const lv = pill.dataset.level;
|
||||
if (levelFilter === lv) {
|
||||
levelFilter = null;
|
||||
pill.classList.remove('active');
|
||||
} else {
|
||||
levelFilter = lv;
|
||||
document.querySelectorAll('.level-pill').forEach(p => p.classList.remove('active'));
|
||||
pill.classList.add('active');
|
||||
}
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('time-badge').addEventListener('click', () => clearTimeFilter());
|
||||
document.getElementById('clear-filters').addEventListener('click', () => {
|
||||
levelFilter = null;
|
||||
document.getElementById('search').value = '';
|
||||
document.querySelectorAll('.level-pill').forEach(p => p.classList.remove('active'));
|
||||
clearTimeFilter();
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// Sort headers
|
||||
document.querySelectorAll('thead th[data-col]').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
const col = th.dataset.col;
|
||||
if (sortCol === col) sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
||||
else { sortCol = col; sortDir = 'asc'; }
|
||||
renderTable();
|
||||
});
|
||||
});
|
||||
|
||||
// Drag & drop
|
||||
const dropOverlay = document.getElementById('drop-overlay');
|
||||
let dragCount = 0;
|
||||
|
||||
document.addEventListener('dragenter', e => {
|
||||
e.preventDefault();
|
||||
dragCount++;
|
||||
if (dragCount === 1) dropOverlay.classList.add('active');
|
||||
});
|
||||
document.addEventListener('dragleave', e => {
|
||||
dragCount--;
|
||||
if (dragCount === 0) dropOverlay.classList.remove('active');
|
||||
});
|
||||
document.addEventListener('dragover', e => e.preventDefault());
|
||||
document.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
dragCount = 0;
|
||||
dropOverlay.classList.remove('active');
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) loadFile(file);
|
||||
});
|
||||
|
||||
// Load demo on start
|
||||
//loadDemo();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,function(){"use strict";var s=Object.freeze({__proto__:null,get Colors(){return dn},get Decimation(){return fn},get Filler(){return Dn},get Legend(){return Tn},get SubTitle(){return In},get Title(){return En},get Tooltip(){return Un}});function t(){}const F=(()=>{let t=0;return()=>t++})();function P(t){return null==t}function O(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function A(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function p(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function g(t,e){return p(t)?t:e}function T(t,e){return void 0===t?e:t}const V=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,B=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function k(t,e,i,s){let a,n,o;if(O(t))if(n=t.length,s)for(a=n-1;0<=a;a--)e.call(i,t[a],a);else for(a=0;a<n;a++)e.call(i,t[a],a);else if(A(t))for(o=Object.keys(t),n=o.length,a=0;a<n;a++)e.call(i,t[o[a]],o[a])}function W(t,e){let i,s,a,n;if(!t||!e||t.length!==e.length)return!1;for(i=0,s=t.length;i<s;++i)if(a=t[i],n=e[i],a.datasetIndex!==n.datasetIndex||a.index!==n.index)return!1;return!0}function N(e){if(O(e))return e.map(N);if(A(e)){const i=Object.create(null),s=Object.keys(e),a=s.length;let t=0;for(;t<a;++t)i[s[t]]=N(e[s[t]]);return i}return e}function H(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}function j(t,e,i,s){var a;H(t)&&(a=e[t],i=i[t],A(a)&&A(i)?Y(a,i,s):e[t]=N(i))}function Y(i,t,s){var a,e=O(t)?t:[t],n=e.length;if(!A(i))return i;const o=(s=s||{}).merger||j;for(let t=0;t<n;++t)if(A(a=e[t])){const O=Object.keys(a);for(let t=0,e=O.length;t<e;++t)o(O[t],i,a,s)}return i}function $(t,e){return Y(t,e,{merger:U})}function U(t,e,i){var s;H(t)&&(s=e[t],i=i[t],A(s)&&A(i)?$(s,i):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=N(i)))}const X={"":t=>t,x:t=>t.x,y:t=>t.y};function q(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s=s.endsWith("\\")?s.slice(0,-1)+".":(i.push(s),"");return i}function m(t,e){const i=X[e]||(X[e]=function(){const i=q(e);return t=>{for(const e of i){if(""===e)break;t=t&&t[e]}return t}}());return i(t)}function K(t){return t.charAt(0).toUpperCase()+t.slice(1)}const G=t=>void 0!==t,u=t=>"function"==typeof t,Z=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function J(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const S=Math.PI,_=2*S,Q=_+S,tt=Number.POSITIVE_INFINITY,et=S/180,D=S/2,it=S/4,st=2*S/3,r=Math.log10,y=Math.sign;function at(t,e,i){return Math.abs(t-e)<i}function nt(t){var e=Math.round(t),e=(t=at(t,e,t/1e3)?e:t,Math.pow(10,Math.floor(r(t)))),t=t/e;return(t<=1?1:t<=2?2:t<=5?5:10)*e}function ot(t){const e=[],i=Math.sqrt(t);let s;for(s=1;s<i;s++)t%s==0&&(e.push(s),e.push(t/s));return i===(0|i)&&e.push(i),e.sort((t,e)=>t-e).pop(),e}function rt(t){return!isNaN(parseFloat(t))&&isFinite(t)}function lt(t,e){var i=Math.round(t);return i-e<=t&&t<=i+e}function ht(t,e,i){let s,a,n;for(s=0,a=t.length;s<a;s++)n=t[s][i],isNaN(n)||(e.min=Math.min(e.min,n),e.max=Math.max(e.max,n))}function L(t){return t*(S/180)}function ct(t){return t*(180/S)}function dt(i){if(p(i)){let t=1,e=0;for(;Math.round(i*t)/t!==i;)t*=10,e++;return e}}function ut(t,e){var i=e.x-t.x,e=e.y-t.y,t=Math.sqrt(i*i+e*e);let s=Math.atan2(e,i);return s<-.5*S&&(s+=_),{angle:s,distance:t}}function gt(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function ft(t,e){return(t-e+Q)%_-S}function v(t){return(t%_+_)%_}function pt(t,e,i,s){var t=v(t),e=v(e),i=v(i),a=v(e-t),n=v(i-t),o=v(t-e),r=v(t-i);return t===e||t===i||s&&e===i||n<a&&o<r}function C(t,e,i){return Math.max(e,Math.min(i,t))}function mt(t){return C(t,-32768,32767)}function c(t,e,i,s=1e-6){return t>=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function bt(e,i,t){t=t||(t=>e[t]<i);let s,a=e.length-1,n=0;for(;1<a-n;)t(s=n+a>>1)?n=s:a=s;return{lo:n,hi:a}}const f=(i,s,a,t)=>bt(i,a,t?t=>{var e=i[t][s];return e<a||e===a&&i[t+1][s]===a}:t=>i[t][s]<a),xt=(e,i,s)=>bt(e,s,t=>e[t][i]>=s);function vt(t,e,i){let s=0,a=t.length;for(;s<a&&t[s]<e;)s++;for(;a>s&&t[a-1]>i;)a--;return 0<s||a<t.length?t.slice(s,a):t}const _t=["push","pop","shift","splice","unshift"];function yt(a,t){a._chartjs?a._chartjs.listeners.push(t):(Object.defineProperty(a,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[t]}}),_t.forEach(t=>{const i="_onData"+K(t),s=a[t];Object.defineProperty(a,t,{configurable:!0,enumerable:!1,value(...e){var t=s.apply(this,e);return a._chartjs.listeners.forEach(t=>{"function"==typeof t[i]&&t[i](...e)}),t}})}))}function Mt(e,t){var i=e._chartjs;if(i){const s=i.listeners,a=s.indexOf(Line truncated
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* chartjs-plugin-zoom v2.0.1
|
||||
* undefined
|
||||
* (c) 2016-2023 chartjs-plugin-zoom Contributors
|
||||
* Released under the MIT License
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("chart.js"),require("hammerjs"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","hammerjs","chart.js/helpers"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).ChartZoom=t(e.Chart,e.Hammer,e.Chart.helpers)}(this,(function(e,t,n){"use strict";function o(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var a=o(t);const i=e=>e&&e.enabled&&e.modifierKey,c=(e,t)=>e&&t[e+"Key"],r=(e,t)=>e&&!t[e+"Key"];function s(e,t,n){return void 0===e||("string"==typeof e?-1!==e.indexOf(t):"function"==typeof e&&-1!==e({chart:n}).indexOf(t))}function l(e,t){return"function"==typeof e&&(e=e({chart:t})),"string"==typeof e?{x:-1!==e.indexOf("x"),y:-1!==e.indexOf("y")}:{x:!1,y:!1}}function u(e,t,o){const{mode:a="xy",scaleMode:i,overScaleMode:c}=e||{},r=function({x:e,y:t},n){const o=n.scales,a=Object.keys(o);for(let n=0;n<a.length;n++){const i=o[a[n]];if(t>=i.top&&t<=i.bottom&&e>=i.left&&e<=i.right)return i}return null}(t,o),s=l(a,o),u=l(i,o);if(c){const e=l(c,o);for(const t of["x","y"])e[t]&&(u[t]=s[t],s[t]=!1)}if(r&&u[r.axis])return[r];const m=[];return n.each(o.scales,(function(e){s[e.axis]&&m.push(e)})),m}const m=new WeakMap;function d(e){let t=m.get(e);return t||(t={originalScaleLimits:{},updatedScaleLimits:{},handlers:{},panDelta:{}},m.set(e,t)),t}function f(e,t,n){const o=e.max-e.min,a=o*(t-1),i=e.isHorizontal()?n.x:n.y,c=Math.max(0,Math.min(1,(e.getValueForPixel(i)-e.min)/o||0));return{min:a*c,max:a*(1-c)}}function p(e,t,o,a,i){let c=o[a];if("original"===c){const o=e.originalScaleLimits[t.id][a];c=n.valueOrDefault(o.options,o.scale)}return n.valueOrDefault(c,i)}function h(e,{min:t,max:n},o,a=!1){const i=d(e.chart),{id:c,axis:r,options:s}=e,l=o&&(o[c]||o[r])||{},{minRange:u=0}=l,m=p(i,e,l,"min",-1/0),f=p(i,e,l,"max",1/0),h=a?Math.max(n-t,u):e.max-e.min,x=(h-n+t)/2;return n+=x,(t-=x)<m?(t=m,n=Math.min(m+h,f)):n>f&&(n=f,t=Math.max(f-h,m)),s.min=t,s.max=n,i.updatedScaleLimits[e.id]={min:t,max:n},e.parse(t)!==e.min||e.parse(n)!==e.max}const x=e=>0===e||isNaN(e)?0:e<0?Math.min(Math.round(e),-1):Math.max(Math.round(e),1);const g={second:500,minute:3e4,hour:18e5,day:432e5,week:3024e5,month:1296e6,quarter:5184e6,year:157248e5};function y(e,t,n,o=!1){const{min:a,max:i,options:c}=e,r=c.time&&c.time.round,s=g[r]||0,l=e.getValueForPixel(e.getPixelForValue(a+s)-t),u=e.getValueForPixel(e.getPixelForValue(i+s)-t),{min:m=-1/0,max:d=1/0}=o&&n&&n[e.axis]||{};return!!(isNaN(l)||isNaN(u)||l<m||u>d)||h(e,{min:l,max:u},n,o)}function b(e,t,n){return y(e,t,n,!0)}const v={category:function(e,t,n,o){const a=f(e,t,n);return e.min===e.max&&t<1&&function(e){const t=e.getLabels().length-1;e.min>0&&(e.min-=1),e.max<t&&(e.max+=1)}(e),h(e,{min:e.min+x(a.min),max:e.max-x(a.max)},o,!0)},default:function(e,t,n,o){const a=f(e,t,n);return h(e,{min:e.min+a.min,max:e.max-a.max},o,!0)}},w={default:function(e,t,n,o){h(e,function(e,t,n){const o=e.getValueForPixel(t),a=e.getValueForPixel(n);return{min:Math.min(o,a),max:Math.max(o,a)}}(e,t,n),o,!0)}},z={category:function(e,t,n){const o=e.getLabels().length-1;let{min:a,max:i}=e;const c=Math.max(i-a,1),r=Math.round(function(e){return e.isHorizontal()?e.width:e.height}(e)/Math.max(c,10)),s=Math.round(Math.abs(t/r));let l;return t<-r?(i=Math.min(i+s,o),a=1===c?i:i-c,l=i===o):t>r&&(a=Math.max(0,a-s),i=1===c?a:a+c,l=0===a),h(e,{min:a,max:i},n)||l},default:y,logarithmic:b,timeseries:b};function M(e,t){n.each(e,((n,o)=>{t[o]||delete e[o]}))}function k(e,t){const{scales:o}=e,{originalScaleLimits:a,updatedScaleLimits:i}=t;return n.each(o,(function(e){(function(e,t,n){const{id:o,options:{min:a,max:i}}=e;if(!t[o]||!n[o])return!0;const c=n[o];return c.min!==a||c.max!==i})(e,a,i)&&(a[e.id]={min:{scale:e.min,options:e.options.min},max:{scale:e.max,options:e.options.max}})})),M(a,o),M(i,o),a}function S(e,t,o,a){const i=v[e.type]||v.default;n.callback(i,[e,t,o,a])}function P(e,t,o,a,i){const c=w[e.type]||w.default;n.callback(c,[e,t,o,a,i])}function D(e){const t=e.chartArea;return{x:(t.left+t.right)/2,y:(t.top+t.bottom)/2}}function j(e,t,o="none"){const{x:a=1,y:i=1,focalPoint:c=D(e)}="number"==typeof t?{x:t,y:t}:t,r=d(e),{options:{limits:s,zoom:l}}=r;k(e,r);const m=1!==a,f=1!==i,p=u(l,c,e);n.each(p||e.scales,(function(e){e.isHorizontal()&&m?S(e,a,c,s):!e.isHorizontal()&&f&&S(e,i,c,s)})),e.update(o),n.callback(l.onZoom,[{chart:e}])}function O(e,t,o,a="none"){const i=d(e),{options:{limits:c,zoom:r}}=i,{mode:l="xy"}=r;k(e,i);const u=s(l,"x",e),m=s(l,"y",e);n.each(e.scales,(function(e){e.isHorizontal()&&u?P(e,t.x,o.x,c):!e.isHorizontal()&&m&&P(e,t.y,o.y,c)})),e.update(a),n.callback(r.onZoom,[{chart:e}])}function C(e){const t=d(e);let o=1,a=1;return n.each(e.scales,(function(e){const i=function(e,t){const o=e.originalScaleLimits[t];if(!o)return;const{min:a,max:i}=o;return n.valueOrDefault(i.options,i.scale)-n.valueOrDefault(a.options,a.scale)}(t,e.id);if(i){const t=Math.round(i/(e.max-e.min)*10Line truncated
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/*! Hammer.JS - v2.0.7 - 2016-04-22
|
||||
* http://hammerjs.github.io/
|
||||
*
|
||||
* Copyright (c) 2016 Jorik Tangelder;
|
||||
* Licensed under the MIT license */
|
||||
!function(a,b,c,d){"use strict";function e(a,b,c){return setTimeout(j(a,c),b)}function f(a,b,c){return Array.isArray(a)?(g(a,c[b],c),!0):!1}function g(a,b,c){var e;if(a)if(a.forEach)a.forEach(b,c);else if(a.length!==d)for(e=0;e<a.length;)b.call(c,a[e],e,a),e++;else for(e in a)a.hasOwnProperty(e)&&b.call(c,a[e],e,a)}function h(b,c,d){var e="DEPRECATED METHOD: "+c+"\n"+d+" AT \n";return function(){var c=new Error("get-stack-trace"),d=c&&c.stack?c.stack.replace(/^[^\(]+?[\n$]/gm,"").replace(/^\s+at\s+/gm,"").replace(/^Object.<anonymous>\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",f=a.console&&(a.console.warn||a.console.log);return f&&f.call(a.console,e,d),b.apply(this,arguments)}}function i(a,b,c){var d,e=b.prototype;d=a.prototype=Object.create(e),d.constructor=a,d._super=e,c&&la(d,c)}function j(a,b){return function(){return a.apply(b,arguments)}}function k(a,b){return typeof a==oa?a.apply(b?b[0]||d:d,b):a}function l(a,b){return a===d?b:a}function m(a,b,c){g(q(b),function(b){a.addEventListener(b,c,!1)})}function n(a,b,c){g(q(b),function(b){a.removeEventListener(b,c,!1)})}function o(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1}function p(a,b){return a.indexOf(b)>-1}function q(a){return a.trim().split(/\s+/g)}function r(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;d<a.length;){if(c&&a[d][c]==b||!c&&a[d]===b)return d;d++}return-1}function s(a){return Array.prototype.slice.call(a,0)}function t(a,b,c){for(var d=[],e=[],f=0;f<a.length;){var g=b?a[f][b]:a[f];r(e,g)<0&&d.push(a[f]),e[f]=g,f++}return c&&(d=b?d.sort(function(a,c){return a[b]>c[b]}):d.sort()),d}function u(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g<ma.length;){if(c=ma[g],e=c?c+f:b,e in a)return e;g++}return d}function v(){return ua++}function w(b){var c=b.ownerDocument||b;return c.defaultView||c.parentWindow||a}function x(a,b){var c=this;this.manager=a,this.callback=b,this.element=a.element,this.target=a.options.inputTarget,this.domHandler=function(b){k(a.options.enable,[a])&&c.handler(b)},this.init()}function y(a){var b,c=a.options.inputClass;return new(b=c?c:xa?M:ya?P:wa?R:L)(a,z)}function z(a,b,c){var d=c.pointers.length,e=c.changedPointers.length,f=b&Ea&&d-e===0,g=b&(Ga|Ha)&&d-e===0;c.isFirst=!!f,c.isFinal=!!g,f&&(a.session={}),c.eventType=b,A(a,c),a.emit("hammer.input",c),a.recognize(c),a.session.prevInput=c}function A(a,b){var c=a.session,d=b.pointers,e=d.length;c.firstInput||(c.firstInput=D(b)),e>1&&!c.firstMultiple?c.firstMultiple=D(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=E(d);b.timeStamp=ra(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=I(h,i),b.distance=H(h,i),B(c,b),b.offsetDirection=G(b.deltaX,b.deltaY);var j=F(b.deltaTime,b.deltaX,b.deltaY);b.overallVelocityX=j.x,b.overallVelocityY=j.y,b.overallVelocity=qa(j.x)>qa(j.y)?j.x:j.y,b.scale=g?K(g.pointers,d):1,b.rotation=g?J(g.pointers,d):0,b.maxPointers=c.prevInput?b.pointers.length>c.prevInput.maxPointers?b.pointers.length:c.prevInput.maxPointers:b.pointers.length,C(c,b);var k=a.element;o(b.srcEvent.target,k)&&(k=b.srcEvent.target),b.target=k}function B(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};b.eventType!==Ea&&f.eventType!==Ga||(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function C(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Ha&&(i>Da||h.velocity===d)){var j=b.deltaX-h.deltaX,k=b.deltaY-h.deltaY,l=F(i,j,k);e=l.x,f=l.y,c=qa(l.x)>qa(l.y)?l.x:l.y,g=G(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function D(a){for(var b=[],c=0;c<a.pointers.length;)b[c]={clientX:pa(a.pointers[c].clientX),clientY:pa(a.pointers[c].clientY)},c++;return{timeStamp:ra(),pointers:b,center:E(b),deltaX:a.deltaX,deltaY:a.deltaY}}function E(a){var b=a.length;if(1===b)return{x:pa(a[0].clientX),y:pa(a[0].clientY)};for(var c=0,d=0,e=0;b>e;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:pa(c/b),y:pa(d/b)}}function F(a,b,c){return{x:b/a||0,y:c/a||0}}function G(a,b){return a===b?Ia:qa(a)>=qa(b)?0>a?Ja:Ka:0>b?La:Ma}function H(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function I(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function J(a,b){return I(b[1],b[0],Ra)+I(a[1],a[0],Ra)}function K(a,b){return H(b[0],b[1],Ra)/H(a[0],a[1],Ra)}function L(){this.evEl=Ta,this.evWin=Ua,this.pressed=!1,x.apply(this,arguments)}function M(){this.evEl=Xa,this.evWin=Ya,x.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function N(){this.evTarget=$a,this.evWin=_a,this.started=!1,x.apply(this,arguments)}function O(a,b){var c=s(a.touches),d=s(a.changedTouches);return b&(Ga|Ha)&&(c=t(c.concat(d),"identifier",!0)),[c,d]}function P(){this.evTarget=bb,this.targetIds={},x.apply(this,arguments)}function Q(a,b){var c=s(a.touches)Line truncated
|
||||
//# sourceMappingURL=hammer.min.js.map
|
||||
Reference in new issue
Block a user