diff options
| author | Paul-Christian Volkmer | 2026-01-08 16:22:57 +0100 |
|---|---|---|
| committer | GitHub | 2026-01-08 15:22:57 +0000 |
| commit | ed4b068127530346345ed16b2e79b33bc5b03d57 (patch) | |
| tree | 173d36e24bb04ea61a2a960dd765e1c745dd6f6b /src/web | |
| parent | 7045318e87ecc853c000e9e11c955bc6298f2d56 (diff) | |
build: remove webjars and use custom build (#238)
Diffstat (limited to 'src/web')
| -rw-r--r-- | src/web/charts.js | 195 | ||||
| -rw-r--r-- | src/web/main.js | 18 | ||||
| -rw-r--r-- | src/web/rspack.config.js | 33 | ||||
| -rw-r--r-- | src/web/style.css | 781 |
4 files changed, 1027 insertions, 0 deletions
diff --git a/src/web/charts.js b/src/web/charts.js new file mode 100644 index 0000000..4826696 --- /dev/null +++ b/src/web/charts.js @@ -0,0 +1,195 @@ +import * as echarts from 'echarts/core'; +import { BarChart, PieChart } from 'echarts/charts'; +import { SVGRenderer } from 'echarts/renderers'; +import { + TitleComponent, + TooltipComponent, + DatasetComponent, + GridComponent +} from 'echarts/components'; + +echarts.use([ + BarChart, + PieChart, + TitleComponent, + TooltipComponent, + DatasetComponent, + GridComponent, + SVGRenderer +]); + +window.onload = () => { + drawPieChart('statistics/requeststates', 'piechart1', 'Statusverteilung aller Anfragen'); + drawPieChart('statistics/requestpatientstates', 'piechart2', 'Statusverteilung nach Patient'); + drawBarChart('statistics/requestslastmonth', 'barchart', 'Anfragen der letzten 30 Tage'); + + drawPieChart('statistics/requeststates?delete=true', 'piechartdel1', 'Statusverteilung aller Anfragen'); + drawPieChart('statistics/requestpatientstates?delete=true', 'piechartdel2', 'Statusverteilung nach Patient'); + drawBarChart('statistics/requestslastmonth?delete=true', 'barchartdel', 'Anfragen der letzten 30 Tage'); + + const eventSource = new EventSource('statistics/events'); + eventSource.addEventListener('requeststates', event => { + drawPieChart('statistics/requeststates', 'piechart1', 'Statusverteilung aller Anfragen', JSON.parse(event.data)); + }); + eventSource.addEventListener('requestpatientstates', event => { + drawPieChart('statistics/requestpatientstates', 'piechart2', 'Statusverteilung nach Patient', JSON.parse(event.data)); + }); + eventSource.addEventListener('requestslastmonth', event => { + drawBarChart('statistics/requestslastmonth', 'barchart', 'Anfragen des letzten Monats', JSON.parse(event.data)); + }); + + eventSource.addEventListener('deleterequeststates', event => { + drawPieChart('statistics/requeststates?delete=true', 'piechartdel1', 'Statusverteilung aller Anfragen', JSON.parse(event.data)); + }); + eventSource.addEventListener('deleterequestpatientstates', event => { + drawPieChart('statistics/requestpatientstates?delete=true', 'piechartdel2', 'Statusverteilung nach Patient', JSON.parse(event.data)); + }); + eventSource.addEventListener('deleterequestslastmonth', event => { + drawBarChart('statistics/requestslastmonth?delete=true', 'barchartdel', 'Anfragen des letzten Monats', JSON.parse(event.data)); + }); +} + +const dateFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit' }; +const dateFormat = new Intl.DateTimeFormat('de-DE', dateFormatOptions); + +export function drawPieChart(url, elemId, title, data) { + if (data) { + update(elemId, data); + } else { + fetch(url) + .then(resp => resp.json()) + .then(d => { + draw(elemId, title, d); + update(elemId, d); + }); + } + + function update(elemId, data) { + let chartDom = document.getElementById(elemId); + let chart = echarts.init(chartDom, null, {renderer: 'svg'}); + + let option = { + color: data.map(i => i.color), + animationDuration: 250, + animationDurationUpdate: 250, + series: [ + { + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + label: { + show: false, + position: 'center' + }, + labelLine: { + show: false + }, + data: data + } + ] + }; + + option && chart.setOption(option); + } + + function draw(elemId, title, data) { + let chartDom = document.getElementById(elemId); + let chart = echarts.init(chartDom, null, {renderer: 'svg'}); + let option= { + title: { + text: title, + left: 'center' + }, + tooltip: { + trigger: 'item' + }, + color: data.map(i => i.color), + animationDuration: 250, + animationDurationUpdate: 250 + }; + + option && chart.setOption(option); + } +} + +export function drawBarChart(url, elemId, title, data) { + if (data) { + update(elemId, data); + } else { + fetch(url) + .then(resp => resp.json()) + .then(data => { + draw(elemId, title, data); + update(elemId, data); + }); + } + + function update(elemId, data) { + let chartDom = document.getElementById(elemId); + let chart = echarts.init(chartDom, null, {renderer: 'svg'}); + + let option = { + series: [ + { + name: 'UNKNOWN', + type: 'bar', + stack: 'total', + data: data.map(i => i.nameValues.unknown) + }, + { + name: 'ERROR', + type: 'bar', + stack: 'total', + data: data.map(i => i.nameValues.error) + }, + { + name: 'WARNING', + type: 'bar', + stack: 'total', + data: data.map(i => i.nameValues.warning) + }, + { + name: 'SUCCESS', + type: 'bar', + stack: 'total', + data: data.map(i => i.nameValues.success) + }, + { + name: 'DUPLICATION', + type: 'bar', + stack: 'total', + data: data.map(i => i.nameValues.duplication) + } + ] + }; + + option && chart.setOption(option); + } + + function draw(elemId, title, data) { + let chartDom = document.getElementById(elemId); + let chart = echarts.init(chartDom, null, {renderer: 'svg'}); + let option= { + title: { + text: title, + left: 'center' + }, + xAxis: { + type: 'category', + data: data.map(i => dateFormat.format(Date.parse(i.date))) + }, + yAxis: { + type: 'value', + minInterval: 1 + }, + tooltip: { + trigger: 'item' + }, + color: ['slategray', 'red', 'darkorange', 'green', 'slategray'], + animationDuration: 250, + animationDurationUpdate: 250 + }; + + option && chart.setOption(option); + } +}
\ No newline at end of file diff --git a/src/web/main.js b/src/web/main.js new file mode 100644 index 0000000..ca1ecc9 --- /dev/null +++ b/src/web/main.js @@ -0,0 +1,18 @@ +import * as styles from './style.css'; + +import 'htmx.org'; + +const dateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: 'numeric', second: 'numeric' }; +const dateTimeFormat = new Intl.DateTimeFormat('de-DE', dateTimeFormatOptions); + +const formatTimeElements = () => { + Array.from(document.getElementsByTagName('time')).forEach((timeTag) => { + let date = Date.parse(timeTag.getAttribute('datetime')); + if (! isNaN(date)) { + timeTag.innerText = dateTimeFormat.format(date); + } + }); +}; + +window.addEventListener('load', formatTimeElements); +window.addEventListener('htmx:afterRequest', formatTimeElements);
\ No newline at end of file diff --git a/src/web/rspack.config.js b/src/web/rspack.config.js new file mode 100644 index 0000000..aa92d31 --- /dev/null +++ b/src/web/rspack.config.js @@ -0,0 +1,33 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default { + entry: { + main: './src/web/main.js', + charts: './src/web/charts.js' + }, + output: { + path: path.resolve(__dirname, '../../src/main/resources/static'), + chunkFilename: '[id].js', + sourceMap: false, + library: { + type: "window" + } + }, + module: { + rules: [ + { + test: /\.css$/, + use: [{ + loader: "postcss-loader" + }], + type: "css" + }, + ] + }, + experiments: { + css: true, + } +}
\ No newline at end of file diff --git a/src/web/style.css b/src/web/style.css new file mode 100644 index 0000000..3631750 --- /dev/null +++ b/src/web/style.css @@ -0,0 +1,781 @@ +:root { + --text: #333; + --table-border: rgba(16, 24, 40, .1); + + --dark: brightness(.90); + + --bg-blue: rgb(0, 74, 157); + --bg-blue-op: rgba(0, 74, 157, .35); + + --bg-green: rgb(0, 128, 0); + --bg-green-op: rgba(0, 128, 0, .35); + + + --bg-yellow: rgb(255, 140, 0); + --bg-yellow-op: rgba(255, 140, 0, .35); + + + --bg-red: rgb(255, 0, 0); + --bg-red-op: rgba(255, 0, 0, .35); + + --bg-gray: rgb(112, 128, 144); + --bg-gray-op: rgba(112, 128, 144, .35); +} + +* { + font-family: sans-serif; + box-sizing: border-box; +} + +html { + background: + linear-gradient(transparent 30rem, white 50rem), + linear-gradient(-135deg, transparent 20vw, #004d6e10 25vw, transparent 30vw), + linear-gradient(-135deg, transparent 30vw, #706f6f10 35vw, transparent 40vw), + linear-gradient(-135deg, transparent 40vw, #f59e0010 45vw, transparent 50vw); + overflow-y: scroll; +} + +body { + margin: 0; + font-size: .8rem; + color: var(--text); + background-size: contain; +} + +div.headline { + position: fixed; + display: block; + top: 0; + z-index: 1000; + width: 100%; + height: 5rem; + align-content: center; + background: white; + border-bottom: 1px solid var(--table-border); +} + +nav { + display: flex; + margin: 0 auto; + line-height: 1.5rem; + max-width: 1140px; +} + +nav a.nav-home { + margin: auto 0; + + color: var(--text); + line-height: 1.5rem; + text-decoration: none; + + font-size: 2rem; + font-weight: bold; +} + +nav a.nav-home > img { + width: 1.5rem; + vertical-align: middle; +} + +nav > ul { + display: block; + margin: 0 0 0 auto; + padding: 0; + width: max-content; +} + +nav > ul > li { + display: inline-block; + padding: 0 1rem; +} + +nav > ul > li.login { + margin: 0 0 0 1rem; + padding: 0 0 0 2rem; + border-left: 1px solid var(--table-border); + line-height: 3.5rem; +} + +nav > ul > li.login a { + text-decoration: none; + text-transform: none; + padding: 1rem; +} + +nav .login .user-name { + font-weight: bold; +} + +nav > ul > li.login > span { + display: inline-block; + margin: 0 .5rem; +} + +nav > ul > li.login .user-icon { + flex-direction: column; + display: inline flex; + vertical-align: middle; + inline-size: 4rem; +} + +nav > ul > li.login .user-icon img { + margin: 0 0 -1em 0; + width: 80%; + align-self: center; +} + +nav > ul > li.login .user-icon span { + padding: 0 .6em; + color: white; + font-size: .8rem; + font-weight: bold; + border-radius: 4px; + line-height: normal; + text-align: center; +} + +nav > ul > li.login .user-icon span.guest { + background: darkslategray; +} + +nav > ul > li.login .user-icon span.user { + background: darkgreen; +} + +nav > ul > li.login .user-icon span.admin { + background: darkred; +} + +nav li a { + color: var(--bg-blue); + text-transform: uppercase; + text-decoration: none; + font-weight: 700; +} + +nav li a:hover { + text-decoration: underline; +} + +a { + color: var(--bg-blue); +} + +.breadcrumps { + margin: 0 auto; + max-width: 1140px; +} + +.breadcrumps ul { + margin: 2px 0; + padding: .4rem 1rem; + list-style: none; + background: #eee; +} + +.breadcrumps ul li { + display: inline; +} + +.breadcrumps ul li + li:before { + padding: .4rem; + color: gray; + content: "/\00a0"; +} + +.breadcrumps ul li a { + color: var(--text); + text-decoration: none; +} + +.centered { + text-align: center; +} + +.container { + margin: auto; +} + +main { + margin: 6rem auto 0; + min-height: calc(100dvh - 10rem); +} + +main, .container { + max-width: 1140px; +} + +footer { + width: 100%; + height: 4rem; + position: relative; + align-content: center; + display: flex; + bottom: 0; + padding: 1rem 0; + border-top: 1px solid var(--table-border); + background: var(--bg-blue); + color: white; +} + +footer > .container > div { + margin: 0 auto; + max-width: 1140px; + display: inline-block; +} + +footer > .container > div, +footer > .container > div:after { + content: "-"; + padding-left: 1rem; +} + +footer > .container > div:last-child, +footer > .container > div:last-child:after { + content: ""; +} + +footer svg { + height: 1.4rem; + color: white; + vertical-align: text-bottom; +} + +section { + margin: 3rem 0; +} + +form { + margin: 1rem 0; + padding: 1rem; + + border: 1px solid lightgray; + border-radius: 3px; + background: #eee; + + text-align: center; +} + +form > h2 { + margin: 0; +} + +form.samplecode-input > div { + padding: 0.6rem; + display: inline-block; + + border: 1px solid lightgray; + border-radius: 3px; + + background: white; +} + +form.samplecode-input input { + padding: 0; + + border: none; + outline: none; + + text-align: left; + appearance: textfield; + font-size: 1.2rem; + font-weight: bold; +} + +form.samplecode-input input:focus-visible { + background: lightgreen; +} + +.login-form { + width: fit-content; + margin: 3rem auto; + padding: 2em 5rem; + + border: 1px solid var(--table-border); + border-radius: .5rem; + background: white; +} + +.login-form form { + width: 20rem; + margin: 0 auto; + display: grid; + grid-gap: .5rem; + + border: none; + background: none; +} + +.login-form img { + margin: 0 auto; + width: 4rem; + display: block; +} + +.userrole-form { + display: inline-block; +} + +.userrole-form form { + margin: 0; + padding: 0; + + border: none; + border-radius: 0; + background: none; + + text-align: inherit; +} + +.login-form form *, +.token-form form * { + padding: 0.5rem; + border: 1px solid var(--table-border); + border-radius: 3px; +} + +.login-form form hr, +.token-form form hr, +.userrole-form form hr { + padding: 0; + width: 100%; +} + +.login-form button, +.login-form a.btn, +.token-form button { + margin: 1rem 0; + background: var(--bg-blue); + color: white; + border: none; +} + +.userrole-form form select { + padding: 0.5rem; + border: none; + border-radius: 3px; + line-height: 1.2rem; + font-size: 0.8rem; +} + +.border { + padding: 1rem; + border: 1px solid var(--table-border); + border-radius: .5rem; + background: white; +} + +table, .chart { + border: 1px solid var(--table-border); + padding: 1.5rem; + + border-spacing: 0; + border-radius: .5rem; + + background: white; +} + +table { + min-width: 100%; + font-family: sans-serif; +} + +table.config-table td:first-child { + width: 26rem; + min-width: fit-content; +} + +table.config-table td > button:last-of-type { + float: right; +} + +.border > table { + padding: 0; + border: none; + background: transparent; +} + +.page-control { + border-radius: .5rem; + padding: 1rem 2rem; + text-align: center; + + line-height: 1.75em; +} + +.page-control a { + padding: 0 .25rem; + font-size: 1.75rem; + color: var(--bg-gray); + text-decoration: none; +} + +.page-control a[href] { + color: var(--bg-blue); +} + +.page-control span { + padding: 0 .5rem; + vertical-align: text-bottom; +} + +#samples-table.max { + width: 100vw; + position: fixed; + padding: 1rem; + top: 0; + left: 0; + background: white; + min-height: 100vh; +} + +table.samples { + max-width: 100%; + overflow-x: scroll; + display: block; +} + +th, td { + padding: 0.4rem .2rem; + + line-height: 2rem; + + text-align: left; + white-space: nowrap; + vertical-align: top; +} + +th { + border-bottom: 1px solid var(--bg-gray); +} + +td { + border-bottom: 1px solid var(--bg-gray-op); +} + +td, td > a { + font-family: monospace; +} + +tr:last-of-type > td { + border-bottom: none; +} + +td > small { + display: block; + text-align: center; +} + +td.patient-id { + min-width: 20rem; + text-overflow: ellipsis; + overflow: hidden; +} + +td.bg-blue, th.bg-blue, +td.bg-green, th.bg-green, +td.bg-yellow, th.bg-yellow, +td.bg-red, th.bg-red, +td.bg-gray, th.bg-gray +{ + width: 8rem; +} + +td.bg-blue > small, th.bg-blue > small { + background: var(--bg-blue); + color: white; + border-radius: 0.4rem; +} + +td.bg-green > small, th.bg-green > small { + background: var(--bg-green); + color: white; + border-radius: 0.4rem; +} + +td.bg-yellow > small, th.bg-yellow > small { + background: var(--bg-yellow); + color: white; + border-radius: 0.4rem; +} + +td.bg-red > small, th.bg-red > small { + background: var(--bg-red); + color: white; + border-radius: 0.4rem; +} + +td.bg-gray > small, th.bg-gray > small { + background: var(--bg-gray); + color: white; + border-radius: 0.4rem; +} + +.bg-path { + background: var(--bg-gray-op); +} + +.bg-from { + background: var(--bg-red-op); +} + +.bg-to { + background: var(--bg-green-op); +} + +.bg-path, .bg-from, .bg-to { + padding: 0.25rem 0.5rem; + border-radius: 3px; + + font-family: monospace; +} + +td.bg-shaded, th.bg-shaded { + background: repeating-linear-gradient(140deg, white, #e5e5f5 4px, white 8px); +} + +td.clipboard { + cursor: copy; +} + +td.clipboard.clipped { + box-shadow: 0 0 1rem lightgreen inset; +} + +.btn { + margin: 4px; + padding: 4px 8px; + + line-height: 1.2rem; + + border: 0 solid transparent; + border-radius: 3px; + + text-decoration: none; + font-size: 0.8rem; + font-weight: normal; + + cursor: pointer; +} + +.btn:active, +.btn:hover { + filter: drop-shadow(0px 1px 1px var(--bg-gray)) var(--dark); +} + +.btn:active { + translate: 0 1px; +} + +.btn.btn-red { + background: var(--bg-red); + color: white; +} + +.btn.btn-yellow { + background: var(--bg-yellow); + color: white; +} + +.btn.btn-green { + background: var(--bg-green); + color: white; +} + +.btn.btn-blue { + background: var(--bg-blue); + color: white; +} + +.btn.btn-delete:before { + content: '\1F5D1'; + padding: .2rem; +} + +button:disabled, +.btn:disabled { + background: slategray !important; + color: lightgray; + filter: none; + cursor: default; +} + +input.inline { + border: none; + font-size: 1.1rem; + outline: none; +} + +input.inline:focus-visible { + background: lightgreen; +} + +.monospace { + font-family: monospace; + color: #333333; + border-bottom: 1px dotted gray !important; +} + +.help { + padding: 1rem; + + border: 1px solid darkslategray; + border-radius: 3px; + background: slategray; + color: white; +} + +.help.error { + border: 3px dashed red; + background: darkorange; +} + +.help .help-header { + font-size: 1.2rem; + font-weight: bold; +} + +.charts { + display: grid; + grid-gap: 1em; + grid-template: + "a b" 28rem + "c c" 28rem / 1fr 1fr; +} + +.charts > .grid-left { + grid-area: a; +} + +.charts > .grid-right { + grid-area: b; +} + +.charts > .grid-full { + grid-area: c; +} + +.connection-display { + display: grid; + grid-template-columns: 10rem 16rem 10rem; + place-items: center; + width: fit-content; + margin: 1em auto; +} + +.connection-display > * { + text-align: center; + margin: auto 0; +} + +.connection-display .connection { + display: block; + width: 100%; + height: 4px; + background: repeating-linear-gradient(to left, white, white 2px, transparent 2px, transparent 8px, white 8px) var(--bg-red); +} + +.connection-display .connection.available { + background: var(--bg-green); +} + +.notification { + margin: 1rem; + padding: .5rem; + border-radius: 3px; + text-align: center; +} + +.notification.info { + color: var(--bg-blue); +} + +.notification.success { + color: var(--bg-green); +} + +.notification.notice { + color: var(--bg-yellow); +} + +.notification.error { + color: var(--bg-red); +} + +.tab { + padding: 1rem; + border: none; + border-radius: 3px 3px 0 0; + cursor: pointer; + transition: all 0.2s; + + font-weight: bold; +} + +.tab:hover, +.tab.active { + background: var(--bg-gray); + color: white; +} + +.tabcontent { + border: 2px solid var(--bg-gray); + border-radius: 0 .5rem .5rem .5rem; + display: none; + padding: 1rem; + background: white; +} + +.tabcontent.active { + display: block; +} + +a.reload { + display: none; + margin: 0; + vertical-align: top; + border-radius: 1.4rem; +} + +a.reload::before { + content: "⟳"; + font-size: 1.2rem; + vertical-align: top; +} + +a.reload span { + display: none; +} + +a.reload:hover span { + display: inline; +} + +.new-token { + padding: 1rem; + background: var(--bg-green-op); +} + +.new-token > pre { + margin: 0; + border: 1px solid var(--bg-green); + padding: .5rem; + width: max-content; + display: inline-block; +} + +.no-token { + padding: 1rem; + background: var(--bg-red-op); +} + +.issue-message { + font-family: monospace; + font-weight: bolder; +} + +.issue-path { + font-family: monospace; + line-height: 1rem; +} |
