This commit is contained in:
hardliner66 2024-10-03 20:41:43 +02:00 committed by Steve Biedermann
commit 9f74bc65ed
11 changed files with 6468 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/dist/
/static/
/.dioxus/
# this file will generate by tailwind:
/assets/tailwind.css
# These are backup files generated by rustfmt
**/*.rs.bk

45
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'fast-log-viewer'",
"cargo": {
"args": [
"build",
"--bin=fast-log-viewer",
"--package=fast-log-viewer"
],
"filter": {
"name": "fast-log-viewer",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'fast-log-viewer'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=fast-log-viewer",
"--package=fast-log-viewer"
],
"filter": {
"name": "fast-log-viewer",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

5223
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "fast-log-viewer"
version = "0.1.0"
authors = ["hardliner66 <hardliner66@gmail.com>"]
edition = "2021"
[dependencies]
clap = { version = "4.5.19", features = ["derive"] }
dioxus = { version = "0.5", features = ["desktop", "router"] }
dioxus-logger = "0.5.1"
manganis = "0.2.2"
regex = "1.11.0"
[features]
bundle = []

47
Dioxus.toml Normal file
View File

@ -0,0 +1,47 @@
[application]
# App (Project) Name
name = "fast-log-viewer"
# Dioxus App Default Platform
# web, desktop, fullstack
default_platform = "desktop"
# `build` & `serve` dist path
out_dir = "dist"
# assets file folder
asset_dir = "assets"
[web.app]
# HTML title tag content
title = "fast-log-viewer"
[web.watcher]
# when watcher trigger, regenerate the `index.html`
reload_html = true
# which files or dirs will be watcher monitoring
watch_path = ["src", "assets"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = ["/assets/tailwind.css"]
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []
[bundle]
# The list of files to include in the bundle. These can contain globs.
resources = ["assets/**/*"]

15
README.md Normal file
View File

@ -0,0 +1,15 @@
# Development
1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
2. Install the tailwind css cli: https://tailwindcss.com/docs/installation
3. Run the following command in the root of the project to start the tailwind CSS compiler:
```bash
npx tailwindcss -i ./input.css -o ./assets/tailwind.css --watch
```
Run the following command in the root of the project to start the Dioxus dev server:
```bash
dx serve --hot-reload --platform desktop
```

BIN
icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

48
input.css Normal file
View File

@ -0,0 +1,48 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
th {
border-color: black;
@apply border px-4 py-2;
}
td {
@apply border px-4 py-2;
@apply text-ellipsis overflow-hidden;
}
td div{
@apply text-ellipsis overflow-hidden;
}
/* Ensure the body and html take up the full height */
html, body {
height: 100vh;
margin: 0;
}
/* Parent container as a flex container */
.p-4 {
display: flex;
flex-direction: column;
overflow: hidden; /* Prevents scrollbar on the body */
}
/* Header stays at the top */
.header {
position: fixed;
/* Set height as needed */
max-height: 80px; /* Adjust to your header's height */
background-color: white; /* Optional */
}
/* Content area fills the remaining space and is scrollable */
.content {
position: fixed;
max-height: calc(100vh - 90px); /* Adjust to your header's height */
max-width: calc(100vw - 20px);
overflow-y: scroll;
overflow-x: scroll;
top: 80px;
}

675
public/tailwind.css Normal file
View File

