New version with VueJS

This commit is contained in:
Axel 2021-12-18 20:42:21 +01:00
parent a0fd4aea60
commit 187eae16e1
7 changed files with 289 additions and 188 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
web/vendor/**/*
.env
web/sync.php
/**/node_modules

View file

@ -9,24 +9,28 @@ $dotenv->required(['DB_TYPE', 'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASSWORD']);
class DB {
private $link;
public function __construct() {
if (! is_resource($this->link)) {
$dsn = $_ENV['DB_TYPE'].':dbname='.$_ENV['DB_NAME'].';host='.$_ENV['DB_HOST'].';port='.$_ENV['DB_PORT'];
$this->link = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASSWORD']);
$this->link->query('SET NAMES "UTF8"');
$driver_options = array(
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'",
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
);
$this->link = new PDO($dsn, $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $driver_options);
}
}
public function get_all_contacts($task = null) {
$query = '
SELECT c.id, c.surname, c.firstname, c.email, c.phone, c.creation_date, c.active
FROM contacts c
SELECT c.id, c.surname, c.firstname, c.email, c.phone, c.creation_date, c.active
FROM contacts c
LEFT JOIN notifications as n ON (n.contact_id = c.id)
LEFT JOIN tasks as t ON (n.task_id = t.id)
';
if (! is_null($task)) {
$query .= ' WHERE t.id = '.$task;
}
@ -37,19 +41,24 @@ class DB {
public function get_all_tasks($status = null) {
if (is_null($status)) {
$query = '
SELECT id, host, type, params, creation_date, frequency, last_execution, active
FROM tasks
SELECT DISTINCT t.id, t.host, t.type, t.params, t.frequency, t.creation_date, t.last_execution, t.active, t.group_id, h.status, g.name as group_name
FROM `tasks` as t
LEFT JOIN `tasks_history` as h ON (h.task_id = t.id)
LEFT JOIN `groups` as g ON (g.id = t.group_id)
WHERE (t.last_execution IS NULL OR h.datetime = t.last_execution)
ORDER BY group_id DESC
';
}
else {
$query = '
SELECT DISTINCT t.id, t.host, t.type, t.params, t.creation_date, t.last_execution, t.active
FROM tasks as t
SELECT DISTINCT t.id, t.host, t.type, t.params, t.creation_date, t.last_execution, t.active, t.group_id
FROM tasks as t
JOIN tasks_history as h ON (h.task_id = t.id)
WHERE h.status = '.intval($status).' AND h.datetime = t.last_execution
ORDER BY group_id DESC
';
}
return $this->query($query);
}
@ -57,21 +66,21 @@ class DB {
$args = [
':limit' => $limit
];
$query = '
SELECT id, status, datetime, task_id
SELECT id, status, datetime, task_id
FROM tasks_history
';
if (! is_null($task)) {
$query .= ' WHERE task_id = :task_id';
$args[':task_id'] = $task;
}
$query .= ' ORDER BY datetime DESC LIMIT :limit ';
return $this->query($query, $args);
}
public function get_task_last_status($task) {
$result = $this->query('SELECT t.id, th.status FROM tasks_history th JOIN tasks t ON (th.task_id = t.id) WHERE t.id = :task ORDER BY datetime DESC LIMIT 1', [':task' => $task]);
foreach ($result as $r) {
@ -81,45 +90,58 @@ class DB {
public function query($query, $args = null) {
$result = $this->prepare($query, $args);
if (! $result->execute()) {
throw new DatabaseException($result->errorInfo()[2]. ' in ('.$query.')');
}
return $result->fetchAll();
}
private function prepare($query, $args = null) {
if (! $result = $this->link->prepare($query)) {
throw new DatabaseException('Cannot prepare query ('.$query.') for execution');
}
if (! is_null($args)) {
foreach ($args as $n => $v) {
$type = gettype($v);
switch ($type) {
case 'boolean':
$cast = PDO::PARAM_BOOL;
break;
case 'integer':
$cast = PDO::PARAM_INT;
break;
case 'null':
$cast = PDO::PARAM_NULL;
break;
case 'double':
case 'string':
default:
$cast = PDO::PARAM_STR;
}
$result->bindValue($n, $v, $cast);
}
}
return $result;
}
public function insert($query, $args = null) {
$result = $this->prepare($query, $args);
if (! $result->execute()) {
throw new DatabaseException($result->errorInfo()[2]. ' in ('.$query.')');
}
return $result->fetchAll();
return $this->link->lastInsertId();
}
}

64
web/api.php Normal file
View file

@ -0,0 +1,64 @@
<?php
require_once __DIR__.'/DB.php';
class Api {
private $db;
public function __construct($db) {
$this->db = $db;
}
public function get_tasks() {
$tasks = [];
$ret = $this->db->get_all_tasks();
foreach ($ret as $t) {
if (is_null($t['group_id'])) {
$group_id = $t['id'];
$group_name = 'ungrouped';
}
else {
$group_id = $t['group_id'];
$group_name = $t['group_name'];
}
if (isset($tasks[$group_id])) {
array_push($tasks[$group_id]['tasks'], $t);
}
else {
$tasks[$group_id] = [
'id' => $group_id,
'name' => $group_name,
'tasks' => [ $t ]
];
}
}
return $tasks;
}
}
if (isset($_GET['a'])) {
$action = trim(htmlentities($_GET['a']), '_');
$api = new Api($db);
if (method_exists($api, $action)) {
try {
echo json_encode(call_user_func([$api, $action]));
}
catch (Exception $e) {
echo json_encode([
'result' => false
]);
}
exit;
}
}
header("HTTP/1.1 404 Not Found");

View file

@ -16,9 +16,18 @@ body {
color: #3D3D3D;
}
a, a:visited {
color: inherit;
text-decoration: inherit;
}
a:hover {
text-decoration: underline;
}
h1, h2, h3, h4 {
margin-top: 2rem;
margin-bottom: 1rem;
margin-top: .8rem;
margin-bottom: .8rem;
}
h1 {
@ -28,7 +37,7 @@ h1 {
}
h2 {
font-size: 1.8rem;
font-size: 1.4rem;
margin-top: 0;
padding-top: 0;
}
@ -63,13 +72,54 @@ td {
border: 1px solid #9ccece;
text-align: center;
vertical-align: middle;
}
#tasks_tbl, #contacts_tbl {
width: 100%;
}
.quick-view {
margin: 3rem auto;
max-width: 1000px;
padding: 1rem;
-webkit-box-shadow: 5px 5px 15px 0px rgba(0,0,0,0.3);
-moz-box-shadow: 5px 5px 15px 0px rgba(0,0,0,0.3);
box-shadow: 5px 5px 15px 0px rgba(0,0,0,0.3);
border-radius: 5px;
position: relative;
}
.new-group {
margin: .2rem;
display: inline-block;
border: 1px solid rgba(7, 7, 7, 0.432);
padding: 1px;
}
.quick-view .square {
width: 2rem;
min-width: 2rem;
max-width: 2rem;
height: 2rem;
min-height: 2rem;
max-height: 2rem;
margin: 0.1rem;
text-align: center;
vertical-align: middle;
float: left;
}
.spacer {
clear:both;
line-height: 0;
padding: 0;
margin:0;
}
.task {
margin: 3rem auto;
max-width: 1000px;
@ -124,6 +174,23 @@ td {
background-image: url('../img/collapse.png');
}
.up {
background-color: #c9ecc9;
}
.down {
background-color: #ffc5c5;
}
.unknown {
background-color: rgb(243, 192, 97);
}
.square.unknown img, .square.down img, .square.up img {
opacity: .5;
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);

BIN
web/img/unknown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -5,144 +5,115 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>MonitoLite - Network monitoring tool</title>
<script
src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="text/javascript" src="js/scripts.js"></script>
<link type="text/css" rel="stylesheet" href="css/styles.css" />
</head>
<body>
<h1>MonitoLite Dashboard</h1>
<?php if ($tasks = $db->get_all_tasks()): ?>
<?php foreach ($tasks as $task): ?>
<div class="task">
<p class="exp-icon" title="Click here to expand/collapse the task">&nbsp;</p>
<!--<p class="task-overlay"><img src="img/expand.png" width="32"></p>-->
<h2>Task <small>#</small><?php echo $task['id']; ?> » <span class="highlight"><?php echo $task['type']; ?></span> for host <span class="highlight"><?php echo $task['host']; ?></span></h2>
<div id="app">
<h1>MonitoLite Dashboard</h1>
<table id="tasks_tbl">
<thead>
<tr>
<th width="5%">Up?</th>
<th width="*">Host</th>
<th width="5%">Type</th>
<th width="10%">Parameters</th>
<th width="20%">Last execution</th>
<th width="10%">Frequency (min)</th>
<th width="5%">Active</th$query>
</tr>
</thead>
<tbody>
<?php
$status = $db->get_task_last_status($task['id']);
$color = $status == 1 ? '#c9ecc9' : '#ffc5c5';
$icon = $status == 1 ? 'up.png': 'down.png';
?>
<tr>
<td style="background-color: <?php echo $color; ?>"><img src="img/<?php echo $icon; ?>" width="16" alt="Status" /></td>
<td style="background-color: <?php echo $color; ?>"><?php echo $task['host']; ?></td>
<td>
<?php if ($task['type'] == 'http'): ?>
<img src="img/http.png" width="16" alt="Warning" title="Type: <?php echo $task['type']; ?>"/>
<?php elseif ($task['type'] == 'ping'): ?>
<img src="img/ping.png" width="16" alt="Warning" title="Type: <?php echo $task['type']; ?>"/>
<?php endif; ?>
</td>
<td><?php echo $task['params']; ?></td>
<td><?php echo $task['last_execution']; ?></td>
<td><?php echo ($task['frequency'] / 60); ?></td>
<td><?php echo ($task['active'] == 1) ? 'Yes' : 'No'; ?></td>
</tr>
</tbody>
</table>
<div class="hidden">
<div id="history">
<h3>Task history</h3>
<?php if ($histories = $db->get_all_history($task['id'], 5)): ?>
<table id="history_tbl">
<thead>
<tr>
<th>Datetime</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($histories as $history): ?>
<?php
$color = ($history['status'] == 1) ? '#c9ecc9' : '#ffc5c5';
?>
<tr>
<td width="20%" align="center"><?php echo $history['datetime']; ?></td>
<?php if ($history['status'] == 1): ?>
<td width="20%" align="center" style="background-color:#c9ecc9;">
<img src="img/success.png" width="16" alt="Success">&nbsp;SUCCESS
</td>
<?php else: ?>
<td width="20%" align="center" style="background-color:#ffc5c5;">
<img src="img/error.png" width="16" alt="Success">&nbsp;ERROR
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p><small>Only the 5 latest entries are displayed</small></p>
<?php else: ?>
<p class="no_result">No history found here</p>
<?php endif; ?>
</div>
<div id="contacts">
<h3>Task contacts</h3>
<?php if ($contacts = $db->get_all_contacts($task['id'])): ?>
<table id="contacts_tbl">
<thead>
<tr>
<th>Surname</th>
<th>Firstname</th>
<th>Email</th>
<th>Phone</th>
<th>Creation date</th>
<th>Active</th>
</tr>
</thead>
<tbody>
<?php foreach ($contacts as $contact): ?>
<tr>
<td width="15%"><?php echo $contact['surname']; ?></td>
<td width="15%"><?php echo $contact['firstname']; ?></td>
<td width="20%"><?php echo $contact['email']; ?></td>
<td width="15%"><?php echo $contact['phone']; ?></td>
<td width="15%"><?php echo $contact['creation_date']; ?></td>
<td width="15%"><?php echo ($contact['active'] == 1) ? 'Yes' : 'No'; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="no_result">
<img src="img/warning.png" width="16" alt="Warning"/>
No contact found here. That means that nobody will get any notification in case of an error.
</p>
<?php endif; ?>
</div>
</div>
<div class="quick-view">
<h3>Quick overview</h3>
<div
v-for="group in tasks"
v-bind:key="group.id"
class="new-group"
:title="group.name"
>
<a
v-for="task in group.tasks"
v-bind:key="task.id"
:href="'#task-'+task.id"
>
<p :class="statusText(task.status)" class="square">
<img :src="'/img/'+statusText(task.status)+'.png'" width="16" alt="">
</p>
</a>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="no_result">No task found here</p>
<?php endif; ?>
<p class="spacer">&nbsp;</p>
</div>
<div
v-for="group in tasks"
v-bind:key="group.group_id"
class="task"
>
<h3>Tasks for group <span class="highlight">{{ group.name }}</span> <small>(#{{ group.id }})</small> </h3>
<table id="tasks_tbl">
<thead>
<tr>
<th width="5%">Up?</th>
<th width="*">Host</th>
<th width="5%">Type</th>
<th width="20%">Last execution</th>
<th width="20%">Frequency (min)</th>
<th width="5%">Active</th$query>
</tr>
</thead>
<tbody>
<tr
v-for="task in group.tasks"
v-bind:key="task.id"
>
<td :class="statusText(task.status)">
<img :src="'img/'+statusText(task.status)+'.png'" width="16" alt="Status" />
<a :name="'task-'+task.id"></a>
</td>
<td :class="statusText(task.status)">
<a :href="task.host" target="_blank">{{ task.host }}</a>
</td>
<td>
<img :src="task.type == 'http' ? 'img/http.png' : 'img/ping.png'" width="16" alt="Type of check" :title="'Type: '+task.type" />
</td>
<td>{{ task.last_execution ?? 'never' }}</td>
<td>{{ task.frequency }}</td>
<td>{{ task.active == 1 ? 'Yes' : 'No' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<script>
var vm = new Vue({
el: '#app',
props: [
'refresh'
],
data: {
tasks: []
},
methods: {
// a computed getter
statusText: function (status) {
switch (status) {
case '1':
return 'up';
break;
case '0':
return 'down';
break;
default:
return 'unknown';
}
},
getTasks: function() {
axios.get('api.php?a=get_tasks')
.then(response => this.tasks = response.data)
.catch(error => window.alert('Cannot get tasks'))
}
},
mounted: function() {
this.getTasks()
this.refresh = window.setInterval(() => {
this.getTasks();
}, 60000)
}
})
</script>
</body>
</html>

View file

@ -1,25 +0,0 @@
$(document).ready(function() {
$('.task .exp-icon').on('click', function() {
var el = $(this).parent('.task');
if (el.hasClass('active')) {
el.removeClass('active');
el.children('.hidden').slideUp();
}
else {
$('.task').removeClass('active');
$('.task').children('.hidden').slideUp();
el.addClass('active');
el.children('.hidden').slideDown();
}
});
$('.task').not('.active').on('mouseover', function() {
$(this).children('.task-overlay').show();
});
$('.task').not('.active').on('mouseout', function() {
$(this).children('.task-overlay').hide();
});
})