Ping App

Code


├── Dockerfile
├── api
│   └── v1
│       ├── index.php
│       └── util.php
├── docker-compose.yml
└── public
    ├── css
    │   └── master.css
    ├── img
    │   └── spinner.gif
    ├── index.html
    └── js
        └── main.js

docker-compose.yml:

version: "3"

services:
  web:
    container_name: web
    image: ifpb/php:7.3-apache-ping
    build: .
    ports:
      - 8080:80
    volumes:
      - ./:/var/www/html/

Dockerfile:

FROM php:7.3-apache

RUN apt -y update && apt install -y inetutils-ping

Back-end side


Ping API

└── api
    └── v1
        ├── index.php
        └── util.php

Front-end side


└── public
    ├── css
    │   └── master.css
    ├── img
    │   └── spinner.gif
    ├── index.html
    └── js
        └── main.js

public/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css" integrity="sha384-Smlep5jCw/wG7hdkwQ/Z5nLIefveQRIY9nfy6xoR1uRYBtpZgI6339F5dgvm/e9B" crossorigin="anonymous">
  <link rel="stylesheet" href="css/master.css">
</head>
<body>
  <header>
    <h1>Host Monitor</h1>
  </header>
  <main class="container">
    <div class="row mb-5 justify-content-center">
      <section class="tootlbar">
        <input type="text" id="address" placeholder="host" autofocus>
        <input type="text" id="count" placeholder="count" size="5">
        <button id="pingButton">ping</button>
        <img src="img/spinner.gif" alt="spinner" id="spinner" class="invisible">
      </section>
    </div>
    <div class="row">
      <div class="col-6">
        <div class="card" id="ping-history">
          <div class="card-header text-center">
            <h2>History</h2>
          </div>
          <div class="card-body">
            <table id="pingTable" class="table table-hover">
              <thead>
                <tr>
                  <th>Host</th>
                  <th>Tx</th>
                  <th>Rx</th>
                  <th>Losted</th>
                  <th>Time (ms)</th>
                </tr>
              </thead>
              <tbody></tbody>
            </table>
          </div>
        </div>
      </div>
      <div class="col-6">
        <div class="card">
          <div class="card-header text-center">
            <h2>Stats</h2>
          </div>
          <div class="card-body">
            <h3 id="addressStatus" class="text-center"></h3>
            <div class="row">
              <div class="col-4">
                <div id="stats-info" class="text-center">
                  <h4>Received</h4>
                  <span id="transmittedStatus"></span>
                  <canvas id="stats-info-chart"></canvas>
                </div>
              </div>
              <div class="col-8">
                <div id="time-packets">
                  <h4 class="text-center">Packet Time</h4>
                  <canvas id="time-packets-chart"></canvas>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </main>
  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/js/bootstrap.min.js" integrity="sha384-o+RDsa0aLu++PJvFqy8fFScvbHFLtbvScb8AjopnFD+iEQ7wo/CG0xlczd+2O/em" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.0.0/tablesort.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/tablesort/5.0.0/src/sorts/tablesort.number.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

public/js/main.js:

const addressField = document.querySelector("#address");
const countField = document.querySelector("#count");
const pingButton = document.querySelector("#pingButton");
const spinner = document.querySelector("#spinner");

const pingTable = document.querySelector("#pingTable");
const pingTableContent = document.querySelector("#pingTable tbody");

const addressStatus = document.querySelector("#addressStatus");
const transmittedStatus = document.querySelector("#transmittedStatus");

// Events
pingButton.onclick = () => getPing(addressField.value, countField.value);
document.onkeydown = e => {
  if (e.key === "Enter") getPing(addressField.value, countField.value);
};

// Data
let statusValues = {};

function updateStatusValues(ping) {
  let address = ping.address;
  if (statusValues[address]) {
    statusValues[address].transmitted += ping.transmitted;
    statusValues[address].losted += ping.losted;
    statusValues[address].received += ping.received;
    statusValues[address].times.concat(ping.times);
  } else {
    statusValues[address] = {
      transmitted: ping.transmitted,
      losted: ping.losted,
      received: ping.received,
      times: ping.times
    };
  }
}

// Ping
function getPing(addressValue, countValue) {
  spinner.classList.remove("invisible");
  let url = `../api/v1/?host=${addressValue}&count=${countValue}`;
  fetch(url)
    .then(res => res.json())
    .then(json => addPing(json));
}

