mirror of
https://github.com/pcvolkmer/etl-processor.git
synced 2025-04-19 17:26:51 +00:00
Add start and statistics page
This commit is contained in:
parent
46928964ef
commit
cd46fa7e09
42
src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
Normal file
42
src/main/kotlin/dev/dnpm/etl/processor/web/HomeController.kt
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ETL-Processor
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
import org.springframework.ui.Model
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(path = ["/"])
|
||||||
|
class HomeController(
|
||||||
|
private val requestRepository: RequestRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun index(model: Model): String {
|
||||||
|
val requests = requestRepository.findAll().sortedByDescending { it.processedAt }.take(25)
|
||||||
|
model.addAttribute("requests", requests)
|
||||||
|
|
||||||
|
return "index"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ETL-Processor
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(path = ["/statistics"])
|
||||||
|
class StatisticsController {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun index(): String {
|
||||||
|
return "statistics"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ETL-Processor
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Comprehensive Cancer Center Mainfranken, Datenintegrationszentrum Philipps-Universität Marburg and Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.dnpm.etl.processor.web
|
||||||
|
|
||||||
|
import dev.dnpm.etl.processor.monitoring.RequestRepository
|
||||||
|
import dev.dnpm.etl.processor.monitoring.RequestStatus
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.Month
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import java.time.temporal.TemporalUnit
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = ["/statistics"])
|
||||||
|
class StatisticsRestController(
|
||||||
|
private val requestRepository: RequestRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping(path = ["requeststates"])
|
||||||
|
fun requestStates(): List<NameValue> {
|
||||||
|
return requestRepository.findAll()
|
||||||
|
.groupBy { it.status }
|
||||||
|
.map {
|
||||||
|
val color = when (it.key) {
|
||||||
|
RequestStatus.ERROR -> "red"
|
||||||
|
RequestStatus.WARNING -> "darkorange"
|
||||||
|
RequestStatus.SUCCESS -> "green"
|
||||||
|
else -> "slategray"
|
||||||
|
}
|
||||||
|
NameValue(it.key.toString(), it.value.size, color)
|
||||||
|
}
|
||||||
|
.sortedByDescending { it.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = ["requestslastmonth"])
|
||||||
|
fun requestsLastMonth(): List<DateNameValues> {
|
||||||
|
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("Europe/Berlin"))
|
||||||
|
val data = requestRepository.findAll()
|
||||||
|
.filter { it.processedAt.isAfter(Instant.now().minus(30, ChronoUnit.DAYS)) }
|
||||||
|
.groupBy { formatter.format(it.processedAt) }
|
||||||
|
.map {
|
||||||
|
val requestList = it.value
|
||||||
|
.groupBy { it.status }
|
||||||
|
.map {
|
||||||
|
Pair(it.key, it.value.size)
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
Pair(
|
||||||
|
it.key.toString(),
|
||||||
|
DateNameValues(it.key.toString(), NameValues(
|
||||||
|
error = requestList[RequestStatus.ERROR] ?: 0,
|
||||||
|
warning = requestList[RequestStatus.WARNING] ?: 0,
|
||||||
|
success = requestList[RequestStatus.SUCCESS] ?: 0,
|
||||||
|
duplication = requestList[RequestStatus.DUPLICATION] ?: 0,
|
||||||
|
unknown = requestList[RequestStatus.UNKNOWN] ?: 0,
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
return (0L..30L).map { Instant.now().minus(it, ChronoUnit.DAYS) }
|
||||||
|
.map { formatter.format(it) }
|
||||||
|
.map {
|
||||||
|
DateNameValues(it, data[it]?.nameValues ?: NameValues())
|
||||||
|
}
|
||||||
|
.sortedBy { it.date }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NameValue(val name: String, val value: Int, val color: String)
|
||||||
|
|
||||||
|
data class DateNameValues(val date: String, val nameValues: NameValues)
|
||||||
|
|
||||||
|
data class NameValues(val error: Int = 0, val warning: Int = 0, val success: Int = 0, val duplication: Int = 0, val unknown: Int = 0)
|
45
src/main/resources/static/echarts.min.js
vendored
Normal file
45
src/main/resources/static/echarts.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
113
src/main/resources/static/scripts.js
Normal file
113
src/main/resources/static/scripts.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
const dateFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit' };
|
||||||
|
const dateFormat = new Intl.DateTimeFormat('de-DE', dateFormatOptions);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
window.onload = () => {
|
||||||
|
Array.from(document.getElementsByTagName('time')).forEach((timeTag) => {
|
||||||
|
let date = Date.parse(timeTag.getAttribute('datetime'));
|
||||||
|
if (! isNaN(date)) {
|
||||||
|
timeTag.innerText = dateTimeFormat.format(date);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function drawPieChart(url, elemId, title) {
|
||||||
|
fetch(url)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(data => {
|
||||||
|
let chartDom = document.getElementById(elemId);
|
||||||
|
let chart = echarts.init(chartDom);
|
||||||
|
let option= {
|
||||||
|
title: {
|
||||||
|
text: title,
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item'
|
||||||
|
},
|
||||||
|
color: data.map(i => i.color),
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center'
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
data: data
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
option && chart.setOption(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBarChart(url, elemId, title) {
|
||||||
|
fetch(url)
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(data => {
|
||||||
|
let chartDom = document.getElementById(elemId);
|
||||||
|
let chart = echarts.init(chartDom);
|
||||||
|
let option= {
|
||||||
|
title: {
|
||||||
|
text: title,
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: data.map(i => dateFormat.format(Date.parse(i.date)))
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
minInterval: 2,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item'
|
||||||
|
},
|
||||||
|
animation: false,
|
||||||
|
color: ['slategray', 'red', 'darkorange', 'green', 'slategray'],
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
284
src/main/resources/static/style.css
Normal file
284
src/main/resources/static/style.css
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: .8rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #d5dad5;
|
||||||
|
height: 3rem;
|
||||||
|
max-width: 1140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
color: #004a8f;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > ul {
|
||||||
|
margin: 0 3rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > ul > li {
|
||||||
|
background: #fbfbfb;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
padding: 2px 1rem;
|
||||||
|
border-left: 1px solid #d5dad5;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > ul > li:first-of-type {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: #333333;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 1140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-top: 1px solid lightgray;
|
||||||
|
border-left: 1px solid lightgray;
|
||||||
|
border-spacing: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
min-width: 100%;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
padding: .2rem;
|
||||||
|
|
||||||
|
border-right: 1px solid lightgray;
|
||||||
|
border-bottom: 1px solid lightgray;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.bg-green, th.bg-green {
|
||||||
|
background: green;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.bg-yellow, th.bg-yellow {
|
||||||
|
background: darkorange;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.bg-red, th.bg-red {
|
||||||
|
background: red;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.bg-gray, th.bg-gray {
|
||||||
|
background: slategray;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
border: 0 solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
filter: drop-shadow(1px 2px 2px gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
filter: drop-shadow(1px 1px 2px gray);
|
||||||
|
translate: 0 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-red {
|
||||||
|
background: red;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-red:hover, .btn.btn-red:active {
|
||||||
|
background: darkred !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-blue {
|
||||||
|
background: slategray;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-blue:hover, .btn.btn-blue:active {
|
||||||
|
background: darkslategray !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-delete:before {
|
||||||
|
content: '\1F5D1';
|
||||||
|
padding: .2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
padding: 1rem;
|
||||||
|
margin: .2rem;
|
||||||
|
|
||||||
|
border: 1px solid lightgray;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
17
src/main/resources/templates/fragments.html
Normal file
17
src/main/resources/templates/fragments.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" th:href="@{/style.css}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div th:fragment="nav">
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a th:href="@{/}">Übersicht</a></li>
|
||||||
|
<li><a th:href="@{/statistics}">Statistiken</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
40
src/main/resources/templates/index.html
Normal file
40
src/main/resources/templates/index.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>ETL-Prozessor</title>
|
||||||
|
<link rel="stylesheet" th:href="@{/style.css}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div th:replace="~{fragments.html :: nav}"></div>
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<h1>Letzte Anfragen</h1>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Patienten-ID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="request : ${requests}">
|
||||||
|
<td th:if="${request.status.value == 'success'}" class="bg-green"><small>[[ ${request.status} ]]</small></td>
|
||||||
|
<td th:if="${request.status.value == 'warning'}" class="bg-yellow"><small>[[ ${request.status} ]]</small></td>
|
||||||
|
<td th:if="${request.status.value == 'error'}" class="bg-red"><small>[[ ${request.status} ]]</small></td>
|
||||||
|
<td th:if="${request.status.value == 'unknown'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
|
||||||
|
<td th:if="${request.status.value == 'duplication'}" class="bg-gray"><small>[[ ${request.status} ]]</small></td>
|
||||||
|
<td>[[ ${request.uuid} ]]</td>
|
||||||
|
<td><time th:datetime="${request.processedAt}">[[ ${request.processedAt} ]]</time></td>
|
||||||
|
<td>[[ ${request.patientId} ]]</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<script th:src="@{/scripts.js}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
24
src/main/resources/templates/statistics.html
Normal file
24
src/main/resources/templates/statistics.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>ETL-Prozessor</title>
|
||||||
|
<link rel="stylesheet" th:href="@{/style.css}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div th:replace="~{fragments.html :: nav}"></div>
|
||||||
|
<main>
|
||||||
|
<h1>Statistiken</h1>
|
||||||
|
|
||||||
|
<div id="piechart" class="chart" style="width: 320px; height: 320px; display: inline-block"></div>
|
||||||
|
<div id="barchart" class="chart" style="width: 720px; height: 320px; display: inline-block"></div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<script th:src="@{/echarts.min.js}"></script>
|
||||||
|
<script th:src="@{/scripts.js}"></script>
|
||||||
|
<script>
|
||||||
|
drawPieChart('statistics/requeststates', 'piechart', 'Statusverteilung der Anfragen');
|
||||||
|
drawBarChart('statistics/requestslastmonth', 'barchart', 'Anfragen des letzten Monats');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user