mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-18 01:50:50 +00:00
feat: finished log viewer!
This commit is contained in:
parent
8b911ab553
commit
818b183abe
4 changed files with 231 additions and 14 deletions
|
@ -294,6 +294,8 @@
|
|||
"info": "Info",
|
||||
"warn": "Warning",
|
||||
"error": "Error"
|
||||
}
|
||||
},
|
||||
"copy-to-clipboard": "Copy to clipboard",
|
||||
"copied": "Copied!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { main } from '@wailsapp/go/models';
|
||||
|
||||
interface ProcessedLogEntry {
|
||||
export interface ProcessedLogEntry {
|
||||
time: Date;
|
||||
caller: string;
|
||||
level: string;
|
||||
|
@ -39,10 +39,12 @@ const loggingReducer = createSlice({
|
|||
initialState,
|
||||
reducers: {
|
||||
loadedLogData(state, { payload }: PayloadAction<main.LogEntry[]>) {
|
||||
state.messages = payload.map(processEntry);
|
||||
state.messages = payload
|
||||
.map(processEntry)
|
||||
.sort((a, b) => b.time.getTime() - a.time.getTime());
|
||||
},
|
||||
receivedEvent(state, { payload }: PayloadAction<main.LogEntry>) {
|
||||
state.messages = [...state.messages, processEntry(payload)];
|
||||
state.messages = [processEntry(payload), ...state.messages];
|
||||
},
|
||||
clearedEvents(state) {
|
||||
state.messages = [];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||
import { ClipboardCopyIcon, Cross2Icon } from '@radix-ui/react-icons';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
@ -13,7 +13,11 @@ import {
|
|||
MultiToggle,
|
||||
MultiToggleItem,
|
||||
styled,
|
||||
theme,
|
||||
} from '../theme';
|
||||
import { ProcessedLogEntry } from '../../store/logging/reducer';
|
||||
import Scrollbar from './utils/Scrollbar';
|
||||
import { delay } from '../../lib/time-utils';
|
||||
|
||||
const Floating = styled('div', {
|
||||
position: 'fixed',
|
||||
|
@ -22,6 +26,7 @@ const Floating = styled('div', {
|
|||
display: 'flex',
|
||||
gap: '3px',
|
||||
zIndex: 10,
|
||||
transition: 'all 100ms',
|
||||
});
|
||||
|
||||
const LogBubble = styled('div', {
|
||||
|
@ -34,6 +39,10 @@ const LogBubble = styled('div', {
|
|||
lineHeight: '0.7rem',
|
||||
fontSize: '0.7rem',
|
||||
cursor: 'pointer',
|
||||
opacity: '0.5',
|
||||
'&:hover': {
|
||||
opacity: '1',
|
||||
},
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
|
@ -57,8 +66,14 @@ const emptyFilter = {
|
|||
type LogLevel = keyof typeof emptyFilter;
|
||||
const levels: LogLevel[] = ['info', 'warn', 'error'];
|
||||
|
||||
interface LogDialogProps {
|
||||
initialFilter: LogLevel;
|
||||
function isSupportedLevel(level: string): level is LogLevel {
|
||||
return (levels as string[]).includes(level);
|
||||
}
|
||||
|
||||
function formatTime(time: Date): string {
|
||||
return [time.getHours(), time.getMinutes(), time.getSeconds()]
|
||||
.map((x) => x.toString().padStart(2, '0'))
|
||||
.join(':');
|
||||
}
|
||||
|
||||
const LevelToggle = styled(MultiToggleItem, {
|
||||
|
@ -87,6 +102,180 @@ const LevelToggle = styled(MultiToggleItem, {
|
|||
},
|
||||
});
|
||||
|
||||
interface LogItemProps {
|
||||
data: ProcessedLogEntry;
|
||||
}
|
||||
|
||||
const LogEntryContainer = styled('div', {
|
||||
borderRadius: theme.borderRadius.form,
|
||||
backgroundColor: '$gray4',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '75px 1fr',
|
||||
fontSize: '0.9em',
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
backgroundColor: '$yellow4',
|
||||
},
|
||||
error: {
|
||||
backgroundColor: '$red6',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const LogTime = styled('div', {
|
||||
backgroundColor: '$gray6',
|
||||
gridColumn: '1',
|
||||
gridRow: '1/3',
|
||||
padding: '0.2rem 0.5rem',
|
||||
textAlign: 'center',
|
||||
color: '$gray11',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderTopLeftRadius: theme.borderRadius.form,
|
||||
borderBottomLeftRadius: theme.borderRadius.form,
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
color: '$yellow11',
|
||||
backgroundColor: '$yellow6',
|
||||
},
|
||||
error: {
|
||||
color: '$red11',
|
||||
backgroundColor: '$red7',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const LogMessage = styled('div', {
|
||||
gridColumn: '2',
|
||||
padding: '0.4rem 0.5rem',
|
||||
});
|
||||
const LogActions = styled('div', {
|
||||
gridColumn: '3',
|
||||
padding: '0.4rem 0.5rem 0',
|
||||
'&:hover': {
|
||||
color: '$gray12',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
color: '$gray10',
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
'&:hover': {
|
||||
color: '$yellow11',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
'&:hover': {
|
||||
color: '$red11',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const LogDetails = styled('div', {
|
||||
gridRow: '2',
|
||||
gridColumn: '2/4',
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
fontSize: '0.7em',
|
||||
color: '$gray11',
|
||||
backgroundColor: '$gray3',
|
||||
padding: '0.2rem 0.3rem 0.1rem 0.5rem',
|
||||
borderBottomRightRadius: theme.borderRadius.form,
|
||||
borderBottomLeftRadius: theme.borderRadius.form,
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
backgroundColor: '$yellow3',
|
||||
},
|
||||
error: {
|
||||
backgroundColor: '$red4',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const LogDetailItem = styled('div', {
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
});
|
||||
const LogDetailKey = styled('div', {
|
||||
color: '$teal10',
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
color: '$yellow11',
|
||||
},
|
||||
error: {
|
||||
color: '$red11',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const LogDetailValue = styled('div', { flex: '1' });
|
||||
|
||||
function LogItem({ data }: LogItemProps) {
|
||||
const { t } = useTranslation();
|
||||
const levelStyle = isSupportedLevel(data.level) ? data.level : null;
|
||||
const details = Object.entries(data.data).filter(([key]) => key.length > 1);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyToClipboard = async () => {
|
||||
await navigator.clipboard.writeText(JSON.stringify(data.data));
|
||||
setCopied(true);
|
||||
await delay(2000);
|
||||
setCopied(false);
|
||||
};
|
||||
return (
|
||||
<LogEntryContainer level={levelStyle}>
|
||||
<LogTime level={levelStyle}>{formatTime(data.time)}</LogTime>
|
||||
<LogMessage>{data.message}</LogMessage>
|
||||
<LogActions level={levelStyle}>
|
||||
{copied ? (
|
||||
<span style={{ fontSize: '0.9em' }}>{t('logging.copied')}</span>
|
||||
) : (
|
||||
<a
|
||||
style={{ color: 'inherit' }}
|
||||
aria-label={t('logging.copy-to-clipboard')}
|
||||
title={t('logging.copy-to-clipboard')}
|
||||
onClick={() => {
|
||||
void copyToClipboard();
|
||||
}}
|
||||
>
|
||||
<ClipboardCopyIcon />
|
||||
</a>
|
||||
)}
|
||||
</LogActions>
|
||||
{details.length > 0 ? (
|
||||
<LogDetails level={levelStyle}>
|
||||
{details.map(([key, value]) => (
|
||||
<LogDetailItem>
|
||||
<LogDetailKey level={levelStyle}>{key}</LogDetailKey>
|
||||
<LogDetailValue>{JSON.stringify(value)}</LogDetailValue>
|
||||
</LogDetailItem>
|
||||
))}
|
||||
</LogDetails>
|
||||
) : null}
|
||||
</LogEntryContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const LogEntriesContainer = styled('div', {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '5px',
|
||||
});
|
||||
|
||||
interface LogDialogProps {
|
||||
initialFilter: LogLevel;
|
||||
}
|
||||
|
||||
function LogDialog({ initialFilter }: LogDialogProps) {
|
||||
const logEntries = useSelector((state: RootState) => state.logging.messages);
|
||||
const [filter, setFilter] = useState({
|
||||
|
@ -105,11 +294,22 @@ function LogDialog({ initialFilter }: LogDialogProps) {
|
|||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const filtered = logEntries.filter(
|
||||
(entry) => entry.level in filter && filter[entry.level],
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogOverlay />
|
||||
<DialogContainer>
|
||||
<DialogTitle style={{ display: 'flex', gap: '1rem' }}>
|
||||
<DialogContainer style={{ padding: '0.5rem' }}>
|
||||
<DialogTitle
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
margin: '-0.5rem',
|
||||
marginBottom: '0.5rem',
|
||||
}}
|
||||
>
|
||||
{t('logging.dialog-title')}
|
||||
<MultiToggle
|
||||
type="multiple"
|
||||
|
@ -141,7 +341,19 @@ function LogDialog({ initialFilter }: LogDialogProps) {
|
|||
</IconButton>
|
||||
</DialogPrimitive.DialogClose>
|
||||
</DialogTitle>
|
||||
<p></p>
|
||||
<Scrollbar
|
||||
vertical={true}
|
||||
viewport={{ maxHeight: 'calc(80vh - 100px)' }}
|
||||
>
|
||||
<LogEntriesContainer>
|
||||
{filtered.map((entry) => (
|
||||
<LogItem
|
||||
key={entry.caller + entry.time.getTime().toString()}
|
||||
data={entry}
|
||||
/>
|
||||
))}
|
||||
</LogEntriesContainer>
|
||||
</Scrollbar>
|
||||
</DialogContainer>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
|
|
|
@ -23,7 +23,8 @@ const gradientAnimation = keyframes({
|
|||
|
||||
const LogoPic = styled('div', {
|
||||
minHeight: '170px',
|
||||
width: '270px',
|
||||
width: '220px',
|
||||
marginRight: '10px',
|
||||
maskImage: `url(${logo as string})`,
|
||||
maskRepeat: 'no-repeat',
|
||||
maskPosition: 'center',
|
||||
|
@ -47,7 +48,7 @@ const LogoPic = styled('div', {
|
|||
const LogoName = styled('h1', {
|
||||
fontSize: '40pt',
|
||||
fontWeight: 200,
|
||||
textAlign: 'center',
|
||||
textAlign: 'left',
|
||||
'@medium': {
|
||||
fontSize: '80pt',
|
||||
},
|
||||
|
@ -85,7 +86,7 @@ export default function StrimertulPage(): React.ReactElement {
|
|||
if (debugCount < 5) {
|
||||
setDebugCount(debugCount + 1);
|
||||
} else {
|
||||
navigate("/debug");
|
||||
navigate('/debug');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue