From cd13f8496a258b440a7a00659d744128eb069f45 Mon Sep 17 00:00:00 2001 From: Alexander Kislitsky Date: Mon, 27 Jun 2016 17:26:50 +0300 Subject: [PATCH] Elasticsearch removing from fuel-stats analytics We don't use Elasticsearch for flexible reports generation on the fuel-stats web UI, only for five fixed reports. Thus using of Elasticsearch is overhead and it can be removed from the servers Instad of Elasticsearch we use fuel-stats json api calls and PostgreSQL + Memcached. Changes list: - api call added to fuel-stats json api for data required on the web UI page, - column release added to DB installation_structures table schema, - memcached is used for caching data for the web UI page, - elasticsearch client removed from js requirement, - web UI page rewritten to use fuel-stats json api instead Elaticsearch. Change-Id: Ie752e0d0a3c80933888f986e2497b45adce730c9 Closes-Bug: #1595548 --- analytics/fuel_analytics/api/app.py | 2 + analytics/fuel_analytics/api/config.py | 3 + analytics/fuel_analytics/api/db/model.py | 1 + .../api/resources/json_reports.py | 151 ++++ .../api/resources/utils/test_json_reports.py | 551 ++++++++++++ analytics/requirements.txt | 1 + analytics/static/bower.json | 1 - analytics/static/css/fuel-stat.css | 47 + analytics/static/img/loader-bg.svg | 26 + analytics/static/img/loader-logo.svg | 17 + analytics/static/index.html | 9 +- analytics/static/js/app.js | 827 +++++++----------- ..._release_column_addded_to_installation_.py | 61 ++ collector/collector/api/db/model.py | 1 + .../api/resources/installation_structure.py | 5 + .../resources/test_installation_structure.py | 25 + requirements.txt | 1 + 17 files changed, 1210 insertions(+), 519 deletions(-) create mode 100644 analytics/fuel_analytics/api/resources/json_reports.py create mode 100644 analytics/fuel_analytics/test/api/resources/utils/test_json_reports.py create mode 100644 analytics/static/img/loader-bg.svg create mode 100644 analytics/static/img/loader-logo.svg create mode 100644 collector/collector/api/db/migrations/versions/24081e26a283_release_column_addded_to_installation_.py diff --git a/analytics/fuel_analytics/api/app.py b/analytics/fuel_analytics/api/app.py index 31ff5ef..fd55c4e 100644 --- a/analytics/fuel_analytics/api/app.py +++ b/analytics/fuel_analytics/api/app.py @@ -26,9 +26,11 @@ db = flask_sqlalchemy.SQLAlchemy(app) # Registering blueprints from fuel_analytics.api.resources.csv_exporter import bp as csv_exporter_bp from fuel_analytics.api.resources.json_exporter import bp as json_exporter_bp +from fuel_analytics.api.resources.json_reports import bp as json_reports_bp app.register_blueprint(csv_exporter_bp, url_prefix='/api/v1/csv') app.register_blueprint(json_exporter_bp, url_prefix='/api/v1/json') +app.register_blueprint(json_reports_bp, url_prefix='/api/v1/json/report') @app.errorhandler(DateExtractionError) diff --git a/analytics/fuel_analytics/api/config.py b/analytics/fuel_analytics/api/config.py index 76d8ffb..a40baeb 100644 --- a/analytics/fuel_analytics/api/config.py +++ b/analytics/fuel_analytics/api/config.py @@ -32,7 +32,10 @@ class Production(object): CSV_DEFAULT_FROM_DATE_DAYS = 90 CSV_DB_YIELD_PER = 100 JSON_DB_DEFAULT_LIMIT = 1000 + JSON_DB_YIELD_PER = 100 CSV_DEFAULT_LIST_ITEMS_NUM = 5 + MEMCACHED_HOSTS = ['localhost:11211'] + MEMCACHED_JSON_REPORTS_EXPIRATION = 3600 class Testing(Production): diff --git a/analytics/fuel_analytics/api/db/model.py b/analytics/fuel_analytics/api/db/model.py index 26f5305..0ba0dba 100644 --- a/analytics/fuel_analytics/api/db/model.py +++ b/analytics/fuel_analytics/api/db/model.py @@ -39,6 +39,7 @@ class InstallationStructure(db.Model): creation_date = db.Column(db.DateTime) modification_date = db.Column(db.DateTime) is_filtered = db.Column(db.Boolean) + release = db.Column(db.Text) class ActionLog(db.Model): diff --git a/analytics/fuel_analytics/api/resources/json_reports.py b/analytics/fuel_analytics/api/resources/json_reports.py new file mode 100644 index 0000000..aab2dbc --- /dev/null +++ b/analytics/fuel_analytics/api/resources/json_reports.py @@ -0,0 +1,151 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from collections import defaultdict +import copy +import json + +from flask import Blueprint +from flask import request +from flask import Response +import memcache + +from fuel_analytics.api.app import app +from fuel_analytics.api.app import db +from fuel_analytics.api.db.model import InstallationStructure as IS + +bp = Blueprint('reports', __name__) + + +@bp.route('/installations', methods=['GET']) +def get_installations_info(): + release = request.args.get('release', '') + refresh = request.args.get('refresh') + cache_key_prefix = 'fuel-stats-installations-info' + mc = memcache.Client(app.config.get('MEMCACHED_HOSTS')) + app.logger.debug("Fetching installations info for release: %s", release) + + # Checking cache + if not refresh: + cache_key = cache_key_prefix + release + app.logger.debug("Checking installations info by key: %s in cache", + cache_key) + cached_result = mc.get(cache_key) + if cached_result: + app.logger.debug("Installations info cache found by key: %s", + cache_key) + return Response(json.dumps(cached_result), + mimetype='application/json') + else: + app.logger.debug("No cached installations info for key: %s", + cache_key) + else: + app.logger.debug("Enforce refresh cache of installations info " + "for release: %s", release) + + # Fetching data from DB + info_from_db = get_installations_info_from_db(release) + + # Saving fetched data to cache + for for_release, info in info_from_db.items(): + app.logger.debug("Caching installations info for key: %s, data: %s", + cache_key_prefix + for_release, info) + mc.set(cache_key_prefix + for_release, info, + app.config.get('MEMCACHED_JSON_REPORTS_EXPIRATION')) + + return Response(json.dumps(info_from_db[release]), + mimetype='application/json') + + +def get_installations_info_from_db(release): + query = db.session.query(IS.structure, IS.release).\ + filter(IS.is_filtered == bool(0)) + if release: + query = query.filter(IS.release == release) + + info_template = { + 'installations': { + 'count': 0, + 'environments_num': defaultdict(int) + }, + 'environments': { + 'count': 0, + 'operable_envs_count': 0, + 'statuses': defaultdict(int), + 'nodes_num': defaultdict(int), + 'hypervisors_num': defaultdict(int), + 'oses_num': defaultdict(int) + } + } + + info = defaultdict(lambda: copy.deepcopy(info_template)) + + app.logger.debug("Fetching installations info from DB for release: %s", + release) + + yield_per = app.config['JSON_DB_YIELD_PER'] + for row in query.yield_per(yield_per): + structure = row[0] + extract_installation_info(structure, info[release]) + + cur_release = row[1] + # Splitting info by release if fetching for all releases + if not release and cur_release != release: + extract_installation_info(structure, info[cur_release]) + + app.logger.debug("Fetched installations info from DB for release: " + "%s, info: %s", release, info) + + return info + + +def extract_installation_info(source, result): + """Extracts installation info from structure + + :param source: source of installation info data + :type source: dict + :param result: placeholder for extracted data + :type result: dict + """ + + inst_info = result['installations'] + env_info = result['environments'] + + production_statuses = ('operational', 'error') + + inst_info['count'] += 1 + envs_num = 0 + + for cluster in source.get('clusters', []): + envs_num += 1 + env_info['count'] += 1 + + if cluster.get('status') in production_statuses: + current_nodes_num = cluster.get('nodes_num', 0) + env_info['nodes_num'][current_nodes_num] += 1 + env_info['operable_envs_count'] += 1 + + hypervisor = cluster.get('attributes', {}).get('libvirt_type') + if hypervisor: + env_info['hypervisors_num'][hypervisor.lower()] += 1 + + os = cluster.get('release', {}).get('os') + if os: + env_info['oses_num'][os.lower()] += 1 + + status = cluster.get('status') + if status is not None: + env_info['statuses'][status] += 1 + + inst_info['environments_num'][envs_num] += 1 diff --git a/analytics/fuel_analytics/test/api/resources/utils/test_json_reports.py b/analytics/fuel_analytics/test/api/resources/utils/test_json_reports.py new file mode 100644 index 0000000..eee2ea1 --- /dev/null +++ b/analytics/fuel_analytics/test/api/resources/utils/test_json_reports.py @@ -0,0 +1,551 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import memcache +import mock + +from fuel_analytics.test.base import DbTest + +from fuel_analytics.api.app import app +from fuel_analytics.api.app import db +from fuel_analytics.api.db import model + + +class JsonReportsTest(DbTest): + + @mock.patch.object(memcache.Client, 'get', return_value=None) + def test_get_installations_num(self, _): + structures = [ + model.InstallationStructure( + master_node_uid='x0', + structure={}, + is_filtered=False, + release='9.0' + ), + model.InstallationStructure( + master_node_uid='x1', + structure={}, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x2', + structure={}, + is_filtered=True, + release='8.0' + ), + ] + for structure in structures: + db.session.add(structure) + db.session.flush() + + with app.test_request_context(): + url = '/api/v1/json/report/installations' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual(2, resp['installations']['count']) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=8.0' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual(1, resp['installations']['count']) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=xxx' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual(0, resp['installations']['count']) + + @mock.patch.object(memcache.Client, 'get', return_value=None) + def test_get_env_statuses(self, _): + structures = [ + model.InstallationStructure( + master_node_uid='x0', + structure={ + 'clusters': [ + {'status': 'new'}, + {'status': 'operational'}, + {'status': 'error'} + ] + }, + is_filtered=False, + release='9.0' + ), + model.InstallationStructure( + master_node_uid='x1', + structure={ + 'clusters': [ + {'status': 'deployment'}, + {'status': 'operational'}, + {'status': 'operational'}, + ] + }, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x2', + structure={ + 'clusters': [ + {'status': 'deployment'}, + {'status': 'operational'}, + ] + }, + is_filtered=True, + release='8.0' + ), + ] + for structure in structures: + db.session.add(structure) + db.session.flush() + + with app.test_request_context(): + url = '/api/v1/json/report/installations' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual( + {'new': 1, 'deployment': 1, 'error': 1, 'operational': 3}, + resp['environments']['statuses'] + ) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=8.0' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual( + {'deployment': 1, 'operational': 2}, + resp['environments']['statuses'] + ) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=xxx' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual({}, resp['environments']['statuses']) + + @mock.patch.object(memcache.Client, 'get', return_value=None) + def test_get_env_num(self, _): + structures = [ + model.InstallationStructure( + master_node_uid='x0', + structure={'clusters': [{}, {}, {}]}, + is_filtered=False, + release='9.0' + ), + model.InstallationStructure( + master_node_uid='x1', + structure={'clusters': [{}, {}]}, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x2', + structure={'clusters': []}, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x3', + structure={'clusters': []}, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x4', + structure={'clusters': [{}, {}, {}]}, + is_filtered=True, + release='8.0' + ), + ] + for structure in structures: + db.session.add(structure) + db.session.flush() + + with app.test_request_context(): + url = '/api/v1/json/report/installations' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual(5, resp['environments']['count']) + self.assertEqual( + {'0': 2, '2': 1, '3': 1}, + resp['installations']['environments_num'] + ) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=8.0' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual(2, resp['environments']['count']) + self.assertEqual( + {'0': 2, '2': 1}, + resp['installations']['environments_num'] + ) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=xxx' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual(0, resp['environments']['count']) + self.assertEqual({}, resp['installations']['environments_num']) + + @mock.patch.object(memcache.Client, 'set') + @mock.patch.object(memcache.Client, 'get', return_value=None) + def test_caching(self, cached_mc_get, cached_mc_set): + structures = [ + model.InstallationStructure( + master_node_uid='x0', + structure={'clusters': [{}, {}, {}]}, + is_filtered=False, + release='9.0' + ), + model.InstallationStructure( + master_node_uid='x1', + structure={'clusters': [{}, {}]}, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x2', + structure={'clusters': [{}]}, + is_filtered=True, + release='8.0' + ) + ] + for structure in structures: + db.session.add(structure) + db.session.flush() + + with app.test_request_context(): + url = '/api/v1/json/report/installations' + resp = self.client.get(url) + self.check_response_ok(resp) + self.assertEqual(1, cached_mc_get.call_count) + # Checking that mc.set was called for each release and + # for all releases summary info + calls = [ + mock.call( + 'fuel-stats-installations-info', + {'installations': {'environments_num': {2: 1, 3: 1}, + 'count': 2}, + 'environments': {'count': 5, 'hypervisors_num': {}, + 'oses_num': {}, 'nodes_num': {}, + 'operable_envs_count': 0, + 'statuses': {}}}, + 3600 + ), + mock.call( + 'fuel-stats-installations-info8.0', + {'installations': {'environments_num': {2: 1}, 'count': 1}, + 'environments': {'count': 2, 'hypervisors_num': {}, + 'oses_num': {}, 'nodes_num': {}, + 'operable_envs_count': 0, + 'statuses': {}}}, + 3600 + ), + mock.call( + 'fuel-stats-installations-info9.0', + {'installations': {'environments_num': {3: 1}, 'count': 1}, + 'environments': {'count': 3, 'hypervisors_num': {}, + 'oses_num': {}, 'nodes_num': {}, + 'operable_envs_count': 0, + 'statuses': {}}}, + 3600 + ), + ] + cached_mc_set.assert_has_calls(calls, any_order=True) + self.assertEqual(len(calls), cached_mc_set.call_count) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=8.0' + resp = self.client.get(url) + self.check_response_ok(resp) + self.assertEqual(2, cached_mc_get.call_count) + self.assertEqual(len(calls) + 1, cached_mc_set.call_count) + cached_mc_set.assert_called_with( + 'fuel-stats-installations-info8.0', + {'installations': {'environments_num': {2: 1}, 'count': 1}, + 'environments': {'count': 2, 'hypervisors_num': {}, + 'oses_num': {}, 'nodes_num': {}, + 'operable_envs_count': 0, + 'statuses': {}}}, + 3600 + ) + + @mock.patch.object(memcache.Client, 'set') + @mock.patch.object(memcache.Client, 'get') + def test_refresh_cached_data(self, cached_mc_get, cached_mc_set): + structures = [ + model.InstallationStructure( + master_node_uid='x0', + structure={'clusters': [{}, {}, {}]}, + is_filtered=False, + release='9.0' + ), + model.InstallationStructure( + master_node_uid='x1', + structure={'clusters': [{}, {}]}, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x2', + structure={'clusters': [{}]}, + is_filtered=True, + release='8.0' + ) + ] + for structure in structures: + db.session.add(structure) + db.session.flush() + + with app.test_request_context(): + url = '/api/v1/json/report/installations?refresh=1' + resp = self.client.get(url) + self.check_response_ok(resp) + self.assertEqual(0, cached_mc_get.call_count) + self.assertEquals(3, cached_mc_set.call_count) + + @mock.patch.object(memcache.Client, 'get', return_value=None) + def test_get_nodes_num(self, _): + structures = [ + model.InstallationStructure( + master_node_uid='x0', + structure={ + 'clusters': [ + {'status': 'operational', 'nodes_num': 3}, + {'status': 'new', 'nodes_num': 2}, + {'status': 'error', 'nodes_num': 1}, + ] + }, + is_filtered=False, + release='9.0' + ), + model.InstallationStructure( + master_node_uid='x1', + structure={ + 'clusters': [ + {'status': 'operational', 'nodes_num': 3} + ], + }, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x2', + structure={ + 'clusters': [ + {'status': 'operational', 'nodes_num': 5}, + {'status': 'new', 'nodes_num': 6}, + {'status': 'error', 'nodes_num': 7}, + ] + }, + is_filtered=True, + release='8.0' + ), + ] + for structure in structures: + db.session.add(structure) + db.session.flush() + + with app.test_request_context(): + url = '/api/v1/json/report/installations' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual(3, resp['environments']['operable_envs_count']) + self.assertEqual( + {'3': 2, '1': 1}, + resp['environments']['nodes_num'] + ) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=9.0' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual(2, resp['environments']['operable_envs_count']) + self.assertEqual( + {'3': 1, '1': 1}, + resp['environments']['nodes_num'] + ) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=xxx' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual(0, resp['environments']['operable_envs_count']) + self.assertEqual({}, resp['environments']['nodes_num']) + + @mock.patch.object(memcache.Client, 'get', return_value=None) + def test_get_hypervisors_num(self, _): + structures = [ + model.InstallationStructure( + master_node_uid='x0', + structure={ + 'clusters': [ + {'status': 'operational', 'attributes': + {'libvirt_type': 'kvm'}}, + {'status': 'operational', 'attributes': + {'libvirt_type': 'Qemu'}}, + {'status': 'operational', 'attributes': + {'libvirt_type': 'Kvm'}}, + {'status': 'new', 'attributes': + {'libvirt_type': 'kvm'}}, + {'status': 'error', 'attributes': + {'libvirt_type': 'qemu'}}, + ] + }, + is_filtered=False, + release='9.0' + ), + model.InstallationStructure( + master_node_uid='x1', + structure={ + 'clusters': [ + {'status': 'new', 'attributes': + {'libvirt_type': 'qemu'}}, + {'status': 'error', 'attributes': + {'libvirt_type': 'Kvm'}}, + {'status': 'error', 'attributes': + {'libvirt_type': 'vcenter'}}, + ], + }, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x2', + structure={ + 'clusters': [ + {'status': 'operational', 'attributes': + {'libvirt_type': 'kvm'}}, + {'status': 'new', 'attributes': + {'libvirt_type': 'kvm'}}, + {'status': 'error', 'attributes': + {'libvirt_type': 'qemu'}}, + ] + }, + is_filtered=True, + release='8.0' + ), + ] + for structure in structures: + db.session.add(structure) + db.session.flush() + + with app.test_request_context(): + url = '/api/v1/json/report/installations' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual( + {'kvm': 3, 'vcenter': 1, 'qemu': 2}, + resp['environments']['hypervisors_num'] + ) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=8.0' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual( + {'kvm': 1, 'vcenter': 1}, + resp['environments']['hypervisors_num'] + ) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=xxx' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual({}, resp['environments']['hypervisors_num']) + + @mock.patch.object(memcache.Client, 'get', return_value=None) + def test_get_oses_num(self, _): + structures = [ + model.InstallationStructure( + master_node_uid='x0', + structure={ + 'clusters': [ + {'status': 'operational', 'release': {'os': 'Ubuntu'}}, + {'status': 'error', 'release': {'os': 'ubuntu'}}, + {'status': 'error', 'release': {'os': 'Centos'}} + ] + }, + is_filtered=False, + release='9.0' + ), + model.InstallationStructure( + master_node_uid='x1', + structure={ + 'clusters': [ + {'status': 'new', 'release': {'os': 'Ubuntu'}}, + {'status': 'operational', 'release': {'os': 'ubuntu'}} + ], + }, + is_filtered=False, + release='8.0' + ), + model.InstallationStructure( + master_node_uid='x2', + structure={ + 'clusters': [ + {'status': 'new', 'release': {'os': 'centos'}}, + {'status': 'operational', 'release': {'os': 'centos'}}, + {'status': 'operational', 'release': {'os': 'centos'}} + ] + }, + is_filtered=True, + release='8.0' + ), + ] + for structure in structures: + db.session.add(structure) + db.session.flush() + + with app.test_request_context(): + url = '/api/v1/json/report/installations' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual( + {'ubuntu': 3, 'centos': 1}, + resp['environments']['oses_num'] + ) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=8.0' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual({'ubuntu': 1}, resp['environments']['oses_num']) + + with app.test_request_context(): + url = '/api/v1/json/report/installations?release=xxx' + resp = self.client.get(url) + self.check_response_ok(resp) + resp = json.loads(resp.data) + self.assertEqual({}, resp['environments']['oses_num']) diff --git a/analytics/requirements.txt b/analytics/requirements.txt index a4d25c1..e09a9cc 100644 --- a/analytics/requirements.txt +++ b/analytics/requirements.txt @@ -2,5 +2,6 @@ psycopg2==2.5.4 Flask==0.10.1 Flask-Script==2.0.5 Flask-SQLAlchemy==2.0 +python-memcached>=1.56 SQLAlchemy==0.9.8 six>=1.8.0 diff --git a/analytics/static/bower.json b/analytics/static/bower.json index 462c5e7..74c8eed 100644 --- a/analytics/static/bower.json +++ b/analytics/static/bower.json @@ -8,7 +8,6 @@ "d3-tip": "0.6.3", "d3pie": "0.1.4", "nvd3": "1.1.15-beta", - "elasticsearch": "3.0.0", "requirejs": "2.1.15" }, "overrides": { diff --git a/analytics/static/css/fuel-stat.css b/analytics/static/css/fuel-stat.css index 1138884..d61abee 100644 --- a/analytics/static/css/fuel-stat.css +++ b/analytics/static/css/fuel-stat.css @@ -41,6 +41,53 @@ html, body { background-color: #f3f3f4; height: 100%; } + +#loader { + background-color: #415766; + position: absolute; + top: 0; + width: 100%; + height: 100%; +} +#load-error { + background-color: #415766; + position: absolute; + top: 0; + width: 100%; + height: 100%; + color: white; + text-align: center; + padding-top: 100px; + font-size: 24px; +} +@keyframes loading { + from {transform:rotate(0deg);} + to {transform:rotate(360deg);} +} +.loading:before { + content: ""; + display: block; + width: 98px; + height: 98px; + overflow: hidden; + margin: 200px auto 0; + background: url(../img/loader-bg.svg); + animation: loading 6s linear infinite; +} +.loading:after { + content: ""; + display: block; + width: 98px; + height: 98px; + margin: -124px auto 0; + position: relative; + z-index: 9999; + background: url(../img/loader-logo.svg); +} + +.hidden { + display: none; +} .nav-pannel { width: 70px; height: 100%; diff --git a/analytics/static/img/loader-bg.svg b/analytics/static/img/loader-bg.svg new file mode 100644 index 0000000..a858ac5 --- /dev/null +++ b/analytics/static/img/loader-bg.svg @@ -0,0 +1,26 @@ + + + + + + diff --git a/analytics/static/img/loader-logo.svg b/analytics/static/img/loader-logo.svg new file mode 100644 index 0000000..ae98d6b --- /dev/null +++ b/analytics/static/img/loader-logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/analytics/static/index.html b/analytics/static/index.html index 138e823..d8fcf1a 100644 --- a/analytics/static/index.html +++ b/analytics/static/index.html @@ -36,8 +36,15 @@ +
+
+
+ + -
+