mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat(highlight): integrate Highlight.js for syntax highlighting in JSON and SQL views
This commit is contained in:
@@ -20,7 +20,8 @@
|
||||
"@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
|
||||
"@std/assert": "jsr:@std/assert@^1.0.0",
|
||||
"marked": "npm:marked@^15.0.6",
|
||||
"simple-icons": "npm:simple-icons@^15.17.0"
|
||||
"simple-icons": "npm:simple-icons@^15.17.0",
|
||||
"highlight.js": "npm:highlight.js@^11.11.1"
|
||||
},
|
||||
"tasks": {
|
||||
"dev": "APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 deno run -A npm:vite dev",
|
||||
|
||||
2
deno.lock
generated
2
deno.lock
generated
@@ -1947,6 +1947,7 @@
|
||||
"dependencies": [
|
||||
"jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
|
||||
"jsr:@std/assert@1",
|
||||
"npm:highlight.js@^11.11.1",
|
||||
"npm:marked@^15.0.6",
|
||||
"npm:simple-icons@^15.17.0"
|
||||
],
|
||||
@@ -1965,6 +1966,7 @@
|
||||
"npm:eslint-plugin-svelte@^3.12.4",
|
||||
"npm:eslint@^9.36.0",
|
||||
"npm:globals@^16.4.0",
|
||||
"npm:highlight.js@^11.11.1",
|
||||
"npm:kysely@0.27.6",
|
||||
"npm:lucide-svelte@0.546",
|
||||
"npm:marked@^15.0.6",
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@deno/vite-plugin": "^1.0.5",
|
||||
"@jsr/db__sqlite": "^0.12.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"kysely": "0.27.6",
|
||||
"lucide-svelte": "^0.546.0",
|
||||
"marked": "^15.0.6",
|
||||
@@ -2586,6 +2587,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"dependencies": {
|
||||
"@deno/vite-plugin": "^1.0.5",
|
||||
"@jsr/db__sqlite": "^0.12.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"kysely": "0.27.6",
|
||||
"lucide-svelte": "^0.546.0",
|
||||
"marked": "^15.0.6",
|
||||
|
||||
67
src/app.css
67
src/app.css
@@ -2,6 +2,73 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap');
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* Highlight.js: atom-one-light for light mode */
|
||||
@import 'highlight.js/styles/atom-one-light.css';
|
||||
|
||||
/* Highlight.js: atom-one-dark for dark mode */
|
||||
.dark .hljs {
|
||||
color: #abb2bf;
|
||||
}
|
||||
.dark .hljs-comment,
|
||||
.dark .hljs-quote {
|
||||
color: #5c6370;
|
||||
font-style: italic;
|
||||
}
|
||||
.dark .hljs-doctag,
|
||||
.dark .hljs-keyword,
|
||||
.dark .hljs-formula {
|
||||
color: #c678dd;
|
||||
}
|
||||
.dark .hljs-section,
|
||||
.dark .hljs-name,
|
||||
.dark .hljs-selector-tag,
|
||||
.dark .hljs-deletion,
|
||||
.dark .hljs-subst {
|
||||
color: #e06c75;
|
||||
}
|
||||
.dark .hljs-literal {
|
||||
color: #56b6c2;
|
||||
}
|
||||
.dark .hljs-string,
|
||||
.dark .hljs-regexp,
|
||||
.dark .hljs-addition,
|
||||
.dark .hljs-attribute,
|
||||
.dark .hljs-meta .hljs-string {
|
||||
color: #98c379;
|
||||
}
|
||||
.dark .hljs-attr,
|
||||
.dark .hljs-variable,
|
||||
.dark .hljs-template-variable,
|
||||
.dark .hljs-type,
|
||||
.dark .hljs-selector-class,
|
||||
.dark .hljs-selector-attr,
|
||||
.dark .hljs-selector-pseudo,
|
||||
.dark .hljs-number {
|
||||
color: #d19a66;
|
||||
}
|
||||
.dark .hljs-symbol,
|
||||
.dark .hljs-bullet,
|
||||
.dark .hljs-link,
|
||||
.dark .hljs-meta,
|
||||
.dark .hljs-selector-id,
|
||||
.dark .hljs-title {
|
||||
color: #61aeee;
|
||||
}
|
||||
.dark .hljs-built_in,
|
||||
.dark .hljs-title.class_,
|
||||
.dark .hljs-class .hljs-title {
|
||||
color: #e6c07b;
|
||||
}
|
||||
.dark .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.dark .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.dark .hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
|
||||
57
src/lib/client/ui/meta/JsonView.svelte
Normal file
57
src/lib/client/ui/meta/JsonView.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
import sql from 'highlight.js/lib/languages/sql';
|
||||
|
||||
hljs.registerLanguage('json', json);
|
||||
hljs.registerLanguage('sql', sql);
|
||||
|
||||
export let data: unknown;
|
||||
|
||||
// Check if data has a queries array with SQL strings
|
||||
$: hasQueries = data && typeof data === 'object' && 'queries' in data && Array.isArray((data as Record<string, unknown>).queries);
|
||||
|
||||
// Extract queries separately for SQL highlighting
|
||||
$: queries = hasQueries ? ((data as Record<string, unknown>).queries as string[]) : [];
|
||||
|
||||
// Create data without queries for JSON display
|
||||
$: dataWithoutQueries = hasQueries
|
||||
? Object.fromEntries(Object.entries(data as Record<string, unknown>).filter(([k]) => k !== 'queries'))
|
||||
: data;
|
||||
|
||||
$: jsonString = JSON.stringify(dataWithoutQueries, null, 2);
|
||||
$: highlightedJson = hljs.highlight(jsonString, { language: 'json' }).value;
|
||||
|
||||
function highlightSql(query: string): string {
|
||||
return hljs.highlight(query, { language: 'sql' }).value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="json-view space-y-4">
|
||||
<!-- JSON metadata -->
|
||||
<pre class="!bg-transparent !p-0 !m-0 whitespace-pre-wrap font-mono"><code class="hljs font-mono">{@html highlightedJson}</code></pre>
|
||||
|
||||
<!-- SQL Queries -->
|
||||
{#if queries.length > 0}
|
||||
<div class="border-t border-neutral-200 pt-4 dark:border-neutral-700">
|
||||
<div class="mb-2 text-xs font-medium uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
|
||||
Queries ({queries.length})
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
{#each queries as query, i}
|
||||
<div class="rounded border border-neutral-200 bg-neutral-100 p-3 dark:border-neutral-600 dark:bg-neutral-900">
|
||||
<pre class="!bg-transparent !p-0 !m-0 whitespace-pre-wrap text-xs font-mono"><code class="hljs font-mono">{@html highlightSql(query)}</code></pre>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.json-view :global(.hljs),
|
||||
.json-view :global(.hljs *) {
|
||||
background: transparent !important;
|
||||
font-family: var(--font-mono) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,15 @@
|
||||
export let confirmText = 'Confirm';
|
||||
export let cancelText = 'Cancel';
|
||||
export let confirmDanger = false; // If true, confirm button is styled as danger (red)
|
||||
export let size: 'sm' | 'md' | 'lg' | 'xl' | '2xl' = 'md';
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-2xl',
|
||||
xl: 'max-w-4xl',
|
||||
'2xl': 'max-w-6xl'
|
||||
};
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -53,7 +62,7 @@
|
||||
>
|
||||
<!-- Modal -->
|
||||
<div
|
||||
class="relative w-full max-w-md rounded-lg border border-neutral-200 bg-white shadow-xl dark:border-neutral-700 dark:bg-neutral-900"
|
||||
class="relative w-full {sizeClasses[size]} rounded-lg border border-neutral-200 bg-white shadow-xl dark:border-neutral-700 dark:bg-neutral-900"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="border-b border-neutral-200 px-6 py-4 dark:border-neutral-800">
|
||||
|
||||
@@ -105,7 +105,8 @@
|
||||
return sortDirection === 'desc' ? sorted.reverse() : sorted;
|
||||
}
|
||||
|
||||
$: sortedData = sortData(data);
|
||||
$: sortedData = sortKey ? sortData(data) : data;
|
||||
$: sortKey, sortDirection, sortedData = sortData(data);
|
||||
</script>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-neutral-200 dark:border-neutral-800">
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import { Eye, Copy } from 'lucide-svelte';
|
||||
import { alertStore } from '$alerts/store';
|
||||
import Modal from '$ui/modal/Modal.svelte';
|
||||
import JsonView from '$ui/meta/JsonView.svelte';
|
||||
import Table from '$ui/table/Table.svelte';
|
||||
import type { Column } from '$ui/table/types';
|
||||
import LogsActionsBar from './components/LogsActionsBar.svelte';
|
||||
import { createSearchStore } from '$lib/client/stores/search';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
@@ -45,6 +48,38 @@
|
||||
ERROR: 'text-red-600 dark:text-red-400'
|
||||
};
|
||||
|
||||
// Table columns
|
||||
const columns: Column<LogEntry>[] = [
|
||||
{
|
||||
key: 'timestamp',
|
||||
header: 'Timestamp',
|
||||
sortable: true,
|
||||
sortAccessor: (row) => new Date(row.timestamp).getTime(),
|
||||
cell: (row) => ({
|
||||
html: `<span class="font-mono text-xs text-neutral-600 dark:text-neutral-400">${new Date(row.timestamp).toLocaleString()}</span>`
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'level',
|
||||
header: 'Level',
|
||||
sortable: true,
|
||||
cell: (row) => ({
|
||||
html: `<span class="font-semibold ${levelColors[row.level] || 'text-neutral-600 dark:text-neutral-400'}">${row.level}</span>`
|
||||
})
|
||||
},
|
||||
{
|
||||
key: 'source',
|
||||
header: 'Source',
|
||||
sortable: true,
|
||||
cell: (row) => row.source || '-'
|
||||
},
|
||||
{
|
||||
key: 'message',
|
||||
header: 'Message',
|
||||
cell: (row) => row.message
|
||||
}
|
||||
];
|
||||
|
||||
// Meta modal state
|
||||
let showMetaModal = false;
|
||||
let selectedMeta: unknown = null;
|
||||
@@ -155,98 +190,31 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Log Container -->
|
||||
<div
|
||||
class="overflow-hidden rounded-lg border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900"
|
||||
>
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-neutral-50 dark:bg-neutral-800">
|
||||
<tr>
|
||||
<th
|
||||
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
|
||||
<!-- Log Table -->
|
||||
<Table data={filteredLogs} {columns} emptyMessage="No logs found" hoverable={true} compact={true} initialSort={{ key: 'timestamp', direction: 'asc' }}>
|
||||
<svelte:fragment slot="actions" let:row>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => copyLog(row)}
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded border border-neutral-300 bg-white text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
title="Copy log entry"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
{#if row.meta}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => viewMeta(row.meta)}
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded border border-neutral-300 bg-white text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
title="View metadata"
|
||||
>
|
||||
Timestamp
|
||||
</th>
|
||||
<th
|
||||
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
|
||||
>
|
||||
Level
|
||||
</th>
|
||||
<th
|
||||
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
|
||||
>
|
||||
Source
|
||||
</th>
|
||||
<th
|
||||
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
|
||||
>
|
||||
Message
|
||||
</th>
|
||||
<th
|
||||
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="[&_tr:last-child_td]:border-b-0">
|
||||
{#each filteredLogs as log, index (`${log.timestamp}-${log.level}-${log.source}-${log.message}-${index}`)}
|
||||
<tr class="hover:bg-neutral-50 dark:hover:bg-neutral-800">
|
||||
<td
|
||||
class="border-b border-neutral-200 px-4 py-2 font-mono text-xs text-neutral-600 dark:border-neutral-800 dark:text-neutral-400"
|
||||
>
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
</td>
|
||||
<td
|
||||
class="border-b border-neutral-200 px-4 py-2 font-semibold dark:border-neutral-800 {levelColors[
|
||||
log.level
|
||||
] || 'text-neutral-600 dark:text-neutral-400'}"
|
||||
>
|
||||
{log.level}
|
||||
</td>
|
||||
<td
|
||||
class="border-b border-neutral-200 px-4 py-2 text-neutral-600 dark:border-neutral-800 dark:text-neutral-400"
|
||||
>
|
||||
{log.source || '-'}
|
||||
</td>
|
||||
<td
|
||||
class="border-b border-neutral-200 px-4 py-2 text-neutral-900 dark:border-neutral-800 dark:text-neutral-50"
|
||||
>
|
||||
{log.message}
|
||||
</td>
|
||||
<td class="border-b border-neutral-200 px-4 py-2 text-center dark:border-neutral-800">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => copyLog(log)}
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded border border-neutral-300 bg-white text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
title="Copy log entry"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
{#if log.meta}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => viewMeta(log.meta)}
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded border border-neutral-300 bg-white text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
title="View metadata"
|
||||
>
|
||||
<Eye size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-8 text-center text-neutral-500 dark:text-neutral-400">
|
||||
No logs found
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Eye size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<!-- Meta Modal -->
|
||||
@@ -256,14 +224,14 @@
|
||||
bodyMessage=""
|
||||
confirmText="Close"
|
||||
cancelText="Close"
|
||||
size="2xl"
|
||||
on:confirm={closeMetaModal}
|
||||
on:cancel={closeMetaModal}
|
||||
>
|
||||
<pre
|
||||
<div
|
||||
slot="body"
|
||||
class="max-h-[400px] overflow-auto rounded-lg bg-neutral-50 p-4 font-mono text-xs whitespace-pre-wrap text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50">{JSON.stringify(
|
||||
selectedMeta,
|
||||
null,
|
||||
2
|
||||
)}</pre>
|
||||
class="max-h-[70vh] overflow-auto rounded-lg bg-neutral-50 p-4 font-mono text-sm dark:bg-neutral-800"
|
||||
>
|
||||
<JsonView data={selectedMeta} />
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user