function addPing(pingResponse) {
  if (pingResponse.error) {
    alert("Erro na obtenção do Ping!");
  } else {
    const ping = pingParser(pingResponse);
    updateStatusValues(ping);
    loadPingHistory(ping);
    loadStatssInfo(ping.address);
    loadTimePackets(ping.times);
  }
  spinner.classList.add("invisible");
  addressField.focus();
  addressField.value = "";
  countField.value = "";
}

function pingParser(pingResponse) {
  const address = pingResponse.host;
  const transmitted = parseInt(pingResponse.statistics.transmitted);
  const received = parseInt(pingResponse.statistics.received);
  const losted = transmitted - received;
  const totalTimes = pingResponse.packets
    .reduce((t, p) => parseFloat(p.time) + t, 0)
    .toFixed(1);
  const times = pingResponse.packets.map(p => p.time);
  return { address, transmitted, received, losted, totalTimes, times };
}

// Ping History
function loadPingHistory(ping) {
  const values = [
    ping.address,
    ping.transmitted,
    ping.received,
    ping.losted,
    ping.totalTimes
  ];
  pingTableContent.insertAdjacentHTML(
    "afterbegin",
    `<tr data-address="${ping.address}"><td>${values.join(
      "</td><td>"
    )}</td></tr>`
  );
  pingTableContent.firstChild.onclick = () => {
    const row = event.target.parentNode;
    const address = row.dataset.address;
    loadStatssInfo(address);
    loadTimePackets(statusValues[address].times);
    Array.from(pingTableContent.childNodes).map(r =>
      r.classList.remove("selected")
    );
    row.classList.add("selected");
  };
  let sort = new Tablesort(pingTable);
}

// Stats Info
let statsInfoChart;
const statsInfoChartCanvas = document.querySelector("#stats-info-chart");

function createTransmittedChart(transmitted) {
  const data = {
    labels: ["Losted", "Received"],
    datasets: [
      {
        data: transmitted,
        backgroundColor: ["#FF6384", "#36A2EB"],
        hoverBackgroundColor: ["#FF6384", "#36A2EB"]
      }
    ]
  };
  const options = {
    legend: {
      display: false
    }
  };
  const chart = new Chart(statsInfoChartCanvas, {
    type: "doughnut",
    data: data,
    options: options
  });
  return chart;
}

function loadStatssInfo(address) {
  const transmitted = statusValues[address].transmitted;
  const losted = statusValues[address].losted;
  const received = statusValues[address].received;
  const transmittedPercentage = [
    (losted / transmitted) * 100,
    (received / transmitted) * 100
  ];
  addressStatus.innerHTML = address;
  transmittedStatus.innerHTML = `${transmitted} (<span id="lostedStatus">${losted}</span> / <span id="receivedStatus">${received}</span>)`;

  if (statsInfoChart) {
    statsInfoChart.data.datasets[0].data = transmittedPercentage;
    statsInfoChart.update();
  } else {
    statsInfoChart = createTransmittedChart(transmittedPercentage);
  }
}

// Time Packets
let timePacketsChart;
const timePacketsChartCanvas = document.querySelector("#time-packets-chart");

function createTimePacketsChart(times) {
  const data = {
    labels: Object.keys(times).map(t => +t + 1),
    datasets: [{ data: times }]
  };
  const options = {
    legend: {
      display: false
    }
  };
  const chart = new Chart(timePacketsChartCanvas, {
    type: "line",
    data: data,
    options: options
  });
  return chart;
}

function loadTimePackets(times) {
  if (timePacketsChart) {
    timePacketsChart.data.labels = Object.keys(times).map(t => +t + 1);
    timePacketsChart.data.datasets[0].data = times;
    timePacketsChart.update();
  } else {
    timePacketsChart = createTimePacketsChart(times);
  }
}

public/js/master.css:

body {
  margin: 0;
  font-family: Arial, Helvetica, sans-serif;
}

header {
  padding: 0.2rem 0;
  text-align: center;
  background: #fafafa;
  border-bottom: 1px solid #ccc;
  margin-bottom: 3rem;
}

#lostedStatus {
  color: #ff6384;
}

#receivedStatus {
  color: #36a2eb;
}

tr:hover {
  cursor: pointer;
}

tr.selected {
  background-color: rgba(0, 0, 0, 0.075);
}

public/img/spinner.gif: