mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +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-dialog": "^1.0.2",
|
||||||
"@radix-ui/react-icons": "^1.1.1",
|
"@radix-ui/react-icons": "^1.1.1",
|
||||||
"@radix-ui/react-label": "^2.0.0",
|
"@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-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",
|
"@radix-ui/react-toolbar": "^1.0.1",
|
||||||
"@redux-devtools/extension": "^3.2.3",
|
"@redux-devtools/extension": "^3.2.3",
|
||||||
"@reduxjs/toolkit": "^1.9.0",
|
"@reduxjs/toolkit": "^1.9.0",
|
||||||
|
@ -29,15 +32,12 @@
|
||||||
"i18next": "^22.0.6",
|
"i18next": "^22.0.6",
|
||||||
"inter-ui": "^3.19.3",
|
"inter-ui": "^3.19.3",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"overlayscrollbars": "^2.0.1",
|
|
||||||
"overlayscrollbars-react": "^0.5.0",
|
|
||||||
"postcss-import": "^15.0.0",
|
"postcss-import": "^15.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^12.0.0",
|
"react-i18next": "^12.0.0",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
"react-router-dom": "^6.4.3",
|
"react-router-dom": "^6.4.3",
|
||||||
"react-toastify": "^9.1.1",
|
|
||||||
"redux-thunk": "^2.4.2",
|
"redux-thunk": "^2.4.2",
|
||||||
"sass": "^1.56.1",
|
"sass": "^1.56.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
|
@ -708,6 +708,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
|
||||||
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
|
"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": {
|
"node_modules/@radix-ui/primitive": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
|
"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"
|
"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": {
|
"node_modules/@radix-ui/react-separator": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.1.tgz",
|
||||||
|
@ -1811,14 +1840,6 @@
|
||||||
"node": ">= 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": {
|
"node_modules/color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
@ -3921,20 +3942,6 @@
|
||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
|
||||||
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
|
"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": {
|
"@radix-ui/primitive": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
|
"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-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": {
|
"@radix-ui/react-separator": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.1.tgz",
|
"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": {
|
"color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
@ -7859,17 +7874,6 @@
|
||||||
"word-wrap": "^1.2.3"
|
"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": {
|
"p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
|
@ -8100,14 +8104,6 @@
|
||||||
"tslib": "^2.0.0"
|
"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": {
|
"read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"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-dialog": "^1.0.2",
|
||||||
"@radix-ui/react-icons": "^1.1.1",
|
"@radix-ui/react-icons": "^1.1.1",
|
||||||
"@radix-ui/react-label": "^2.0.0",
|
"@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-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",
|
"@radix-ui/react-toolbar": "^1.0.1",
|
||||||
"@redux-devtools/extension": "^3.2.3",
|
"@redux-devtools/extension": "^3.2.3",
|
||||||
"@reduxjs/toolkit": "^1.9.0",
|
"@reduxjs/toolkit": "^1.9.0",
|
||||||
|
@ -24,15 +27,12 @@
|
||||||
"i18next": "^22.0.6",
|
"i18next": "^22.0.6",
|
||||||
"inter-ui": "^3.19.3",
|
"inter-ui": "^3.19.3",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"overlayscrollbars": "^2.0.1",
|
|
||||||
"overlayscrollbars-react": "^0.5.0",
|
|
||||||
"postcss-import": "^15.0.0",
|
"postcss-import": "^15.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^12.0.0",
|
"react-i18next": "^12.0.0",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
"react-router-dom": "^6.4.3",
|
"react-router-dom": "^6.4.3",
|
||||||
"react-toastify": "^9.1.1",
|
|
||||||
"redux-thunk": "^2.4.2",
|
"redux-thunk": "^2.4.2",
|
||||||
"sass": "^1.56.1",
|
"sass": "^1.56.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.3",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
1de5a15d31eda16cf49dfabfe8142773
|
b120abf6c619728b1d1a33e9dda709c0
|
|
@ -6,8 +6,6 @@ import { HashRouter } from 'react-router-dom';
|
||||||
import 'inter-ui/inter.css';
|
import 'inter-ui/inter.css';
|
||||||
import '@fontsource/space-mono/index.css';
|
import '@fontsource/space-mono/index.css';
|
||||||
import 'normalize.css/normalize.css';
|
import 'normalize.css/normalize.css';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
|
||||||
import 'overlayscrollbars/overlayscrollbars.css';
|
|
||||||
|
|
||||||
import './locale/setup';
|
import './locale/setup';
|
||||||
|
|
||||||
|
|
|
@ -286,5 +286,14 @@
|
||||||
"text": "This page is still under construction, apologies for the lackluster view :("
|
"text": "This page is still under construction, apologies for the lackluster view :("
|
||||||
},
|
},
|
||||||
"loading": "{{APPNAME}} is starting up, please wait!"
|
"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 React, { useEffect, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import { ToastContainer } from 'react-toastify';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GetKilovoltBind,
|
GetKilovoltBind,
|
||||||
|
@ -43,6 +42,8 @@ import { APPNAME, styled } from './theme';
|
||||||
|
|
||||||
// @ts-expect-error Asset import
|
// @ts-expect-error Asset import
|
||||||
import spinner from '../assets/icon-loading.svg';
|
import spinner from '../assets/icon-loading.svg';
|
||||||
|
import Scrollbar from './components/utils/Scrollbar';
|
||||||
|
import LogViewer from './components/LogViewer';
|
||||||
|
|
||||||
const LoadingDiv = styled('div', {
|
const LoadingDiv = styled('div', {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -218,7 +219,13 @@ export default function App(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
<LogViewer />
|
||||||
<Sidebar sections={sections} />
|
<Sidebar sections={sections} />
|
||||||
|
<Scrollbar
|
||||||
|
vertical={true}
|
||||||
|
root={{ flex: 1 }}
|
||||||
|
viewport={{ height: '100vh', flex: '1' }}
|
||||||
|
>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<PageWrapper role="main">
|
<PageWrapper role="main">
|
||||||
<Routes>
|
<Routes>
|
||||||
|
@ -242,7 +249,7 @@ export default function App(): JSX.Element {
|
||||||
</Routes>
|
</Routes>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
<ToastContainer position="bottom-center" autoClose={5000} theme="dark" />
|
</Scrollbar>
|
||||||
</Container>
|
</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 { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
|
import { Link, useMatch, useResolvedPath } from 'react-router-dom';
|
||||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
|
||||||
import { RootState } from '../../store';
|
import { RootState } from '../../store';
|
||||||
import { APPNAME, APPREPO } from '../theme';
|
import { APPNAME, APPREPO } from '../theme';
|
||||||
|
import BrowserLink from './BrowserLink';
|
||||||
|
import Scrollbar from './utils/Scrollbar';
|
||||||
|
|
||||||
// @ts-expect-error Asset import
|
// @ts-expect-error Asset import
|
||||||
import logo from '../../assets/icon-logo.svg';
|
import logo from '../../assets/icon-logo.svg';
|
||||||
import BrowserLink from './BrowserLink';
|
|
||||||
|
|
||||||
export interface RouteSection {
|
export interface RouteSection {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -38,6 +39,7 @@ const Header = styled('div', {
|
||||||
});
|
});
|
||||||
|
|
||||||
const AppName = styled('h1', {
|
const AppName = styled('h1', {
|
||||||
|
userSelect: 'none',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
@ -49,6 +51,7 @@ const AppName = styled('h1', {
|
||||||
});
|
});
|
||||||
|
|
||||||
const AppLink = styled(Link, {
|
const AppLink = styled(Link, {
|
||||||
|
userSelect: 'none',
|
||||||
all: 'unset',
|
all: 'unset',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: '$teal12',
|
color: '$teal12',
|
||||||
|
@ -69,6 +72,7 @@ const AppLink = styled(Link, {
|
||||||
});
|
});
|
||||||
|
|
||||||
const VersionLabel = styled('div', {
|
const VersionLabel = styled('div', {
|
||||||
|
userSelect: 'none',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
@ -106,8 +110,10 @@ const MenuHeader = styled('header', {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
padding: '0.5rem 0 0.5rem 0.8rem',
|
padding: '0.5rem 0 0.5rem 0.8rem',
|
||||||
color: '$teal9',
|
color: '$teal9',
|
||||||
|
userSelect: 'none',
|
||||||
});
|
});
|
||||||
const MenuLink = styled(Link, {
|
const MenuLink = styled(Link, {
|
||||||
|
userSelect: 'none',
|
||||||
color: '$teal13 !important',
|
color: '$teal13 !important',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
@ -199,10 +205,7 @@ export default function Sidebar({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<OverlayScrollbarsComponent
|
<Scrollbar vertical={true} viewport={{ maxHeight: '100vh' }}>
|
||||||
style={{ maxHeight: '100vh' }}
|
|
||||||
options={{ scrollbars: { autoHide: 'scroll' } }}
|
|
||||||
>
|
|
||||||
<Header>
|
<Header>
|
||||||
<AppLink to={'/about'} status={matchApp ? 'active' : 'default'}>
|
<AppLink to={'/about'} status={matchApp ? 'active' : 'default'}>
|
||||||
<AppName>
|
<AppName>
|
||||||
|
@ -230,7 +233,7 @@ export default function Sidebar({
|
||||||
))}
|
))}
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
))}
|
))}
|
||||||
</OverlayScrollbarsComponent>
|
</Scrollbar>
|
||||||
</Container>
|
</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 UnstyledLabel from '@radix-ui/react-label';
|
||||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
||||||
|
import * as ToggleGroup from '@radix-ui/react-toggle-group';
|
||||||
import { styled } from './theme';
|
import { styled } from './theme';
|
||||||
import { theme } from '.';
|
import { theme } from '.';
|
||||||
|
|
||||||
|
@ -115,15 +116,15 @@ export const MultiButton = styled('div', {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Button = styled('button', {
|
const button = {
|
||||||
all: 'unset',
|
all: 'unset',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
color: '$gray12',
|
color: '$gray12',
|
||||||
fontWeight: '300',
|
fontWeight: '300',
|
||||||
padding: '0.5rem 1rem',
|
|
||||||
borderRadius: theme.borderRadius.form,
|
borderRadius: theme.borderRadius.form,
|
||||||
fontSize: '1.1rem',
|
fontSize: '1.1rem',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
border: '1px solid $gray6',
|
border: '1px solid $gray6',
|
||||||
backgroundColor: '$gray4',
|
backgroundColor: '$gray4',
|
||||||
display: 'flex',
|
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', {
|
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