@ -0,0 +1,675 @@
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
/*
! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
.visible {
visibility: visible;
}
.static {
position: static;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.right-0 {
right: 0px;
}
.top-0 {
top: 0px;
}
.mb-4 {
margin-bottom: 1rem;
}
.mr-4 {
margin-right: 1rem;
}
.table {
display: table;
}
.h-full {
height: 100%;
}
.w-1 {
width: 0.25rem;
}
.w-full {
width: 100%;
}
.table-auto {
table-layout: auto;
}
.cursor-col-resize {
cursor: col-resize;
}
.resize-x {
resize: horizontal;
}
.border {
border-width: 1px;
}
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.p-2 {
padding: 0.5rem;
}
.p-4 {
padding: 1rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.text-left {
text-align: left;
}
.text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.text-gray-500 {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-yellow-500 {
--tw-text-opacity: 1;
color: rgb(234 179 8 / var(--tw-text-opacity));
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.hover\:bg-gray-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}

379
src/main.rs Normal file
View File

@ -0,0 +1,379 @@
#![allow(non_snake_case)]
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
use std::{fs::File, io::Read};
use clap::Parser;
use dioxus::{desktop::WindowBuilder, prelude::*};
use dioxus_logger::tracing::{info, Level};
use regex::RegexBuilder;
#[derive(Clone, Parser)]
struct Args {
log_file: Option<String>,
}
#[derive(Clone, Debug)]
struct LogEntry {
timestamp: String,
level: LogLevel,
category: String,
thread: String,
source: String,
message: String,
}
impl LogEntry {
fn level_class(&self) -> &str {
match self.level {
LogLevel::Info => "text-blue-500",
LogLevel::Warning => "text-yellow-500",
LogLevel::Error => "text-red-500",
_ => "text-gray-500",
}
}
fn matches_query(&self, query: &str) -> bool {
let query = query.trim().to_lowercase();
if query.is_empty() {
return true;
}
self.timestamp.to_lowercase().contains(&query)
|| self.level.as_str().to_lowercase().contains(&query)
|| self.category.to_lowercase().contains(&query)
|| self.thread.to_lowercase().contains(&query)
|| self.source.to_lowercase().contains(&query)
|| self.message.to_lowercase().contains(&query)
}
}
fn highlight_matches<'a>(text: &'a str, query: &str) -> Vec<(&'a str, bool)> {
if query.is_empty() {
return vec![(text, false)];
}
let mut result = Vec::new();
let mut last_end = 0;
let re = RegexBuilder::new(&regex::escape(query))
.case_insensitive(true)
.build()
.unwrap();
for mat in re.find_iter(text) {
if mat.start() > last_end {
result.push((&text[last_end..mat.start()], false));
}
result.push((&text[mat.start()..mat.end()], true));
last_end = mat.end();
}
if last_end < text.len() {
result.push((&text[last_end..], false));
}
result
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum LogLevel {
Trace,
Debug,
Info,
Warning,
Error,
Unknown,
}
impl LogLevel {
fn as_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warning => "WARN",
LogLevel::Error => "ERROR",
LogLevel::Unknown => "UNKNOWN",
}
}
}
impl From<&str> for LogLevel {
fn from(s: &str) -> Self {
match s.to_uppercase().as_str() {
"TRACE" => LogLevel::Trace,
"DEBUG" => LogLevel::Debug,
"INFO" => LogLevel::Info,
"WARNING" => LogLevel::Warning,
"ERROR" => LogLevel::Error,
_ => LogLevel::Unknown,
}
}
}
#[derive(Clone, Routable, Debug, PartialEq)]
enum Route {
#[route("/")]
Home {},
}
fn make_config() -> dioxus::desktop::Config {
// dioxus::desktop::Config::new()
// .with_custom_head(r#"<script src="/assets/tailwind.js"></script>"#.to_string())
let cfg = dioxus::desktop::Config::new();
#[cfg(not(debug_assertions))]
let cfg = cfg.with_disable_context_menu(true);
#[cfg(not(debug_assertions))]
let cfg = cfg.with_menu(None);
cfg.with_custom_head(r#"<link rel="stylesheet" href="assets/tailwind.css">"#.to_string())
.with_window(make_window())
}
fn make_window() -> WindowBuilder {
WindowBuilder::new()
.with_resizable(true)
.with_always_on_top(false)
}
fn main() {
// Init logger
dioxus_logger::init(Level::ERROR).expect("failed to init logger");
info!("starting app");
LaunchBuilder::desktop().with_cfg(make_config()).launch(App);
}
#[component]
fn App() -> Element {
rsx! {
Router::<Route> {}
}
}
fn parse_log_file(content: &str) -> Vec<LogEntry> {
let mut entries = Vec::new();
for line in content.lines() {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() >= 6 {
entries.push(LogEntry {
timestamp: parts[0].to_string(),
level: LogLevel::from(parts[1]),
category: parts[2].to_string(),
thread: parts[3].to_string(),
source: parts[4].to_string(),
message: parts[5..].join(" "),
});
}
}
entries
}
fn read_log_file(filename: &str) -> Vec<LogEntry> {
let mut entries = Vec::new();
if let Ok(mut file) = File::open(filename) {
let mut contents = String::new();
if file.read_to_string(&mut contents).is_ok() {
entries = parse_log_file(&contents);
}
}
entries
}
#[component]
fn Home() -> Element {
let mut log_entries = use_signal(|| {
let Args { log_file } = Args::parse();
log_file
.map(|log_file| read_log_file(&log_file))
.unwrap_or_default()
});
let mut search_query = use_signal(String::new);
let mut highlight = use_signal(String::new);
// State for column widths
let column_widths = [150.0, 80.0, 100.0, 140.0, 300.0, 150.0]; // Initial widths for 6 columns
// Columns visibility
let mut column_visibility = use_signal(|| vec![true; 6]); // All columns visible by default
// Column titles
let column_titles = [
"Timestamp",
"Level",
"Category",
"Thread",
"Source",
"Message",
];
rsx! {
div { class: "p-4",
// Search input
div {
class: "header",
div {
style: "display: flex;",
input {
style: "flex: 1;",
class: "border w-full mb-2",
r#type: "text",
value: "{search_query}",
placeholder: "Search...",
oninput: move |e| {
search_query.set(e.value());
}
}
input {
style: "flex: 1; margin-left: 8px;",
class: "border w-full mb-2",
r#type: "text",
value: "{highlight}",
placeholder: "Highlight...",
oninput: move |e| {
highlight.set(e.value());
}
},
input {
id: "files",
style: "display:none;",
class: "hidden",
r#type: "file",
onchange: move |e| {
async move {
if let Some(file_engine) = e.files() {
let files = file_engine.files();
if let Some(file) = file_engine.read_file_to_string(&files[0]).await
{
log_entries.set(parse_log_file(&file));
}
}
}
},
},
label {
r#for: "files",
class: "border w-full mb-2 bg-gray-200",
style: "flex: 1; margin-left: 8px;",
"Open log file"
}
}
// Column visibility checkboxes
div { class: "mb-4",
for (i, title) in column_titles.iter().enumerate() {
label { class: "mr-4",
input {
r#type: "checkbox",
checked: "{column_visibility()[i]}",
onchange: move |_| {
let mut visibility = column_visibility();
visibility[i] = !visibility[i];
column_visibility.set(visibility);
}
}
" {title} "
}
}
}
}
div {
class: "content",
// Log entries table
table { class: "table-auto w-full text-left",
thead { class: "bg-gray-200",
style: "position: sticky; top: 0;",
tr {
for (i, title) in column_titles.iter().enumerate() {
if column_visibility()[i] {
th {
class: "px-4 py-2 relative",
// Column title
"{title}"
}
}
}
}
}
tbody {
for entry in log_entries().iter().filter(|entry| {
entry.matches_query(&search_query())
}) {
tr { class: "hover:bg-gray-100",
for i in 0..6 {
if column_visibility()[i] {
match i {
0 => rsx! { td {
class: "resize-null",
style: "width: {column_widths[i]}px;min-width: {column_widths[i]}px;max-width: {column_widths[i]}px;",
for (text, is_highlighted) in highlight_matches(&entry.timestamp, &highlight()) {
if is_highlighted {
span { class: "bg-yellow-200", "{text}" }
} else {
"{text}"
}
}
} },
1 => rsx! { td {
class: "resize-null {entry.level_class()}",
style: "width: {column_widths[i]}px;min-width: {column_widths[i]}px;max-width: {column_widths[i]}px;",
for (text, is_highlighted) in highlight_matches(&entry.level.as_str(), &highlight()) {
if is_highlighted {
span { class: "bg-yellow-200", "{text}" }
} else {
"{text}"
}
}
} },
2 => rsx! { td {
class: "resize-null",
style: "width: {column_widths[i]}px;min-width: {column_widths[i]}px;max-width: {column_widths[i]}px;",
for (text, is_highlighted) in highlight_matches(&entry.category, &highlight()) {
if is_highlighted {
span { class: "bg-yellow-200", "{text}" }
} else {
"{text}"
}
}
} },
3 => rsx! { td {
class: "resize-null",
style: "width: {column_widths[i]}px;min-width: {column_widths[i]}px;max-width: {column_widths[i]}px;",
for (text, is_highlighted) in highlight_matches(&entry.thread, &highlight()) {
if is_highlighted {
span { class: "bg-yellow-200", "{text}" }
} else {
"{text}"
}
}
} },
4 => rsx! { td {
for (text, is_highlighted) in highlight_matches(&entry.source, &highlight()) {
if is_highlighted {
span { class: "bg-yellow-200", "{text}" }
} else {
"{text}"
}
}
} },
5 => rsx! { td {
for (text, is_highlighted) in highlight_matches(&entry.message, &highlight()) {
if is_highlighted {
span { class: "bg-yellow-200", "{text}" }
} else {
"{text}"
}
}
} },
_ => rsx!(),
}
}
}
}
}
}
}
},
}
}
}

9
tailwind.config.js Normal file
View File

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
mode: "all",
content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"],
theme: {
extend: {},
},
plugins: [],
};