From c1b1badc963922216c5d2e00061caa78e2a16fb1 Mon Sep 17 00:00:00 2001 From: redref Date: Sun, 18 Dec 2016 18:52:48 +0100 Subject: [PATCH] Paging - Revamp tables with Jquery Datatables (Ajax) --- MANIFEST.in | 2 +- README.rst | 2 +- puppetboard/app.py | 343 +++++++----------- puppetboard/default_settings.py | 4 +- puppetboard/static/coffeescript/tables.coffee | 24 -- puppetboard/static/css/puppetboard.css | 14 - .../dataTables.semanticui.min.css | 1 + .../dataTables.semanticui.min.js | 9 + .../jquery.dataTables.min.js | 167 +++++++++ .../static/jquery-tablesort-v.0.0.7/README.md | 215 ----------- .../jquery.tablesort.min.js | 6 - puppetboard/static/js/lists.js | 2 +- puppetboard/static/js/tables.js | 35 -- puppetboard/static/js/timestamps.js | 32 +- puppetboard/templates/_macros.html | 164 +++------ puppetboard/templates/index.html | 2 +- puppetboard/templates/layout.html | 25 +- puppetboard/templates/node.html | 31 +- puppetboard/templates/nodes.html | 2 +- puppetboard/templates/reports.html | 75 ++-- puppetboard/templates/reports.json.tpl | 29 ++ puppetboard/utils.py | 33 -- 22 files changed, 510 insertions(+), 707 deletions(-) delete mode 100644 puppetboard/static/coffeescript/tables.coffee create mode 100644 puppetboard/static/jquery-datatables-1.10.13/dataTables.semanticui.min.css create mode 100644 puppetboard/static/jquery-datatables-1.10.13/dataTables.semanticui.min.js create mode 100644 puppetboard/static/jquery-datatables-1.10.13/jquery.dataTables.min.js delete mode 100644 puppetboard/static/jquery-tablesort-v.0.0.7/README.md delete mode 100644 puppetboard/static/jquery-tablesort-v.0.0.7/jquery.tablesort.min.js delete mode 100644 puppetboard/static/js/tables.js create mode 100644 puppetboard/templates/reports.json.tpl diff --git a/MANIFEST.in b/MANIFEST.in index 2f84d3b..237a79b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,4 +2,4 @@ include README.rst include CHANGELOG.rst include LICENSE recursive-include puppetboard/static *.css *.js *icons.* Open_Sans.woff -recursive-include puppetboard/templates *.html +recursive-include puppetboard/templates *.html *.json.tpl diff --git a/README.rst b/README.rst index 0281e4b..4137c42 100644 --- a/README.rst +++ b/README.rst @@ -236,7 +236,7 @@ Other settings that might be interesting in no particular order: * ``REPORTS_COUNT``: Defaults to ``10`` the limit of the number of reports to load on the node or any reports page. * ``OFFLINE_MODE``: If set to ``True`` load static assets (jquery, - semantic-ui, tablesorter, etc) from the local web server instead of a CDN. + semantic-ui, etc) from the local web server instead of a CDN. Defaults to ``False``. .. _pypuppetdb documentation: http://pypuppetdb.readthedocs.org/en/v0.1.0/quickstart.html#ssl diff --git a/puppetboard/app.py b/puppetboard/app.py index f236306..4e14f11 100644 --- a/puppetboard/app.py +++ b/puppetboard/app.py @@ -22,13 +22,22 @@ from pypuppetdb.QueryBuilder import * from puppetboard.forms import (CatalogForm, QueryForm) from puppetboard.utils import ( get_or_abort, yield_or_stop, - jsonprint, prettyprint, Pagination + jsonprint, prettyprint ) from puppetboard.dailychart import get_daily_reports_chart import werkzeug.exceptions as ex -DEFAULT_ORDER_BY = '[{"field": "start_time", "order": "desc"}]' +REPORTS_COLUMNS = [ + {'attr': 'end', 'filter': 'end_time', + 'name': 'End time', 'type': 'datetime'}, + {'attr': 'status', 'name': 'Status', 'type': 'status'}, + {'attr': 'certname', 'name': 'Certname', 'type': 'node'}, + {'attr': 'version', 'filter': 'configuration_version', + 'name': 'Configuration version'}, + {'attr': 'agent_version', 'filter': 'puppet_version', + 'name': 'Agent version'}, +] app = Flask(__name__) @@ -377,9 +386,9 @@ def inventory(env): ))) -@app.route('/node/', +@app.route('/node//', defaults={'env': app.config['DEFAULT_ENVIRONMENT']}) -@app.route('//node/') +@app.route('//node//') def node(env, node_name): """Display a dashboard for a node showing as much data as we have on that node. This includes facts and reports but not Resources as that is too @@ -399,236 +408,158 @@ def node(env, node_name): node = get_or_abort(puppetdb.node, node_name) facts = node.facts() - reports = get_or_abort(puppetdb.reports, - query=query, - limit=app.config['REPORTS_COUNT'], - order_by=DEFAULT_ORDER_BY) - reports, reports_events = tee(reports) - report_event_counts = {} - - for report in reports_events: - report_event_counts[report.hash_] = {} - - for event in report.events(): - if event.status == 'success': - try: - report_event_counts[report.hash_]['successes'] += 1 - except KeyError: - report_event_counts[report.hash_]['successes'] = 1 - elif event.status == 'failure': - try: - report_event_counts[report.hash_]['failures'] += 1 - except KeyError: - report_event_counts[report.hash_]['failures'] = 1 - elif event.status == 'noop': - try: - report_event_counts[report.hash_]['noops'] += 1 - except KeyError: - report_event_counts[report.hash_]['noops'] = 1 - elif event.status == 'skipped': - try: - report_event_counts[report.hash_]['skips'] += 1 - except KeyError: - report_event_counts[report.hash_]['skips'] = 1 return render_template( 'node.html', node=node, facts=yield_or_stop(facts), - reports=yield_or_stop(reports), - reports_count=app.config['REPORTS_COUNT'], - report_event_counts=report_event_counts, envs=envs, - current_env=env) + current_env=env, + columns=REPORTS_COLUMNS[:2]) -@app.route('/reports/', - defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'page': 1}) -@app.route('//reports/', defaults={'page': 1}) -@app.route('//reports/page/') -def reports(env, page): - """Displays a list of reports and status from all nodes, retreived using the - reports endpoint, sorted by start_time. +@app.route( + '/reports/', + defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'node_name': None}) +@app.route('//reports/', defaults={'node_name': None}) +@app.route( + '/reports//', + defaults={'env': app.config['DEFAULT_ENVIRONMENT']}) +@app.route('//reports/') +def reports(env, node_name): + """Query and Return JSON data to reports Jquery datatable :param env: Search for all reports in this environment :type env: :obj:`string` - :param page: Calculates the offset of the query based on the report count - and this value - :type page: :obj:`int` """ envs = environments() check_env(env, envs) - limit = request.args.get('limit', app.config['REPORTS_COUNT']) - status_arg = request.args.get('status') - reports_query = AndOperator() - total_query = ExtractOperator() + return render_template( + 'reports.html', + envs=envs, + current_env=env, + node_name=node_name, + columns=REPORTS_COLUMNS) - total_query.add_field(FunctionOperator("count")) + +@app.route( + '/reports/json', + defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'node_name': None}) +@app.route('//reports/json', defaults={'node_name': None}) +@app.route( + '/reports//json', + defaults={'env': app.config['DEFAULT_ENVIRONMENT']}) +@app.route('//reports//json') +def reports_ajax(env, node_name): + """Query and Return JSON data to reports Jquery datatable + + :param env: Search for all reports in this environment + :type env: :obj:`string` + """ + draw = int(request.args.get('draw', 0)) + start = int(request.args.get('start', 0)) + length = int(request.args.get('length')) + paging_args = {'limit': length, 'offset': start} + search_arg = request.args.get('search[value]') + order_column = int(request.args.get('order[0][column]')) + order_filter = REPORTS_COLUMNS[order_column].get( + 'filter', REPORTS_COLUMNS[order_column]['attr']) + order_dir = request.args.get('order[0][dir]') + order_args = '[{"field": "%s", "order": "%s"}]' % (order_filter, order_dir) + status_args = request.args.get('columns[1][search][value]').split('|') + for i in range(len(REPORTS_COLUMNS)): + if request.args.get("columns[%s][data]" % i, None): + max_col = i + 1 + + envs = environments() + check_env(env, envs) + reports_query = AndOperator() if env != '*': reports_query.add(EqualsOperator("environment", env)) - if status_arg in ['failed', 'changed', 'unchanged']: - reports_query.add(EqualsOperator('status', status_arg)) - reports_query.add(EqualsOperator('noop', False)) - elif status_arg == 'noop': - reports_query.add(EqualsOperator('noop', True)) + if node_name: + reports_query.add(EqualsOperator("certname", node_name)) - if len(reports_query.operations) == 0: - reports_query = None + if search_arg: + search_query = OrOperator() + search_query.add(RegexOperator("certname", r"%s" % search_arg)) + search_query.add(RegexOperator("puppet_version", r"%s" % search_arg)) + search_query.add(RegexOperator( + "configuration_version", r"%s" % search_arg)) + reports_query.add(search_query) - if reports_query is not None: - total_query.add_query(reports_query) + status_query = OrOperator() + for status_arg in status_args: + if status_arg in ['failed', 'changed', 'unchanged']: + arg_query = AndOperator() + arg_query.add(EqualsOperator('status', status_arg)) + arg_query.add(EqualsOperator('noop', False)) + status_query.add(arg_query) + if status_arg == 'unchanged': + arg_query = AndOperator() + arg_query.add(EqualsOperator('noop', True)) + arg_query.add(EqualsOperator('noop_pending', False)) + status_query.add(arg_query) + elif status_arg == 'noop': + arg_query = AndOperator() + arg_query.add(EqualsOperator('noop', True)) + arg_query.add(EqualsOperator('noop_pending', True)) + status_query.add(arg_query) - try: - paging_args = {'limit': int(limit)} - paging_args['offset'] = int((page - 1) * paging_args['limit']) - except ValueError: - paging_args = {} + if len(status_query.operations) == 0: + if len(reports_query.operations) == 0: + reports_query = None + else: + reports_query.add(status_query) + + if status_args[0] != 'none': + reports = get_or_abort( + puppetdb.reports, + query=reports_query, + order_by=order_args, + include_total=True, + **paging_args) + reports, reports_events = tee(reports) + total = None + else: + reports = [] + reports_events = [] + total = 0 - reports = get_or_abort(puppetdb.reports, - query=reports_query, - order_by=DEFAULT_ORDER_BY, - **paging_args) - total = get_or_abort(puppetdb._query, - 'reports', - query=total_query) - total = total[0]['count'] - reports, reports_events = tee(reports) report_event_counts = {} - - if total == 0 and page != 1: - abort(404) - for report in reports_events: - report_event_counts[report.hash_] = {} + if total is None: + total = puppetdb.total - for event in report.events(): - if event.status == 'success': - try: - report_event_counts[report.hash_]['successes'] += 1 - except KeyError: - report_event_counts[report.hash_]['successes'] = 1 - elif event.status == 'failure': - try: - report_event_counts[report.hash_]['failures'] += 1 - except KeyError: - report_event_counts[report.hash_]['failures'] = 1 - elif event.status == 'noop': - try: - report_event_counts[report.hash_]['noops'] += 1 - except KeyError: - report_event_counts[report.hash_]['noops'] = 1 - elif event.status == 'skipped': - try: - report_event_counts[report.hash_]['skips'] += 1 - except KeyError: - report_event_counts[report.hash_]['skips'] = 1 - return Response(stream_with_context(stream_template( - 'reports.html', - reports=yield_or_stop(reports), - reports_count=app.config['REPORTS_COUNT'], + report_counts = {'successes': 0, 'failures': 0, 'skips': 0} + + if report.status != 'unchanged': + events_count = get_or_abort( + puppetdb.event_counts, summarize_by='certname', + query=EqualsOperator('report', report.hash_)) + + report_counts['successes'] = events_count[0].get('successes', None) + report_counts['failures'] = events_count[0].get('failures', None) + if report.status == 'noop': + report_counts['skips'] = events_count[0].get('noops', None) + else: + report_counts['skips'] = events_count[0].get('skips', None) + + report_event_counts[report.hash_] = report_counts + + if total is None: + total = 0 + + return render_template( + 'reports.json.tpl', + draw=draw, + total=total, + total_filtered=total, + reports=reports, report_event_counts=report_event_counts, - pagination=Pagination(page, paging_args.get('limit', total), total), envs=envs, current_env=env, - limit=paging_args.get('limit', total)))) - - -@app.route('/reports//', - defaults={'env': app.config['DEFAULT_ENVIRONMENT'], 'page': 1}) -@app.route('//reports/', defaults={'page': 1}) -@app.route('//reports//page/') -def reports_node(env, node_name, page): - """Fetches all reports for a node and processes them eventually rendering - a table displaying those reports. - - :param env: Search for reports in this environment - :type env: :obj:`string` - :param node_name: Find the reports whose certname match this value - :type node_name: :obj:`string` - :param page: Calculates the offset of the query based on the report count - and this value - :type page: :obj:`int` - """ - envs = environments() - status_arg = request.args.get('status') - check_env(env, envs) - query = AndOperator() - total_query = ExtractOperator() - - total_query.add_field(FunctionOperator("count")) - - if env == '*': - if status_arg in ['failed', 'changed', 'unchanged']: - query.add(EqualsOperator('status', status_arg)) - elif status_arg == 'noop': - query.add(EqualsOperator('noop', True)) - else: - query.add(EqualsOperator("environment", env)) - - if status_arg in ['failed', 'changed', 'unchanged']: - query.add(EqualsOperator('status', status_arg)) - query.add(EqualsOperator('noop', False)) - elif status_arg == 'noop': - query.add(EqualsOperator('noop', True)) - - query.add(EqualsOperator("certname", node_name)) - total_query.add_query(query) - - limit = request.args.get('limit', app.config['REPORTS_COUNT']) - - try: - paging_args = {'limit': int(limit)} - paging_args['offset'] = int((page - 1) * paging_args['limit']) - except ValueError: - paging_args = {} - - reports = get_or_abort(puppetdb.reports, - query=query, - order_by=DEFAULT_ORDER_BY, - **paging_args) - total = get_or_abort(puppetdb._query, - 'reports', - query=total_query) - total = total[0]['count'] - reports, reports_events = tee(reports) - report_event_counts = {} - - if total == 0 and page != 1: - abort(404) - - for report in reports_events: - report_event_counts[report.hash_] = {} - - for event in report.events(): - if event.status == 'success': - try: - report_event_counts[report.hash_]['successes'] += 1 - except KeyError: - report_event_counts[report.hash_]['successes'] = 1 - elif event.status == 'failure': - try: - report_event_counts[report.hash_]['failures'] += 1 - except KeyError: - report_event_counts[report.hash_]['failures'] = 1 - elif event.status == 'noop': - try: - report_event_counts[report.hash_]['noops'] += 1 - except KeyError: - report_event_counts[report.hash_]['noops'] = 1 - elif event.status == 'skipped': - try: - report_event_counts[report.hash_]['skips'] += 1 - except KeyError: - report_event_counts[report.hash_]['skips'] = 1 - return render_template( - 'reports.html', - reports=reports, - reports_count=app.config['REPORTS_COUNT'], - report_event_counts=report_event_counts, - pagination=Pagination(page, paging_args.get('limit', total), total), - envs=envs, - current_env=env) + columns=REPORTS_COLUMNS[:max_col]) @app.route('/report//', diff --git a/puppetboard/default_settings.py b/puppetboard/default_settings.py index 794ef94..9019488 100644 --- a/puppetboard/default_settings.py +++ b/puppetboard/default_settings.py @@ -15,7 +15,9 @@ UNRESPONSIVE_HOURS = 2 ENABLE_QUERY = True LOCALISE_TIMESTAMP = True LOGLEVEL = 'info' -REPORTS_COUNT = 10 +NORMAL_TABLE_COUNT = 100 +LITTLE_TABLE_COUNT = 10 +TABLE_COUNT_SELECTOR = [10, 20, 50, 100, 500] OFFLINE_MODE = False ENABLE_CATALOG = False OVERVIEW_FILTER = None diff --git a/puppetboard/static/coffeescript/tables.coffee b/puppetboard/static/coffeescript/tables.coffee deleted file mode 100644 index fb54613..0000000 --- a/puppetboard/static/coffeescript/tables.coffee +++ /dev/null @@ -1,24 +0,0 @@ -$ = jQuery -$ -> - -if $('th.default-sort').data() - $('table.sortable').tablesort().data('tablesort').sort($("th.default-sort"),"desc") - -$('thead th.date').data 'sortBy', (th, td, tablesort) -> - return moment.utc(td.text()).unix() - -$('input.filter-table').parent('div').removeClass('hide') -$("input.filter-table").on "keyup", (e) -> - rex = new RegExp($(this).val(), "i") - - $(".searchable tr").hide() - $(".searchable tr").filter( -> - rex.test $(this).text() - ).show() - - if e.keyCode is 27 - $(e.currentTarget).val "" - ev = $.Event("keyup") - ev.keyCode = 13 - $(e.currentTarget).trigger(ev) - e.currentTarget.blur() diff --git a/puppetboard/static/css/puppetboard.css b/puppetboard/static/css/puppetboard.css index bf2a823..4f7f478 100644 --- a/puppetboard/static/css/puppetboard.css +++ b/puppetboard/static/css/puppetboard.css @@ -13,20 +13,6 @@ h1.ui.header.no-margin-bottom { margin-bottom: 0; } -.tablesorter-header-inner { - float: left; -} - -th.tablesorter-headerAsc::after { - content: '\25b4' !important; - float: right; -} - -th.tablesorter-headerDesc::after { - content: '\25be' !important; - float: right; -} - .ui.grid.padding-bottom { padding-bottom: 40px !important; } diff --git a/puppetboard/static/jquery-datatables-1.10.13/dataTables.semanticui.min.css b/puppetboard/static/jquery-datatables-1.10.13/dataTables.semanticui.min.css new file mode 100644 index 0000000..c192a34 --- /dev/null +++ b/puppetboard/static/jquery-datatables-1.10.13/dataTables.semanticui.min.css @@ -0,0 +1 @@ +table.dataTable.table{margin:0}table.dataTable.table thead th,table.dataTable.table thead td{position:relative}table.dataTable.table thead th.sorting,table.dataTable.table thead th.sorting_asc,table.dataTable.table thead th.sorting_desc,table.dataTable.table thead td.sorting,table.dataTable.table thead td.sorting_asc,table.dataTable.table thead td.sorting_desc{padding-right:20px}table.dataTable.table thead th.sorting:after,table.dataTable.table thead th.sorting_asc:after,table.dataTable.table thead th.sorting_desc:after,table.dataTable.table thead td.sorting:after,table.dataTable.table thead td.sorting_asc:after,table.dataTable.table thead td.sorting_desc:after{position:absolute;top:12px;right:8px;display:block;font-family:Icons}table.dataTable.table thead th.sorting:after,table.dataTable.table thead td.sorting:after{content:"\f0dc";color:#ddd;font-size:0.8em}table.dataTable.table thead th.sorting_asc:after,table.dataTable.table thead td.sorting_asc:after{content:"\f0de"}table.dataTable.table thead th.sorting_desc:after,table.dataTable.table thead td.sorting_desc:after{content:"\f0dd"}table.dataTable.table td,table.dataTable.table th{-webkit-box-sizing:content-box;box-sizing:content-box}table.dataTable.table td.dataTables_empty,table.dataTable.table th.dataTables_empty{text-align:center}table.dataTable.table.nowrap th,table.dataTable.table.nowrap td{white-space:nowrap}div.dataTables_wrapper div.dataTables_length select{vertical-align:middle;min-height:2.7142em}div.dataTables_wrapper div.dataTables_length .ui.selection.dropdown{min-width:0}div.dataTables_wrapper div.dataTables_filter input{margin-left:0.5em}div.dataTables_wrapper div.dataTables_info{padding-top:13px;white-space:nowrap}div.dataTables_wrapper div.dataTables_processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;text-align:center}div.dataTables_wrapper div.row.dt-table{padding:0}div.dataTables_wrapper div.dataTables_scrollHead table.dataTable{border-bottom-right-radius:0;border-bottom-left-radius:0;border-bottom:none}div.dataTables_wrapper div.dataTables_scrollBody thead .sorting:after,div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_asc:after,div.dataTables_wrapper div.dataTables_scrollBody thead .sorting_desc:after{display:none}div.dataTables_wrapper div.dataTables_scrollBody table.dataTable{border-radius:0;border-top:none;border-bottom-width:0}div.dataTables_wrapper div.dataTables_scrollBody table.dataTable.no-footer{border-bottom-width:1px}div.dataTables_wrapper div.dataTables_scrollFoot table.dataTable{border-top-right-radius:0;border-top-left-radius:0;border-top:none} diff --git a/puppetboard/static/jquery-datatables-1.10.13/dataTables.semanticui.min.js b/puppetboard/static/jquery-datatables-1.10.13/dataTables.semanticui.min.js new file mode 100644 index 0000000..2e9fef5 --- /dev/null +++ b/puppetboard/static/jquery-datatables-1.10.13/dataTables.semanticui.min.js @@ -0,0 +1,9 @@ +/*! + DataTables Bootstrap 3 integration + ©2011-2015 SpryMedia Ltd - datatables.net/license +*/ +(function(b){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return b(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return b(d,a,a.document)}:b(jQuery,window,document)})(function(b,a,d,m){var e=b.fn.dataTable;b.extend(!0,e.defaults,{dom:"<'ui grid'<'row'<'eight wide column'l><'right aligned eight wide column'f>><'row dt-table'<'sixteen wide column'tr>><'row'<'seven wide column'i><'right aligned nine wide column'p>>>", +renderer:"semanticUI"});b.extend(e.ext.classes,{sWrapper:"dataTables_wrapper dt-semanticUI",sFilter:"dataTables_filter ui input",sProcessing:"dataTables_processing ui segment",sPageButton:"paginate_button item"});e.ext.renderer.pageButton.semanticUI=function(h,a,r,s,j,n){var o=new e.Api(h),t=h.oClasses,k=h.oLanguage.oPaginate,u=h.oLanguage.oAria.paginate||{},f,g,p=0,q=function(a,d){var e,i,l,c,m=function(a){a.preventDefault();!b(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&o.page(a.data.action).draw("page")}; +e=0;for(i=d.length;e",{"class":t.sPageButton+" "+g,id:0===r&&"string"===typeof c? +h.sTableId+"_"+c:null,href:"#","aria-controls":h.sTableId,"aria-label":u[c],"data-dt-idx":p,tabindex:h.iTabIndex}).html(f).appendTo(a),h.oApi._fnBindAction(l,{action:c},m),p++)}},i;try{i=b(a).find(d.activeElement).data("dt-idx")}catch(v){}q(b(a).empty().html('