mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-18 01:50:50 +00:00
feat: (incomplete) logging window
This commit is contained in:
parent
1ec6da850a
commit
17f00c6960
11 changed files with 404 additions and 126 deletions
118
frontend/package-lock.json
generated
118
frontend/package-lock.json
generated
|
@ -16,7 +16,10 @@
|
|||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@radix-ui/react-label": "^2.0.0",
|
||||
"@radix-ui/react-scroll-area": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.1",
|
||||
"@radix-ui/react-toggle": "^1.0.1",
|
||||
"@radix-ui/react-toggle-group": "^1.0.1",
|
||||
"@radix-ui/react-toolbar": "^1.0.1",
|
||||
"@redux-devtools/extension": "^3.2.3",
|
||||
"@reduxjs/toolkit": "^1.9.0",
|
||||
|
@ -29,15 +32,12 @@
|
|||
"i18next": "^22.0.6",
|
||||
"inter-ui": "^3.19.3",
|
||||
"normalize.css": "^8.0.1",
|
||||
"overlayscrollbars": "^2.0.1",
|
||||
"overlayscrollbars-react": "^0.5.0",
|
||||
"postcss-import": "^15.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.0.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-toastify": "^9.1.1",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"sass": "^1.56.1",
|
||||
"typescript": "^4.9.3",
|
||||
|
@ -708,6 +708,14 @@
|
|||
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
|
||||
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz",
|
||||
"integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
|
||||
|
@ -966,6 +974,27 @@
|
|||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.2.tgz",
|
||||
"integrity": "sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/number": "1.0.0",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.1.tgz",
|
||||
|
@ -1811,14 +1840,6 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
|
||||
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
|
@ -3921,20 +3942,6 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/overlayscrollbars": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.0.1.tgz",
|
||||
"integrity": "sha512-tER9iKasFqcJiYdtspHbzlhJJg8kicyj/Oag/GRAK658rDouat2BGFfSFg3AgIw/Yc9CQ78AuX6ieHgg1wQw7Q=="
|
||||
},
|
||||
"node_modules/overlayscrollbars-react": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.0.tgz",
|
||||
"integrity": "sha512-uCNTnkfWW74veoiEv3kSwoLelKt4e8gTNv65D771X3il0x5g5Yo0fUbro7SpQzR9yNgi23cvB2mQHTTdQH96pA==",
|
||||
"peerDependencies": {
|
||||
"overlayscrollbars": "^2.0.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
|
@ -4350,18 +4357,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-toastify": {
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz",
|
||||
"integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==",
|
||||
"dependencies": {
|
||||
"clsx": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -5634,6 +5629,14 @@
|
|||
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
|
||||
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
|
||||
},
|
||||
"@radix-ui/number": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz",
|
||||
"integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"@radix-ui/primitive": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
|
||||
|
@ -5831,6 +5834,23 @@
|
|||
"@radix-ui/react-use-controllable-state": "1.0.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-scroll-area": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.2.tgz",
|
||||
"integrity": "sha512-k8VseTxI26kcKJaX0HPwkvlNBPTs56JRdYzcZ/vzrNUkDlvXBy8sMc7WvCpYzZkHgb+hd72VW9MqkqecGtuNgg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/number": "1.0.0",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-separator": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.1.tgz",
|
||||
|
@ -6403,11 +6423,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"clsx": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
|
||||
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
|
@ -7859,17 +7874,6 @@
|
|||
"word-wrap": "^1.2.3"
|
||||
}
|
||||
},
|
||||
"overlayscrollbars": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.0.1.tgz",
|
||||
"integrity": "sha512-tER9iKasFqcJiYdtspHbzlhJJg8kicyj/Oag/GRAK658rDouat2BGFfSFg3AgIw/Yc9CQ78AuX6ieHgg1wQw7Q=="
|
||||
},
|
||||
"overlayscrollbars-react": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.0.tgz",
|
||||
"integrity": "sha512-uCNTnkfWW74veoiEv3kSwoLelKt4e8gTNv65D771X3il0x5g5Yo0fUbro7SpQzR9yNgi23cvB2mQHTTdQH96pA==",
|
||||
"requires": {}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
|
@ -8100,14 +8104,6 @@
|
|||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"react-toastify": {
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz",
|
||||
"integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==",
|
||||
"requires": {
|
||||
"clsx": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
|
|
@ -11,7 +11,10 @@
|
|||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@radix-ui/react-label": "^2.0.0",
|
||||
"@radix-ui/react-scroll-area": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.1",
|
||||
"@radix-ui/react-toggle": "^1.0.1",
|
||||
"@radix-ui/react-toggle-group": "^1.0.1",
|
||||
"@radix-ui/react-toolbar": "^1.0.1",
|
||||
"@redux-devtools/extension": "^3.2.3",
|
||||
"@reduxjs/toolkit": "^1.9.0",
|
||||
|
@ -24,15 +27,12 @@
|
|||
"i18next": "^22.0.6",
|
||||
"inter-ui": "^3.19.3",
|
||||
"normalize.css": "^8.0.1",
|
||||
"overlayscrollbars": "^2.0.1",
|
||||
"overlayscrollbars-react": "^0.5.0",
|
||||
"postcss-import": "^15.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.0.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-toastify": "^9.1.1",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"sass": "^1.56.1",
|
||||
"typescript": "^4.9.3",
|
||||
|
|
|
@ -1 +1 @@
|
|||
1de5a15d31eda16cf49dfabfe8142773
|
||||
b120abf6c619728b1d1a33e9dda709c0
|
|
@ -6,8 +6,6 @@ import { HashRouter } from 'react-router-dom';
|
|||
import 'inter-ui/inter.css';
|
||||
import '@fontsource/space-mono/index.css';
|
||||
import 'normalize.css/normalize.css';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
|
||||
import './locale/setup';
|
||||
|
||||
|
|
|
@ -286,5 +286,14 @@
|
|||
"text": "This page is still under construction, apologies for the lackluster view :("
|
||||
},
|
||||
"loading": "{{APPNAME}} is starting up, please wait!"
|
||||
},
|
||||
"logging": {
|
||||
"dialog-title": "Application logs",
|
||||
"levelFilter": "Filter per log severity",
|
||||
"level": {
|
||||
"info": "Info",
|
||||
"warn": "Warning",
|
||||
"error": "Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import { t } from 'i18next';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
|
||||
import {
|
||||
GetKilovoltBind,
|
||||
|
@ -43,6 +42,8 @@ import { APPNAME, styled } from './theme';
|
|||
|
||||
// @ts-expect-error Asset import
|
||||
import spinner from '../assets/icon-loading.svg';
|
||||
import Scrollbar from './components/utils/Scrollbar';
|
||||
import LogViewer from './components/LogViewer';
|
||||
|
||||
const LoadingDiv = styled('div', {
|
||||
display: 'flex',
|
||||
|
@ -218,7 +219,13 @@ export default function App(): JSX.Element {
|
|||
|
||||
return (
|
||||
<Container>
|
||||
<LogViewer />
|
||||
<Sidebar sections={sections} />
|
||||
<Scrollbar
|
||||
vertical={true}
|
||||
root={{ flex: 1 }}
|
||||
viewport={{ height: '100vh', flex: '1' }}
|
||||
>
|
||||
<PageContent>
|
||||
<PageWrapper role="main">
|
||||
<Routes>
|
||||
|
@ -242,7 +249,7 @@ export default function App(): JSX.Element {
|
|||
</Routes>
|
||||
</PageWrapper>
|
||||
</PageContent>
|
||||
<ToastContainer position="bottom-center" autoClose={5000} theme="dark" />
|
||||
</Scrollbar>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
194
frontend/src/ui/components/LogViewer.tsx
Normal file
194
frontend/src/ui/components/LogViewer.tsx
Normal file
|
@ -0,0 +1,194 @@
|
|||
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from 'src/store';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContainer,
|
||||
DialogOverlay,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
MultiToggle,
|
||||
MultiToggleItem,
|
||||
styled,
|
||||
} from '../theme';
|
||||
|
||||
const Floating = styled('div', {
|
||||
position: 'fixed',
|
||||
top: '6px',
|
||||
right: '10px',
|
||||
display: 'flex',
|
||||
gap: '3px',
|
||||
zIndex: 10,
|
||||
});
|
||||
|
||||
const LogBubble = styled('div', {
|
||||
borderRadius: '6px',
|
||||
minWidth: '10px',
|
||||
minHeight: '10px',
|
||||
backgroundColor: '$gray6',
|
||||
color: '$gray11',
|
||||
padding: '4px 5px 3px',
|
||||
lineHeight: '0.7rem',
|
||||
fontSize: '0.7rem',
|
||||
cursor: 'pointer',
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
backgroundColor: '$yellow6',
|
||||
color: '$yellow11',
|
||||
},
|
||||
error: {
|
||||
backgroundColor: '$red6',
|
||||
color: '$red11',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emptyFilter = {
|
||||
info: false,
|
||||
warn: false,
|
||||
error: false,
|
||||
};
|
||||
type LogLevel = keyof typeof emptyFilter;
|
||||
const levels: LogLevel[] = ['info', 'warn', 'error'];
|
||||
|
||||
interface LogDialogProps {
|
||||
initialFilter: LogLevel;
|
||||
}
|
||||
|
||||
const LevelToggle = styled(MultiToggleItem, {
|
||||
variants: {
|
||||
level: {
|
||||
info: {},
|
||||
warn: {
|
||||
backgroundColor: '$yellow4',
|
||||
'&:hover': {
|
||||
backgroundColor: '$yellow5',
|
||||
},
|
||||
"&[data-state='on']": {
|
||||
backgroundColor: '$yellow8',
|
||||
},
|
||||
},
|
||||
error: {
|
||||
backgroundColor: '$red4',
|
||||
'&:hover': {
|
||||
backgroundColor: '$red5',
|
||||
},
|
||||
"&[data-state='on']": {
|
||||
backgroundColor: '$red8',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function LogDialog({ initialFilter }: LogDialogProps) {
|
||||
const logEntries = useSelector((state: RootState) => state.logging.messages);
|
||||
const [filter, setFilter] = useState({
|
||||
...emptyFilter,
|
||||
[initialFilter]: true,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const enabled = levels.filter((level) => filter[level]);
|
||||
|
||||
const count = logEntries.reduce((acc, entry) => {
|
||||
if (entry.level in acc) {
|
||||
acc[entry.level] += 1;
|
||||
} else {
|
||||
acc[entry.level] = 1;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogOverlay />
|
||||
<DialogContainer>
|
||||
<DialogTitle style={{ display: 'flex', gap: '1rem' }}>
|
||||
{t('logging.dialog-title')}
|
||||
<MultiToggle
|
||||
type="multiple"
|
||||
aria-label={t(`logging.levelFilter`)}
|
||||
value={enabled}
|
||||
onValueChange={(values: LogLevel[]) => {
|
||||
const newFilter = { ...emptyFilter };
|
||||
values.forEach((level) => {
|
||||
newFilter[level] = true;
|
||||
});
|
||||
setFilter(newFilter);
|
||||
}}
|
||||
>
|
||||
{levels.map((level) => (
|
||||
<LevelToggle
|
||||
key={level}
|
||||
size="small"
|
||||
level={level}
|
||||
value={level}
|
||||
aria-label={t(`logging.level.${level}`)}
|
||||
>
|
||||
{t(`logging.level.${level}`)} ({count[level] ?? 0})
|
||||
</LevelToggle>
|
||||
))}
|
||||
</MultiToggle>
|
||||
<DialogPrimitive.DialogClose asChild>
|
||||
<IconButton>
|
||||
<Cross2Icon />
|
||||
</IconButton>
|
||||
</DialogPrimitive.DialogClose>
|
||||
</DialogTitle>
|
||||
<p></p>
|
||||
</DialogContainer>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function LogViewer() {
|
||||
const logEntries = useSelector((state: RootState) => state.logging.messages);
|
||||
const [activeDialog, setActiveDialog] = useState<LogLevel>(null);
|
||||
|
||||
const count = logEntries.reduce((acc, entry) => {
|
||||
if (entry.level in acc) {
|
||||
acc[entry.level] += 1;
|
||||
} else {
|
||||
acc[entry.level] = 1;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Floating>
|
||||
{levels.map((level) =>
|
||||
level in count && count[level] > 0 ? (
|
||||
<LogBubble
|
||||
key={level}
|
||||
level={level}
|
||||
onClick={() => setActiveDialog(level)}
|
||||
>
|
||||
{count[level]}
|
||||
</LogBubble>
|
||||
) : null,
|
||||
)}
|
||||
</Floating>
|
||||
|
||||
<Dialog
|
||||
open={!!activeDialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
// Reset dialog status on dialog close
|
||||
setActiveDialog(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{activeDialog ? <LogDialog initialFilter={activeDialog} /> : null}
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(LogViewer);
|
|
@ -3,13 +3,14 @@ import React, { useEffect, useState } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
|
||||
import { RootState } from '../../store';
|
||||
import { APPNAME, APPREPO } from '../theme';
|
||||
import BrowserLink from './BrowserLink';
|
||||
import Scrollbar from './utils/Scrollbar';
|
||||
|
||||
// @ts-expect-error Asset import
|
||||
import logo from '../../assets/icon-logo.svg';
|
||||
import BrowserLink from './BrowserLink';
|
||||
|
||||
export interface RouteSection {
|
||||
title: string;
|
||||
|
@ -38,6 +39,7 @@ const Header = styled('div', {
|
|||
});
|
||||
|
||||
const AppName = styled('h1', {
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
@ -49,6 +51,7 @@ const AppName = styled('h1', {
|
|||
});
|
||||
|
||||
const AppLink = styled(Link, {
|
||||
userSelect: 'none',
|
||||
all: 'unset',
|
||||
cursor: 'pointer',
|
||||
color: '$teal12',
|
||||
|
@ -69,6 +72,7 @@ const AppLink = styled(Link, {
|
|||
});
|
||||
|
||||
const VersionLabel = styled('div', {
|
||||
userSelect: 'none',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 'bold',
|
||||
|
@ -106,8 +110,10 @@ const MenuHeader = styled('header', {
|
|||
fontWeight: 'bold',
|
||||
padding: '0.5rem 0 0.5rem 0.8rem',
|
||||
color: '$teal9',
|
||||
userSelect: 'none',
|
||||
});
|
||||
const MenuLink = styled(Link, {
|
||||
userSelect: 'none',
|
||||
color: '$teal13 !important',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
@ -199,10 +205,7 @@ export default function Sidebar({
|
|||
|
||||
return (
|
||||
<Container>
|
||||
<OverlayScrollbarsComponent
|
||||
style={{ maxHeight: '100vh' }}
|
||||
options={{ scrollbars: { autoHide: 'scroll' } }}
|
||||
>
|
||||
<Scrollbar vertical={true} viewport={{ maxHeight: '100vh' }}>
|
||||
<Header>
|
||||
<AppLink to={'/about'} status={matchApp ? 'active' : 'default'}>
|
||||
<AppName>
|
||||
|
@ -230,7 +233,7 @@ export default function Sidebar({
|
|||
))}
|
||||
</MenuSection>
|
||||
))}
|
||||
</OverlayScrollbarsComponent>
|
||||
</Scrollbar>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
64
frontend/src/ui/components/utils/Scrollbar.tsx
Normal file
64
frontend/src/ui/components/utils/Scrollbar.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import React from 'react';
|
||||
import * as ScrollArea from '@radix-ui/react-scroll-area';
|
||||
import { styled } from '../../theme';
|
||||
|
||||
export interface ScrollbarProps {
|
||||
vertical?: boolean;
|
||||
horizontal?: boolean;
|
||||
root?: React.CSSProperties;
|
||||
viewport?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const StyledScrollbar = styled(ScrollArea.Scrollbar, {
|
||||
display: 'flex',
|
||||
userSelect: 'none',
|
||||
touchAction: 'none',
|
||||
padding: '2px',
|
||||
background: '$blackA6',
|
||||
transition: 'background 160ms ease-out',
|
||||
'&:hover': {
|
||||
background: '$blackA8',
|
||||
},
|
||||
});
|
||||
|
||||
const StyledThumb = styled(ScrollArea.Thumb, {
|
||||
flex: '1',
|
||||
background: '$teal6',
|
||||
borderRadius: '10px',
|
||||
position: 'relative',
|
||||
'&:hover': {
|
||||
background: '$teal8',
|
||||
},
|
||||
});
|
||||
|
||||
function Scrollbar({
|
||||
vertical,
|
||||
horizontal,
|
||||
root,
|
||||
viewport,
|
||||
children,
|
||||
}: React.PropsWithChildren<ScrollbarProps>): React.ReactElement {
|
||||
return (
|
||||
<ScrollArea.Root style={root ?? {}}>
|
||||
<ScrollArea.Viewport style={viewport ?? {}}>
|
||||
{children}
|
||||
</ScrollArea.Viewport>
|
||||
{vertical ? (
|
||||
<StyledScrollbar orientation="vertical" style={{ width: '10px' }}>
|
||||
<StyledThumb />
|
||||
</StyledScrollbar>
|
||||
) : null}
|
||||
{horizontal ? (
|
||||
<StyledScrollbar
|
||||
orientation="horizontal"
|
||||
style={{ flexDirection: 'column', height: '10px' }}
|
||||
>
|
||||
<StyledThumb />
|
||||
</StyledScrollbar>
|
||||
) : null}
|
||||
<ScrollArea.Corner />
|
||||
</ScrollArea.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Scrollbar);
|
|
@ -1,5 +1,6 @@
|
|||
import * as UnstyledLabel from '@radix-ui/react-label';
|
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||
import * as ToggleGroup from '@radix-ui/react-toggle-group';
|
||||
import { styled } from './theme';
|
||||
import { theme } from '.';
|
||||
|
||||
|
@ -115,15 +116,15 @@ export const MultiButton = styled('div', {
|
|||
display: 'flex',
|
||||
});
|
||||
|
||||
export const Button = styled('button', {
|
||||
const button = {
|
||||
all: 'unset',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
color: '$gray12',
|
||||
fontWeight: '300',
|
||||
padding: '0.5rem 1rem',
|
||||
borderRadius: theme.borderRadius.form,
|
||||
fontSize: '1.1rem',
|
||||
padding: '0.5rem 1rem',
|
||||
border: '1px solid $gray6',
|
||||
backgroundColor: '$gray4',
|
||||
display: 'flex',
|
||||
|
@ -220,6 +221,37 @@ export const Button = styled('button', {
|
|||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MultiToggle = styled(ToggleGroup.Root, {
|
||||
display: 'inline-flex',
|
||||
borderRadius: theme.borderRadius.form,
|
||||
backgroundColor: '$gray4',
|
||||
});
|
||||
|
||||
export const MultiToggleItem = styled(ToggleGroup.Item, {
|
||||
...button,
|
||||
borderRadius: 0,
|
||||
border: 0,
|
||||
'&:first-child': {
|
||||
borderTopLeftRadius: theme.borderRadius.form,
|
||||
borderBottomLeftRadius: theme.borderRadius.form,
|
||||
},
|
||||
'&:last-child': {
|
||||
borderTopRightRadius: theme.borderRadius.form,
|
||||
borderBottomRightRadius: theme.borderRadius.form,
|
||||
},
|
||||
'&:hover': {
|
||||
...button['&:hover'],
|
||||
},
|
||||
"&[data-state='on']": {
|
||||
...button['&:active'],
|
||||
background: '$gray8',
|
||||
},
|
||||
});
|
||||
|
||||
export const Button = styled('button', {
|
||||
...button,
|
||||
});
|
||||
|
||||
export const ComboBox = styled('select', {
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package http
|
||||
|
||||
import "sync"
|
||||
|
||||
type SafeBool struct {
|
||||
val bool
|
||||
mux sync.RWMutex
|
||||
}
|
||||
|
||||
func newSafeBool(val bool) *SafeBool {
|
||||
return &SafeBool{val: val, mux: sync.RWMutex{}}
|
||||
}
|
||||
|
||||
func (s *SafeBool) Set(val bool) {
|
||||
s.mux.Lock()
|
||||
s.val = val
|
||||
s.mux.Unlock()
|
||||
}
|
||||
|
||||
func (s *SafeBool) Get() bool {
|
||||
s.mux.RLock()
|
||||
val := s.val
|
||||
s.mux.RUnlock()
|
||||
return val
|
||||
}
|
Loading…
Reference in a new issue