├── services ├── __init__.py ├── impl │ ├── __init__.py │ ├── mockExecutorService.py │ └── pingExecutorService.py ├── notificationService.py ├── executorService.py ├── discoveryService.py ├── deviceRegistryService.py └── discoveryWorker.py ├── run.sh ├── requirements.txt ├── wsgi.py ├── Dockerfile ├── resources ├── template_device.json └── web │ ├── index.html │ └── static │ └── css │ └── main.css ├── constants.py ├── docker-compose.yaml ├── mockDataGenerator.py ├── .gitignore └── app.py /services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/impl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | exec gunicorn --bind=0.0.0.0:8080 --workers=1 wsgi:app 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2==2.11.2 2 | requests==2.23.0 3 | Flask==1.1.2 4 | gunicorn==20.0.4 -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | if __name__ == "__main__": 4 | app.run(host="0.0.0.0", port=8080, debug=True) 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.3-onbuild 2 | 3 | EXPOSE 8080 4 | 5 | RUN chmod a+x ./run.sh 6 | 7 | ENTRYPOINT ["./run.sh"] 8 | -------------------------------------------------------------------------------- /resources/template_device.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template_device", 3 | "ip": "192.168.1.1", 4 | "notification": "disable", 5 | "monitoring": "enable", 6 | "index": 1 7 | } -------------------------------------------------------------------------------- /services/impl/mockExecutorService.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class MockExecutorService(object): 5 | 6 | @staticmethod 7 | def exec_ping(ip_address, count=None, timeout=None): 8 | return random.choice([True, False]) 9 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 5 | RESOURCES_DIR = os.path.join(BASE_DIR, "resources") 6 | WEB_DIR = os.path.join(RESOURCES_DIR, "web") 7 | LOG_DIR = os.path.join(BASE_DIR, "log") 8 | 9 | OFFLINE_STATE = "offline" 10 | ONLINE_STATE = "online" 11 | 12 | DISCOVERY_PERIOD_SEC = int(os.environ.get("DISCOVERY_PERIOD_SEC", default=30)) 13 | -------------------------------------------------------------------------------- /services/impl/pingExecutorService.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | 5 | class PingExecutorService(object): 6 | _log = None 7 | 8 | def __init__(self): 9 | self._log = logging.getLogger(__name__) 10 | 11 | def exec_ping(self, ip_address, count=1, timeout=5): 12 | response = os.system("ping -c %d -W %d %s" % (count, timeout, ip_address)) 13 | self._log.debug("Device %s. Response %s" % (ip_address, str(response))) 14 | return response == 0 15 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | device-monitoring: 5 | image: artemprojects/device-monitoring 6 | volumes: 7 | - "./devices:/usr/src/app/resources/devices" 8 | restart: always 9 | ports: 10 | - 127.0.0.1:8080:8080 11 | environment: 12 | - DEMO_MODE=disable 13 | - MOCK_DATA_COUNT=20 14 | - DISCOVERY_WORKER_POOL=20 15 | - LOG_MODE=dev 16 | - TZ=Europe/Moscow 17 | - DISCOVERY_PERIOD_SEC=30 18 | - EXECUTOR_SERVICE_IMPL=PingExecutorService 19 | -------------------------------------------------------------------------------- /services/notificationService.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class NotificationService(object): 5 | _instance = None 6 | _log = None 7 | 8 | @classmethod 9 | def get_instance(cls, *args, **kwargs): 10 | if cls._instance is None: 11 | cls._instance = super().__new__(cls, *args, **kwargs) 12 | cls.__init__(cls._instance, *args, **kwargs) 13 | return cls._instance 14 | 15 | def __init__(self): 16 | self._log = logging.getLogger(__name__) 17 | 18 | def notify(self, msg): 19 | self._log.info(msg) 20 | -------------------------------------------------------------------------------- /services/executorService.py: -------------------------------------------------------------------------------- 1 | import os 2 | from services.impl.pingExecutorService import PingExecutorService 3 | from services.impl.mockExecutorService import MockExecutorService 4 | 5 | IMPL = os.environ.get("EXECUTOR_SERVICE_IMPL", "PingExecutorService") 6 | 7 | 8 | class ExecutorService(object): 9 | 10 | @classmethod 11 | def get_instance(cls): 12 | if os.environ.get("DEMO_MODE", "disable") == "enable": 13 | return MockExecutorService() 14 | if IMPL == "PingExecutorService": 15 | return PingExecutorService() 16 | if IMPL == "MockExecutorService": 17 | return MockExecutorService() 18 | -------------------------------------------------------------------------------- /services/discoveryService.py: -------------------------------------------------------------------------------- 1 | import os 2 | from queue import Queue 3 | from services.discoveryWorker import DiscoveryWorker 4 | from services.deviceRegistryService import DeviceRegistryService 5 | 6 | 7 | class DiscoveryService(object): 8 | _instance = None 9 | _deviceRegistryService = None 10 | _discoveryWorkerPool = None 11 | 12 | @classmethod 13 | def get_instance(cls, *args, **kwargs): 14 | if cls._instance is None: 15 | cls._instance = super().__new__(cls, *args, **kwargs) 16 | cls.__init__(cls._instance, *args, **kwargs) 17 | return cls._instance 18 | 19 | def __init__(self): 20 | self._deviceRegistryService = DeviceRegistryService.get_instance() 21 | self._discoveryWorkerPool = int(os.environ.get("DISCOVERY_WORKER_POOL", 10)) 22 | 23 | def discover_devices(self): 24 | queue = Queue() 25 | devices = self._deviceRegistryService.get_all_devices() 26 | 27 | for _ in range(len(devices) if len(devices) < self._discoveryWorkerPool else self._discoveryWorkerPool): 28 | t = DiscoveryWorker(queue) 29 | t.setDaemon(True) 30 | t.start() 31 | 32 | for device in devices: 33 | if device.get("monitoring", "enable") == "enable": 34 | queue.put(device) 35 | 36 | queue.join() 37 | -------------------------------------------------------------------------------- /mockDataGenerator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import random 4 | import logging 5 | import shutil 6 | from constants import RESOURCES_DIR 7 | 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | DEVICES_DIR = os.path.join(RESOURCES_DIR, "devices") 12 | MOCK_DATA_COUNT = int(os.environ.get("MOCK_DATA_COUNT", 30)) 13 | 14 | 15 | def clean_data(): 16 | log.debug("Cleaning devices dir...") 17 | try: 18 | shutil.rmtree(DEVICES_DIR) 19 | except Exception as e: 20 | log.warning(str(e)) 21 | 22 | 23 | def create_environment(): 24 | if not os.path.exists(DEVICES_DIR): 25 | log.debug("Creating %s" % DEVICES_DIR) 26 | os.makedirs(DEVICES_DIR) 27 | 28 | 29 | def get_random_names(): 30 | log.debug("Generating device names...") 31 | titles = set() 32 | while len(titles) < MOCK_DATA_COUNT: 33 | titles.add("Mock Device " + str(int(MOCK_DATA_COUNT * 1000 * random.random()))) 34 | return list(titles) 35 | 36 | 37 | def get_random_private_ip(): 38 | private_ip = [str(random.choice([172, 192, 10]))] 39 | for _ in range(3): 40 | private_ip.append(str(random.randint(1, 254))) 41 | return ".".join(private_ip) 42 | 43 | 44 | def generate_mock_data(): 45 | clean_data() 46 | create_environment() 47 | for word in get_random_names(): 48 | monitoring_state = random.choice(["enable", "disable"]) 49 | with open(os.path.join(DEVICES_DIR, word + ".json"), "w") as f: 50 | f.write(json.dumps(dict(ip=get_random_private_ip(), name=word, monitoring=monitoring_state), indent=4)) 51 | -------------------------------------------------------------------------------- /services/deviceRegistryService.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import logging 5 | 6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | RESOURCES_DIR = os.path.join(BASE_DIR, "resources") 8 | 9 | 10 | class DeviceRegistryService(object): 11 | _instance = None 12 | _dev_dir = None 13 | _log = None 14 | 15 | @classmethod 16 | def get_instance(cls, *args, **kwargs): 17 | if cls._instance is None: 18 | cls._instance = super().__new__(cls, *args, **kwargs) 19 | cls.__init__(cls._instance, *args, **kwargs) 20 | return cls._instance 21 | 22 | def _make_folders(self): 23 | if not os.path.exists(self._dev_dir): 24 | os.makedirs(self._dev_dir) 25 | 26 | def __init__(self): 27 | self._log = logging.getLogger(__name__) 28 | self._dev_dir = os.path.join(RESOURCES_DIR, "devices") 29 | # self._make_folders() 30 | 31 | def get_all_devices(self): 32 | devices = [] 33 | for dev in [dev for dev in os.listdir(self._dev_dir) if dev.endswith(".json")]: 34 | try: 35 | with open(os.path.join(self._dev_dir, dev)) as device: 36 | devices.append(json.loads(device.read())) 37 | except Exception as e: 38 | self._log.error("Cannot read device %s: %s" % (dev, str(e))) 39 | return sorted(devices, key=lambda entity: entity.get("index", sys.maxsize)) 40 | 41 | def update_device(self, device): 42 | with open(os.path.join(self._dev_dir, device.get("name") + ".json"), "w") as f: 43 | return f.write(json.dumps(device, indent=4)) 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | start_service.sh 8 | # C extensions 9 | *.so 10 | resources/devices 11 | log 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | -------------------------------------------------------------------------------- /services/discoveryWorker.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | import threading 4 | from constants import * 5 | from services.executorService import ExecutorService 6 | from services.notificationService import NotificationService 7 | from services.deviceRegistryService import DeviceRegistryService 8 | 9 | 10 | class DiscoveryWorker(threading.Thread): 11 | _queue = None 12 | _log = None 13 | _ns = None 14 | _deviceRegistryService = None 15 | _executorService = None 16 | 17 | def __init__(self, queue): 18 | threading.Thread.__init__(self) 19 | self._queue = queue 20 | self._log = logging.getLogger(__name__) 21 | self._ns = NotificationService.get_instance() 22 | self._deviceRegistryService = DeviceRegistryService.get_instance() 23 | self._executorService = ExecutorService.get_instance() 24 | 25 | def run(self): 26 | while True: 27 | device_dto = self._queue.get() 28 | self._discovery_device(device_dto) 29 | self._queue.task_done() 30 | 31 | def _discovery_device(self, device_dto): 32 | self._log.debug("Start discovery device %s with ip: %s" % (device_dto.get("name"), device_dto.get("ip"))) 33 | ping_response = self._executorService.exec_ping(device_dto.get("ip"), count=1, timeout=5) 34 | 35 | if not ping_response: 36 | time.sleep(2) 37 | ping_response = self._executorService.exec_ping(device_dto.get("ip"), count=3, timeout=15) 38 | 39 | if not ping_response: 40 | if device_dto.get("status", OFFLINE_STATE) == ONLINE_STATE: 41 | device_dto["status"] = OFFLINE_STATE 42 | if device_dto.get("notification", "enable") == "enable": 43 | self._ns.notify("%s is offline!" % device_dto.get("name")) 44 | else: 45 | device_dto["last_online"] = int(time.time()) 46 | if device_dto.get("status", OFFLINE_STATE) == OFFLINE_STATE: 47 | device_dto["status"] = ONLINE_STATE 48 | if device_dto.get("notification", "enable") == "enable": 49 | self._ns.notify("%s is online!" % device_dto.get("name")) 50 | device_dto["last_discovery"] = int(time.time()) 51 | self._deviceRegistryService.update_device(device_dto) 52 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | import datetime 4 | import threading 5 | from constants import * 6 | from flask import Flask 7 | from jinja2 import Template 8 | from flask import make_response 9 | from flask import send_from_directory 10 | from mockDataGenerator import generate_mock_data 11 | from services.discoveryService import DiscoveryService 12 | from services.notificationService import NotificationService 13 | from services.deviceRegistryService import DeviceRegistryService 14 | 15 | 16 | if os.environ.get("LOG_MODE") == "prod": 17 | # logging in prod mode 18 | if not os.path.exists(LOG_DIR): 19 | os.makedirs(LOG_DIR) 20 | logging.basicConfig( 21 | format=u'%(threadName)s\t%(filename)s\t[LINE:%(lineno)d]# %(levelname)-8s\t [%(asctime)s] %(message)s', 22 | level="INFO", 23 | handlers=[logging.FileHandler(os.path.join(LOG_DIR, "log.txt"), 'w', 'utf-8')]) 24 | else: 25 | # logging in dev mode 26 | logging.basicConfig( 27 | format=u'%(threadName)s\t%(filename)s\t[LINE:%(lineno)d]# %(levelname)-8s\t [%(asctime)s] %(message)s', 28 | level="DEBUG") 29 | 30 | log = logging.getLogger(__name__) 31 | 32 | if os.environ.get("DEMO_MODE", "disable") == "enable": 33 | generate_mock_data() 34 | 35 | notificationService = NotificationService.get_instance() 36 | discoveryService = DiscoveryService.get_instance() 37 | deviceRegistryService = DeviceRegistryService.get_instance() 38 | 39 | 40 | def pretty_datetime(data): 41 | try: 42 | return datetime.datetime.fromtimestamp(data).strftime("%d.%m.%Y %H:%M:%S") 43 | except Exception as e: 44 | log.error("Cannot format for datetime: %s" % str(e)) 45 | return "-" 46 | 47 | 48 | def do_discovery(): 49 | while True: 50 | try: 51 | discoveryService.discover_devices() 52 | except Exception as e: 53 | notificationService.notify("ERROR DISCOVERY! " + str(e)) 54 | time.sleep(DISCOVERY_PERIOD_SEC) 55 | 56 | 57 | threading.Thread(target=do_discovery, args=(), daemon=True).start() 58 | 59 | 60 | app = Flask(__name__, static_url_path='/static', static_folder=os.path.join(WEB_DIR, "static")) 61 | 62 | 63 | @app.route('/static/') 64 | def send_static(path): 65 | return send_from_directory('static', path) 66 | 67 | 68 | @app.route('/') 69 | def root(): 70 | data_dto = dict(devices=deviceRegistryService.get_all_devices()) 71 | for dev in data_dto.get("devices"): 72 | dev["last_discovery"] = pretty_datetime(dev.get("last_discovery")) 73 | dev["last_online"] = pretty_datetime(dev.get("last_online")) 74 | with open(os.path.join(WEB_DIR, "index.html")) as f: 75 | return make_response(Template(f.read()).render(**data_dto)) 76 | 77 | 78 | if __name__ == "__main__": 79 | app.run(host="0.0.0.0", port=8080, debug=True) 80 | -------------------------------------------------------------------------------- /resources/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Device Monitoring 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Device nameIPStatusLast DiscoveryLast Online
33 |
34 |
35 | 36 | 37 | {% for dev in devices %} 38 | 39 | {% if dev.monitoring == 'disable' %} 40 | 41 | 42 | 43 | 44 | 45 | {% else %} 46 | 47 | 48 | {% if dev.status == 'online' %} 49 | 50 | {% else %} 51 | 52 | {% endif %} 53 | 54 | 55 | {% endif %} 56 | 57 | {% endfor %} 58 | 59 |
{{ dev.name }}{{ dev.ip }}{{ dev.last_discovery }}{{ dev.last_online }}{{ dev.name }}{{ dev.ip }}{{ dev.last_discovery }}{{ dev.last_online }}
60 |
61 |
62 | 63 |
64 |
65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /resources/web/static/css/main.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /*////////////////////////////////////////////////////////////////// 4 | [ FONT ]*/ 5 | 6 | 7 | .gray-dot { 8 | height: 20px; 9 | width: 20px; 10 | background-color: gray; 11 | border-radius: 50%; 12 | display: inline-block; 13 | } 14 | 15 | .red-dot { 16 | height: 20px; 17 | width: 20px; 18 | background-color: red; 19 | border-radius: 50%; 20 | display: inline-block; 21 | } 22 | 23 | .green-dot { 24 | height: 20px; 25 | width: 20px; 26 | background-color: #63ff00; 27 | border-radius: 50%; 28 | display: inline-block; 29 | } 30 | 31 | @font-face { 32 | font-family: Lato-Regular; 33 | src: url('/static/fonts/Lato/Lato-Regular.ttf'); 34 | } 35 | 36 | @font-face { 37 | font-family: Lato-Bold; 38 | src: url('/static/fonts/Lato/Lato-Bold.ttf'); 39 | } 40 | 41 | /*////////////////////////////////////////////////////////////////// 42 | [ RESTYLE TAG ]*/ 43 | * { 44 | margin: 0px; 45 | padding: 0px; 46 | box-sizing: border-box; 47 | } 48 | 49 | body, html { 50 | height: 100%; 51 | font-family: sans-serif; 52 | } 53 | 54 | /* ------------------------------------ */ 55 | a { 56 | margin: 0px; 57 | transition: all 0.4s; 58 | -webkit-transition: all 0.4s; 59 | -o-transition: all 0.4s; 60 | -moz-transition: all 0.4s; 61 | } 62 | 63 | a:focus { 64 | outline: none !important; 65 | } 66 | 67 | a:hover { 68 | text-decoration: none; 69 | } 70 | 71 | /* ------------------------------------ */ 72 | h1,h2,h3,h4,h5,h6 {margin: 0px;} 73 | 74 | p {margin: 0px;} 75 | 76 | ul, li { 77 | margin: 0px; 78 | list-style-type: none; 79 | } 80 | 81 | 82 | /* ------------------------------------ */ 83 | input { 84 | display: block; 85 | outline: none; 86 | border: none !important; 87 | } 88 | 89 | textarea { 90 | display: block; 91 | outline: none; 92 | } 93 | 94 | textarea:focus, input:focus { 95 | border-color: transparent !important; 96 | } 97 | 98 | /* ------------------------------------ */ 99 | button { 100 | outline: none !important; 101 | border: none; 102 | background: transparent; 103 | } 104 | 105 | button:hover { 106 | cursor: pointer; 107 | } 108 | 109 | iframe { 110 | border: none !important; 111 | } 112 | 113 | /*////////////////////////////////////////////////////////////////// 114 | [ Scroll bar ]*/ 115 | .js-pscroll { 116 | position: relative; 117 | overflow: hidden; 118 | } 119 | 120 | .table100 .ps__rail-y { 121 | width: 9px; 122 | background-color: transparent; 123 | opacity: 1 !important; 124 | right: 5px; 125 | } 126 | 127 | .table100 .ps__rail-y::before { 128 | content: ""; 129 | display: block; 130 | position: absolute; 131 | background-color: #ebebeb; 132 | border-radius: 5px; 133 | width: 100%; 134 | height: calc(100% - 30px); 135 | left: 0; 136 | top: 15px; 137 | } 138 | 139 | .table100 .ps__rail-y .ps__thumb-y { 140 | width: 100%; 141 | right: 0; 142 | background-color: transparent; 143 | opacity: 1 !important; 144 | } 145 | 146 | .table100 .ps__rail-y .ps__thumb-y::before { 147 | content: ""; 148 | display: block; 149 | position: absolute; 150 | background-color: #cccccc; 151 | border-radius: 5px; 152 | width: 100%; 153 | height: calc(100% - 30px); 154 | left: 0; 155 | top: 15px; 156 | } 157 | 158 | 159 | /*////////////////////////////////////////////////////////////////// 160 | [ Table ]*/ 161 | 162 | .limiter { 163 | width: 1366px; 164 | margin: 0 auto; 165 | } 166 | 167 | .container-table100 { 168 | width: 100%; 169 | min-height: 100vh; 170 | background: #fff; 171 | 172 | display: -webkit-box; 173 | display: -webkit-flex; 174 | display: -moz-box; 175 | display: -ms-flexbox; 176 | display: flex; 177 | align-items: center; 178 | justify-content: center; 179 | flex-wrap: wrap; 180 | padding: 33px 30px; 181 | } 182 | 183 | .wrap-table100 { 184 | width: 1170px; 185 | } 186 | 187 | /*////////////////////////////////////////////////////////////////// 188 | [ Table ]*/ 189 | .table100 { 190 | background-color: #fff; 191 | } 192 | 193 | table { 194 | width: 100%; 195 | } 196 | 197 | th, td { 198 | font-weight: unset; 199 | padding-right: 10px; 200 | } 201 | 202 | .column1 { 203 | width: 33%; 204 | padding-left: 40px; 205 | } 206 | 207 | .column2 { 208 | width: 13%; 209 | } 210 | 211 | .column3 { 212 | width: 22%; 213 | } 214 | 215 | .column4 { 216 | width: 19%; 217 | } 218 | 219 | .column5 { 220 | width: 13%; 221 | } 222 | 223 | .table100-head th { 224 | padding-top: 18px; 225 | padding-bottom: 18px; 226 | } 227 | 228 | .table100-body td { 229 | padding-top: 16px; 230 | padding-bottom: 16px; 231 | } 232 | 233 | /*================================================================== 234 | [ Fix header ]*/ 235 | .table100 { 236 | position: relative; 237 | padding-top: 60px; 238 | } 239 | 240 | .table100-head { 241 | position: absolute; 242 | width: 100%; 243 | top: 0; 244 | left: 0; 245 | } 246 | 247 | /**/ 248 | /*Screen into table*/ 249 | /**/ 250 | .table100-body { 251 | /*max-height: 585px;*/ 252 | /*overflow: auto;*/ 253 | } 254 | 255 | 256 | /*================================================================== 257 | [ Ver1 ]*/ 258 | 259 | .table100.ver1 th { 260 | font-family: Lato-Bold; 261 | font-size: 18px; 262 | color: #fff; 263 | line-height: 1.4; 264 | 265 | background-color: #6c7ae0; 266 | } 267 | 268 | .table100.ver1 td { 269 | font-family: Lato-Regular; 270 | font-size: 15px; 271 | color: #808080; 272 | line-height: 1.4; 273 | } 274 | 275 | .table100.ver1 .table100-body tr:nth-child(even) { 276 | background-color: #f8f6ff; 277 | } 278 | 279 | /*---------------------------------------------*/ 280 | 281 | .table100.ver1 { 282 | border-radius: 10px; 283 | overflow: hidden; 284 | box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 285 | -moz-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 286 | -webkit-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 287 | -o-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 288 | -ms-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 289 | } 290 | 291 | .table100.ver1 .ps__rail-y { 292 | right: 5px; 293 | } 294 | 295 | .table100.ver1 .ps__rail-y::before { 296 | background-color: #ebebeb; 297 | } 298 | 299 | .table100.ver1 .ps__rail-y .ps__thumb-y::before { 300 | background-color: #cccccc; 301 | } 302 | 303 | 304 | /*================================================================== 305 | [ Ver2 ]*/ 306 | 307 | .table100.ver2 .table100-head { 308 | box-shadow: 0 5px 20px 0px rgba(0, 0, 0, 0.1); 309 | -moz-box-shadow: 0 5px 20px 0px rgba(0, 0, 0, 0.1); 310 | -webkit-box-shadow: 0 5px 20px 0px rgba(0, 0, 0, 0.1); 311 | -o-box-shadow: 0 5px 20px 0px rgba(0, 0, 0, 0.1); 312 | -ms-box-shadow: 0 5px 20px 0px rgba(0, 0, 0, 0.1); 313 | } 314 | 315 | .table100.ver2 th { 316 | font-family: Lato-Bold; 317 | font-size: 18px; 318 | color: #fa4251; 319 | line-height: 1.4; 320 | 321 | background-color: transparent; 322 | } 323 | 324 | .table100.ver2 td { 325 | font-family: Lato-Regular; 326 | font-size: 15px; 327 | color: #808080; 328 | line-height: 1.4; 329 | } 330 | 331 | .table100.ver2 .table100-body tr { 332 | border-bottom: 1px solid #f2f2f2; 333 | } 334 | 335 | /*---------------------------------------------*/ 336 | 337 | .table100.ver2 { 338 | border-radius: 10px; 339 | overflow: hidden; 340 | box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 341 | -moz-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 342 | -webkit-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 343 | -o-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 344 | -ms-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 345 | } 346 | 347 | .table100.ver2 .ps__rail-y { 348 | right: 5px; 349 | } 350 | 351 | .table100.ver2 .ps__rail-y::before { 352 | background-color: #ebebeb; 353 | } 354 | 355 | .table100.ver2 .ps__rail-y .ps__thumb-y::before { 356 | background-color: #cccccc; 357 | } 358 | 359 | /*================================================================== 360 | [ Ver3 ]*/ 361 | 362 | .table100.ver3 { 363 | background-color: #393939; 364 | } 365 | 366 | .table100.ver3 th { 367 | font-family: Lato-Bold; 368 | font-size: 15px; 369 | color: #00ad5f; 370 | line-height: 1.4; 371 | text-transform: uppercase; 372 | background-color: #393939; 373 | } 374 | 375 | .table100.ver3 td { 376 | font-family: Lato-Regular; 377 | font-size: 15px; 378 | color: #808080; 379 | line-height: 1.4; 380 | background-color: #222222; 381 | } 382 | 383 | 384 | /*---------------------------------------------*/ 385 | 386 | .table100.ver3 { 387 | border-radius: 10px; 388 | overflow: hidden; 389 | box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 390 | -moz-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 391 | -webkit-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 392 | -o-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 393 | -ms-box-shadow: 0 0px 40px 0px rgba(0, 0, 0, 0.15); 394 | } 395 | 396 | .table100.ver3 .ps__rail-y { 397 | right: 5px; 398 | } 399 | 400 | .table100.ver3 .ps__rail-y::before { 401 | background-color: #4e4e4e; 402 | } 403 | 404 | .table100.ver3 .ps__rail-y .ps__thumb-y::before { 405 | background-color: #00ad5f; 406 | } 407 | 408 | 409 | /*================================================================== 410 | [ Ver4 ]*/ 411 | .table100.ver4 { 412 | margin-right: -20px; 413 | } 414 | 415 | .table100.ver4 .table100-head { 416 | padding-right: 20px; 417 | } 418 | 419 | .table100.ver4 th { 420 | font-family: Lato-Bold; 421 | font-size: 18px; 422 | color: #4272d7; 423 | line-height: 1.4; 424 | 425 | background-color: transparent; 426 | border-bottom: 2px solid #f2f2f2; 427 | } 428 | 429 | .table100.ver4 .column1 { 430 | padding-left: 7px; 431 | } 432 | 433 | .table100.ver4 td { 434 | font-family: Lato-Regular; 435 | font-size: 15px; 436 | color: #808080; 437 | line-height: 1.4; 438 | } 439 | 440 | .table100.ver4 .table100-body tr { 441 | border-bottom: 1px solid #f2f2f2; 442 | } 443 | 444 | /*---------------------------------------------*/ 445 | 446 | .table100.ver4 { 447 | overflow: hidden; 448 | } 449 | 450 | .table100.ver4 .table100-body{ 451 | padding-right: 20px; 452 | } 453 | 454 | .table100.ver4 .ps__rail-y { 455 | right: 0px; 456 | } 457 | 458 | .table100.ver4 .ps__rail-y::before { 459 | background-color: #ebebeb; 460 | } 461 | 462 | .table100.ver4 .ps__rail-y .ps__thumb-y::before { 463 | background-color: #cccccc; 464 | } 465 | 466 | 467 | /*================================================================== 468 | [ Ver5 ]*/ 469 | .table100.ver5 { 470 | margin-right: -30px; 471 | } 472 | 473 | .table100.ver5 .table100-head { 474 | padding-right: 30px; 475 | } 476 | 477 | .table100.ver5 th { 478 | font-family: Lato-Bold; 479 | font-size: 14px; 480 | color: #555555; 481 | line-height: 1.4; 482 | text-transform: uppercase; 483 | 484 | background-color: transparent; 485 | } 486 | 487 | .table100.ver5 td { 488 | font-family: Lato-Regular; 489 | font-size: 15px; 490 | color: #808080; 491 | line-height: 1.4; 492 | 493 | background-color: #f7f7f7; 494 | } 495 | 496 | .table100.ver5 .table100-body tr { 497 | overflow: hidden; 498 | border-bottom: 10px solid #fff; 499 | border-radius: 10px; 500 | } 501 | 502 | .table100.ver5 .table100-body table { 503 | border-collapse: separate; 504 | border-spacing: 0 10px; 505 | } 506 | .table100.ver5 .table100-body td { 507 | border: solid 1px transparent; 508 | border-style: solid none; 509 | padding-top: 10px; 510 | padding-bottom: 10px; 511 | } 512 | .table100.ver5 .table100-body td:first-child { 513 | border-left-style: solid; 514 | border-top-left-radius: 10px; 515 | border-bottom-left-radius: 10px; 516 | } 517 | .table100.ver5 .table100-body td:last-child { 518 | border-right-style: solid; 519 | border-bottom-right-radius: 10px; 520 | border-top-right-radius: 10px; 521 | } 522 | 523 | .table100.ver5 tr:hover td { 524 | background-color: #ebebeb; 525 | cursor: pointer; 526 | } 527 | 528 | .table100.ver5 .table100-head th { 529 | padding-top: 25px; 530 | padding-bottom: 25px; 531 | } 532 | 533 | /*---------------------------------------------*/ 534 | 535 | .table100.ver5 { 536 | overflow: hidden; 537 | } 538 | 539 | .table100.ver5 .table100-body{ 540 | padding-right: 30px; 541 | } 542 | 543 | .table100.ver5 .ps__rail-y { 544 | right: 0px; 545 | } 546 | 547 | .table100.ver5 .ps__rail-y::before { 548 | background-color: #ebebeb; 549 | } 550 | 551 | .table100.ver5 .ps__rail-y .ps__thumb-y::before { 552 | background-color: #cccccc; 553 | } 554 | --------------------------------------------------------------------------------