diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce013a1 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Test work + +1. `docker-compose build` +2. `docker-compose run --rm app php src/baseInit.php` +3. `docker-compose up` \ No newline at end of file diff --git a/docker/php-fpm.docker b/docker/php-fpm.docker index 2af8053..88b3471 100644 --- a/docker/php-fpm.docker +++ b/docker/php-fpm.docker @@ -5,3 +5,5 @@ RUN set -ex \ && docker-php-ext-install pdo pdo_mysql gd zip WORKDIR /app + +RUN chmod u+s /bin/ping \ No newline at end of file diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 84237ce..0117a77 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -216,4 +216,177 @@ a.btn, a.btn:hover, a.btn:focus { select { height: 1.5rem; padding-left: 0 !important; -} \ No newline at end of file +} + +.servers-tree * { + list-style-type: none; +} + +.servers-tree { + margin: 0; + padding: 0; +} + +.servers-tree-group { + cursor: pointer; + user-select: none; +} + +.servers-tree-group::before { + content: " "; + display: inline-block; + margin-right: 6px; + border-top: .375rem solid transparent; + border-bottom: .375rem solid transparent; + + border-left: .375rem solid black; +} + +.servers-tree-group--down::before { + transform: rotate(90deg); +} + +.servers-tree-group__list { + display: none; +} + +.servers-tree-group__list--active { + display: block; +} + +.modal { + display: none; +} + +.modal--active { + display: block; +} + +.modal__bg { + height: 100%; + width: 100%; + background: rgba(0, 0, 0, .3); + position: fixed; + top: 0; + left: 0; + z-index: 1; +} + +.modal__window { + width: 300px; + position: fixed; + top: calc(50% - 96px); + left: calc(50vw - 150px); + height: 190px; + z-index: 2; +} + +.modal-window--spiner::before { + content: ""; + display: block; + color: black; + font-size: .25rem; + margin: 0; + width: 1em; + height: 1em; + border-radius: 50%; + position: absolute; + animation: wloadingkf 1.3s infinite linear; + transform: translate3d(0, -50%, 0); + font-family: monospace; + left: 50%; + top: 50%; + z-index: -1; +} + +.modal-window--spiner .modal-window__body { + display: none; +} + +.modal-window { + border: 1px solid #666; + width: 100%; + position: relative; + height: 100%; + background: #eee; + z-index: 2; + border-radius: .325rem; + box-shadow: 0 .125rem .25rem #666; +} + +.modal-window__title { + text-align: center; +} + +.modal-window__close { + position: absolute; + right: 0; + top: .125rem; + width: 1rem; + height: 1rem; + opacity: 0.3; + cursor: pointer; +} + +.modal-window__close:hover { + opacity: 1; +} + +.modal-window__close:before, +.modal-window__close:after { + position: absolute; + left: .25rem; + content: ' '; + height: 1.0625rem; + width: .125rem; + background-color: #333; +} + +.modal-window__close:before { + transform: rotate(45deg); +} + +.modal-window__close:after { + transform: rotate(-45deg); +} + +@keyframes wloadingkf { + 0%, + 100% { + box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0; + } + 12.5% { + box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em; + } + 25% { + box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em; + } + 37.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 50% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em; + } + 62.5% { + box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em; + } + 75% { + box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0; + } + 87.5% { + box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em; + } +} + +.modal-window-body { + padding: 1.5rem 0; +} + +.modal-window-body__label { + font-weight: 700; + padding: 0 .5rem; + font-size: 85%; + min-width: 7.5rem; + display: inline-block; +} + diff --git a/public/assets/js/script.js b/public/assets/js/script.js index dec5348..0985e25 100644 --- a/public/assets/js/script.js +++ b/public/assets/js/script.js @@ -22,11 +22,55 @@ d.querySelectorAll(".js-server-btn-delete").forEach(v => { deleteEvent(v, "You're sure to delete this server?", "/servers/delete") }); -d.querySelector(".js-server-form-on-submit").addEventListener("submit", e=>{ - let check_ip = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - let ip = e.target.ip; - if (!check_ip.exec(ip.value)){ - e.preventDefault() - ip.focus() - } +let serverFormOnSubmit; +serverFormOnSubmit = d.querySelector(".js-server-form-on-submit"); +if (serverFormOnSubmit) { + serverFormOnSubmit.addEventListener("submit", e => { + let check_ip = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + let ip = e.target.ip; + if (!check_ip.exec(ip.value)) { + e.preventDefault(); + ip.focus() + } + }); +} + +d.querySelectorAll(".js-servers-tree-group").forEach(v => { + v.addEventListener("click", e => { + let parent = e.target.parentElement.querySelector(".js-servers-tree-group-list"); + if (parent) { + parent.classList.toggle("servers-tree-group__list--active"); + } + e.target.classList.toggle("servers-tree-group--down"); + + }) +}); + +d.querySelectorAll(".js-modal-close").forEach(v => { + v.addEventListener("click", () => { + d.querySelector(".modal").classList.remove("modal--active") + }) +}); + +d.querySelectorAll(".js-ping").forEach(v => { + v.addEventListener("click", e => { + d.querySelector(".modal").classList.add("modal--active"); + let mwClass = d.querySelector(".modal-window").classList; + mwClass.add("modal-window--spiner"); + + let xhr = new XMLHttpRequest(); + xhr.open("POST", "/ping/" + e.target.dataset.id); + xhr.send(); + xhr.onload = () => { + let resp = JSON.parse(xhr.response); + + if (resp.status === 'ok') { + ['name', 'ip', 'lost', 'transmitted', 'received'].map(i => { + d.querySelector('.js-ping-result-' + i).innerHTML = resp[i] + }) + } + mwClass.remove("modal-window--spiner"); + + }; + }) }); \ No newline at end of file diff --git a/src/controllers/DefaultController.php b/src/controllers/DefaultController.php index a40d6c6..8399700 100644 --- a/src/controllers/DefaultController.php +++ b/src/controllers/DefaultController.php @@ -4,6 +4,7 @@ namespace MyApp\Controller; use MyApp\Core\Controller; use MyApp\View\DefaultView as View; +use MyApp\Service\DefaultService; class DefaultController extends Controller { @@ -12,6 +13,6 @@ class DefaultController extends Controller */ public function index() { - $this->htmlResponse((new View())->index()); + $this->htmlResponse((new View())->index((new DefaultService())->getTree())); } } diff --git a/src/controllers/PingController.php b/src/controllers/PingController.php new file mode 100644 index 0000000..fc28d83 --- /dev/null +++ b/src/controllers/PingController.php @@ -0,0 +1,44 @@ +\d+)$|" + * @HttpMethod = "GET" + * + * @param $id + */ + public function history($id) + { + if ($model = (new PingService())->getTableData((int) $id)) { + $servers = ServersModel::find([(int) $id]); + $server_name = $servers ? $servers[0]->getName() : ''; + $this->htmlResponse((new View())->history($model, $server_name)); + } else { + $this->redirect('/'); + } + } + + /** + * @RouteRegExp = "|^/ping/(?\d+)$|" + * @HttpMethod = "POST" + * + * @param $server_id + */ + public function ping($server_id) + { + $out = (new PingService())->ping($server_id); + if ($out) { + $this->jsonResponse((object) array_merge(['status' => 'ok'], $out)); + } else { + $this->jsonResponse((object) ['status' => 'error']); + } + } +} diff --git a/src/models/TallyLogsModel.php b/src/models/TallyLogsModel.php index dc74242..59a9e5d 100644 --- a/src/models/TallyLogsModel.php +++ b/src/models/TallyLogsModel.php @@ -65,9 +65,9 @@ class TallyLogsModel extends Model } /** - * @return int + * @return string */ - public function getTime(): int + public function getTime(): string { return $this->time; } @@ -79,7 +79,7 @@ class TallyLogsModel extends Model */ public function setTime(DateTime $time): self { - $this->time = $time->getTimestamp(); + $this->time = $time->format('Y-m-d H:i:s'); return $this; } @@ -143,4 +143,16 @@ class TallyLogsModel extends Model return $this; } + + /** + * @param ServersModel $server + * + * @return TallyLogsModel + */ + public function setServer(ServersModel $server): self + { + $this->server = $server->getId(); + + return $this; + } } diff --git a/src/services/DefaultService.php b/src/services/DefaultService.php new file mode 100644 index 0000000..21798cf --- /dev/null +++ b/src/services/DefaultService.php @@ -0,0 +1,61 @@ +getParent() === $parent_id) { + $out_item = [ + 'name' => $item->getName(), + 'type' => 'group', + 'items' => $this->getGroupsArray($models, $servers_arr, + $item->getId()), + ]; + if (array_key_exists($item->getId(), $servers_arr)) { + $out_item['items'] = array_merge($out_item['items'], + $servers_arr[$item->getId()]); + } + + $out[] = $out_item; + } + } + } + + return $out; + } + + public function getTree() + { + $groups = (new GroupsService())->getTableData(); + $servers = (new ServersService())->getTableData(); + $servers_arr = []; + if ($servers) { + foreach ($servers as $server) { + $servers_arr[$server->getGroup()][] = [ + 'name' => $server->getName(), + 'type' => 'server', + 'id' => $server->getId(), + ]; + } + } + + $out = []; + + $out = $this->getGroupsArray($groups, $servers_arr); + if (array_key_exists(null, $servers_arr)) { + $out = array_merge($out, $servers_arr[null]); + } + + return $out; + } +} diff --git a/src/services/GroupsService.php b/src/services/GroupsService.php index 6726f9b..d160245 100644 --- a/src/services/GroupsService.php +++ b/src/services/GroupsService.php @@ -35,14 +35,17 @@ class GroupsService public function sortForSelect($models, $parent_id = null, $level = 1) { $out = []; - foreach ($models as $item) { - /** GroupsModel $item */ - if ( - $item->getParent() === $parent_id) { - $item->{'select-level'} = str_repeat('-', $level); - $out[] = $item; - $out = array_merge($out, - $this->sortForSelect($models, $item->getId(), $level + 1)); + if ($models) { + foreach ($models as $item) { + /** GroupsModel $item */ + if ( + $item->getParent() === $parent_id) { + $item->{'select-level'} = str_repeat('-', $level); + $out[] = $item; + $out = array_merge($out, + $this->sortForSelect($models, $item->getId(), + $level + 1)); + } } } diff --git a/src/services/PingService.php b/src/services/PingService.php index 5c3f78e..142141b 100644 --- a/src/services/PingService.php +++ b/src/services/PingService.php @@ -9,26 +9,39 @@ use DateTime; class PingService { - public function ping($server_id): ? TallyLogsModel + public function getTableData(int $server_id): ?array { - $server = ServersModel::find([$server_id],null,null,[],'','1'); + $models = TallyLogsModel::find(null, null, '{server} = :server_id', + ['server_id' => $server_id], 'id DESC'); - if (!empty($server)){ + return $models; + } + + public function ping($server_id): ?array + { + $server = ServersModel::find([$server_id], null, null, [], '', '1'); + + if (!empty($server)) { $ip = $server[0]->getIp(); exec("ping -c 10 $ip", $output, $status); foreach ($output as $line) { if (preg_match('/(?\d+)[\w\ ]*,\ (?\d+)[\w\ ]*, (?\d+)%/', $line, $m)) { - $log = new TallyLogsModel(); - - $log->setTime(new DateTime()) - ->setLost($m['lost']) - ->setTransmitted($m['transmitted']) - ->setReceived($m['received']); + $log = (new TallyLogsModel())->setTime(new DateTime()) + ->setLost((int) $m['lost']) + ->setTransmitted((int) $m['transmitted']) + ->setReceived((int) $m['received']) + ->setServer($server[0]); Model::save([$log]); - return $log; + return [ + 'name' => $server[0]->getName(), + 'ip' => $server[0]->getIp(), + 'lost' => $m['lost'], + 'transmitted' => $m['transmitted'], + 'received' => $m['received'], + ]; } } } diff --git a/src/services/ServersService.php b/src/services/ServersService.php index eb9eda4..e359c7b 100644 --- a/src/services/ServersService.php +++ b/src/services/ServersService.php @@ -18,12 +18,15 @@ class ServersService { $models = ServersModel::find(null, null, '1 = 1'); $records = []; - foreach ($models as $model) { - /** @var ServersModel $model */ - if ($group_id = $model->getGroup()) { - $records[] = $group_id; + if ($models) { + foreach ($models as $model) { + /** @var ServersModel $model */ + if ($group_id = $model->getGroup()) { + $records[] = $group_id; + } } } + if (count($records)) { if ($groups = GroupsModel::find($records)) { $groups_names = []; diff --git a/src/templates/default/index.tpl.php b/src/templates/default/index.tpl.php index a119214..1f85f74 100644 --- a/src/templates/default/index.tpl.php +++ b/src/templates/default/index.tpl.php @@ -1,5 +1,70 @@ +require __DIR__.'/../shared/head.php'; ?> +tree)): ?> +
    + '; + if ('group' === $item['type']) { + ?> + + +
      + +
    + + +
    ping +
    + ping history + '; + } + } + })($this->tree); + ?> +
+ + +table): ?> + + + + + + + + table as $item): ?> + + + + + + + + +
TimeTransmittedReceivedLost
+ getTime(); ?> + + getTransmitted(); ?> + + getReceived(); ?> + + getLost(); ?>% +
+ +{'tree'} = $tree; + return $this->setTitle('Home')->render('default/index'); } } diff --git a/src/views/PingView.php b/src/views/PingView.php new file mode 100644 index 0000000..c146876 --- /dev/null +++ b/src/views/PingView.php @@ -0,0 +1,15 @@ +{'table'} = $table; + + return $this->setTitle('History of ping to '.trim($server_name))->render('ping/index'); + } +}