1
0
Fork 0
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:
Ash Keel 2022-11-25 13:44:00 +01:00
parent 8b911ab553
commit 818b183abe
No known key found for this signature in database
GPG key ID: BAD8D93E7314ED3E
4 changed files with 231 additions and 14 deletions

View file

@ -294,6 +294,8 @@
"info": "Info",
"warn": "Warning",
"error": "Error"
}
},
"copy-to-clipboard": "Copy to clipboard",
"copied": "Copied!"
}
}

View file

@ -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 = [];

View file

@ -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>
);

View file

@ -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',
},
@ -83,9 +84,9 @@ export default function StrimertulPage(): React.ReactElement {
const [debugCount, setDebugCount] = useState(0);
const countForDebug = () => {
if (debugCount < 5) {
setDebugCount(debugCount+1);
setDebugCount(debugCount + 1);
} else {
navigate("/debug");
navigate('/debug');
}
};