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",
|
"info": "Info",
|
||||||
"warn": "Warning",
|
"warn": "Warning",
|
||||||
"error": "Error"
|
"error": "Error"
|
||||||
}
|
},
|
||||||
|
"copy-to-clipboard": "Copy to clipboard",
|
||||||
|
"copied": "Copied!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { main } from '@wailsapp/go/models';
|
import { main } from '@wailsapp/go/models';
|
||||||
|
|
||||||
interface ProcessedLogEntry {
|
export interface ProcessedLogEntry {
|
||||||
time: Date;
|
time: Date;
|
||||||
caller: string;
|
caller: string;
|
||||||
level: string;
|
level: string;
|
||||||
|
@ -39,10 +39,12 @@ const loggingReducer = createSlice({
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
loadedLogData(state, { payload }: PayloadAction<main.LogEntry[]>) {
|
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>) {
|
receivedEvent(state, { payload }: PayloadAction<main.LogEntry>) {
|
||||||
state.messages = [...state.messages, processEntry(payload)];
|
state.messages = [processEntry(payload), ...state.messages];
|
||||||
},
|
},
|
||||||
clearedEvents(state) {
|
clearedEvents(state) {
|
||||||
state.messages = [];
|
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 React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
@ -13,7 +13,11 @@ import {
|
||||||
MultiToggle,
|
MultiToggle,
|
||||||
MultiToggleItem,
|
MultiToggleItem,
|
||||||
styled,
|
styled,
|
||||||
|
theme,
|
||||||
} from '../theme';
|
} from '../theme';
|
||||||
|
import { ProcessedLogEntry } from '../../store/logging/reducer';
|
||||||
|
import Scrollbar from './utils/Scrollbar';
|
||||||
|
import { delay } from '../../lib/time-utils';
|
||||||
|
|
||||||
const Floating = styled('div', {
|
const Floating = styled('div', {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
@ -22,6 +26,7 @@ const Floating = styled('div', {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: '3px',
|
gap: '3px',
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
|
transition: 'all 100ms',
|
||||||
});
|
});
|
||||||
|
|
||||||
const LogBubble = styled('div', {
|
const LogBubble = styled('div', {
|
||||||
|
@ -34,6 +39,10 @@ const LogBubble = styled('div', {
|
||||||
lineHeight: '0.7rem',
|
lineHeight: '0.7rem',
|
||||||
fontSize: '0.7rem',
|
fontSize: '0.7rem',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
opacity: '0.5',
|
||||||
|
'&:hover': {
|
||||||
|
opacity: '1',
|
||||||
|
},
|
||||||
variants: {
|
variants: {
|
||||||
level: {
|
level: {
|
||||||
info: {},
|
info: {},
|
||||||
|
@ -57,8 +66,14 @@ const emptyFilter = {
|
||||||
type LogLevel = keyof typeof emptyFilter;
|
type LogLevel = keyof typeof emptyFilter;
|
||||||
const levels: LogLevel[] = ['info', 'warn', 'error'];
|
const levels: LogLevel[] = ['info', 'warn', 'error'];
|
||||||
|
|
||||||
interface LogDialogProps {
|
function isSupportedLevel(level: string): level is LogLevel {
|
||||||
initialFilter: 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, {
|
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) {
|
function LogDialog({ initialFilter }: LogDialogProps) {
|
||||||
const logEntries = useSelector((state: RootState) => state.logging.messages);
|
const logEntries = useSelector((state: RootState) => state.logging.messages);
|
||||||
const [filter, setFilter] = useState({
|
const [filter, setFilter] = useState({
|
||||||
|
@ -105,11 +294,22 @@ function LogDialog({ initialFilter }: LogDialogProps) {
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
const filtered = logEntries.filter(
|
||||||
|
(entry) => entry.level in filter && filter[entry.level],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Portal>
|
<DialogPrimitive.Portal>
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogContainer>
|
<DialogContainer style={{ padding: '0.5rem' }}>
|
||||||
<DialogTitle style={{ display: 'flex', gap: '1rem' }}>
|
<DialogTitle
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '1rem',
|
||||||
|
margin: '-0.5rem',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t('logging.dialog-title')}
|
{t('logging.dialog-title')}
|
||||||
<MultiToggle
|
<MultiToggle
|
||||||
type="multiple"
|
type="multiple"
|
||||||
|
@ -141,7 +341,19 @@ function LogDialog({ initialFilter }: LogDialogProps) {
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</DialogPrimitive.DialogClose>
|
</DialogPrimitive.DialogClose>
|
||||||
</DialogTitle>
|
</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>
|
</DialogContainer>
|
||||||
</DialogPrimitive.Portal>
|
</DialogPrimitive.Portal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,7 +23,8 @@ const gradientAnimation = keyframes({
|
||||||
|
|
||||||
const LogoPic = styled('div', {
|
const LogoPic = styled('div', {
|
||||||
minHeight: '170px',
|
minHeight: '170px',
|
||||||
width: '270px',
|
width: '220px',
|
||||||
|
marginRight: '10px',
|
||||||
maskImage: `url(${logo as string})`,
|
maskImage: `url(${logo as string})`,
|
||||||
maskRepeat: 'no-repeat',
|
maskRepeat: 'no-repeat',
|
||||||
maskPosition: 'center',
|
maskPosition: 'center',
|
||||||
|
@ -47,7 +48,7 @@ const LogoPic = styled('div', {
|
||||||
const LogoName = styled('h1', {
|
const LogoName = styled('h1', {
|
||||||
fontSize: '40pt',
|
fontSize: '40pt',
|
||||||
fontWeight: 200,
|
fontWeight: 200,
|
||||||
textAlign: 'center',
|
textAlign: 'left',
|
||||||
'@medium': {
|
'@medium': {
|
||||||
fontSize: '80pt',
|
fontSize: '80pt',
|
||||||
},
|
},
|
||||||
|
@ -83,9 +84,9 @@ export default function StrimertulPage(): React.ReactElement {
|
||||||
const [debugCount, setDebugCount] = useState(0);
|
const [debugCount, setDebugCount] = useState(0);
|
||||||
const countForDebug = () => {
|
const countForDebug = () => {
|
||||||
if (debugCount < 5) {
|
if (debugCount < 5) {
|
||||||
setDebugCount(debugCount+1);
|
setDebugCount(debugCount + 1);
|
||||||
} else {
|
} else {
|
||||||
navigate("/debug");
|
navigate('/debug');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue