diff --git a/dashed/__init__.py b/dashed/__init__.py index 83e6cce5b4..6dfc8b2aa3 100644 --- a/dashed/__init__.py +++ b/dashed/__init__.py @@ -28,7 +28,7 @@ migrate = Migrate(app, db, directory=APP_DIR + "/migrations") class MyIndexView(IndexView): @expose('/') def index(self): - return redirect('/dashed/featured') + return redirect('/dashed/welcome') appbuilder = AppBuilder( app, db.session, diff --git a/dashed/assets/.eslintrc b/dashed/assets/.eslintrc index 882ad973a8..c85071b2e7 100644 --- a/dashed/assets/.eslintrc +++ b/dashed/assets/.eslintrc @@ -57,7 +57,7 @@ }], "max-depth": [2, 5], "max-len": [0, 80, 4], - "max-nested-callbacks": [1, 2], + "max-nested-callbacks": [1, 3], "max-params": [1, 4], "new-parens": [2], "newline-after-var": [0], diff --git a/dashed/assets/javascripts/featured.js b/dashed/assets/javascripts/featured.js deleted file mode 100644 index 2b8617f3a8..0000000000 --- a/dashed/assets/javascripts/featured.js +++ /dev/null @@ -1,19 +0,0 @@ -var $ = window.$ = require('jquery'); -var jQuery = window.jQuery = $; -require('./modules/dashed.js'); - -require('bootstrap'); -require('datatables'); -require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css'); - -$(document).ready(function () { - $('#dataset-table').DataTable({ - bPaginate: false, - order: [ - [1, "asc"] - ] - }); - $('#dataset-table_info').remove(); - //$('input[type=search]').addClass('form-control'); # TODO get search box to look nice - $('#dataset-table').show(); -}); diff --git a/dashed/assets/javascripts/welcome.js b/dashed/assets/javascripts/welcome.js new file mode 100644 index 0000000000..6d0db5d80a --- /dev/null +++ b/dashed/assets/javascripts/welcome.js @@ -0,0 +1,48 @@ +var $ = window.$ = require('jquery'); +var jQuery = window.jQuery = $; + +require('../stylesheets/dashed.css'); +require('../stylesheets/welcome.css'); +require('bootstrap'); +require('datatables'); +require('../node_modules/cal-heatmap/cal-heatmap.css'); + +var CalHeatMap = require('cal-heatmap'); + +function modelViewTable(selector, modelEndpoint, ordering) { + // Builds a dataTable from a flask appbuilder api endpoint + $.getJSON(modelEndpoint + '/api/read', function (data) { + var tableData = jQuery.map(data.result, function (el, i) { + var row = $.map(data.list_columns, function (col, i) { + return el[col]; + }); + return [row]; + }); + var cols = jQuery.map(data.list_columns, function (col, i) { + return { sTitle: data.label_columns[col] }; + }); + $(selector).DataTable({ + aaData: tableData, + aoColumns: cols, + bPaginate: false, + order: ordering, + searching: false + }); + $('[data-toggle="tooltip"]').tooltip({ container: 'body' }); + }); +} + +$(document).ready(function () { + var cal = new CalHeatMap(); + cal.init({ + start: new Date().setFullYear(new Date().getFullYear() - 1), + range: 13, + data: '/dashed/activity_per_day', + domain: "month", + subDomain: "day", + itemName: "action", + tooltip: true + }); + modelViewTable('#dash_table', '/dashboardmodelviewasync'); + modelViewTable('#slice_table', '/sliceasync'); +}); diff --git a/dashed/assets/package.json b/dashed/assets/package.json index 49b3611363..4017399df2 100644 --- a/dashed/assets/package.json +++ b/dashed/assets/package.json @@ -43,6 +43,7 @@ "bootstrap-datepicker": "^1.6.0", "bootstrap-toggle": "^2.2.1", "brace": "^0.7.0", + "cal-heatmap": "3.5.4", "css-loader": "^0.23.1", "d3": "^3.5.14", "d3-cloud": "^1.2.1", diff --git a/dashed/assets/stylesheets/dashed.css b/dashed/assets/stylesheets/dashed.css index dd8d5cc578..10a5872064 100644 --- a/dashed/assets/stylesheets/dashed.css +++ b/dashed/assets/stylesheets/dashed.css @@ -9,6 +9,10 @@ body { font-size: 100%; } +.no-wrap { + white-space: nowrap; +} + input.form-control { background-color: white; } @@ -68,6 +72,10 @@ form div { } .navbar-brand a { color: white; + text-decoration: none; +} +.navbar-brand a:hover { + color: white; } .header span { diff --git a/dashed/assets/stylesheets/welcome.css b/dashed/assets/stylesheets/welcome.css new file mode 100644 index 0000000000..d8760f694a --- /dev/null +++ b/dashed/assets/stylesheets/welcome.css @@ -0,0 +1,21 @@ +.welcome .widget{ + border-radius: 0; + border: 1px solid #ccc; + box-shadow: 2px 1px 5px -2px #aaa; + background-color: #fff; +} + +.welcome .widget .header { + background-color: #f1f1f1; + text-align: center; +} + +.welcome .widget>div { + padding: 3px; + overflow: auto; + max-height: 500px; +} + +.table i { + padding-top: 6px; +} diff --git a/dashed/assets/webpack.config.js b/dashed/assets/webpack.config.js index 465a04e1c8..d9253f3d5c 100644 --- a/dashed/assets/webpack.config.js +++ b/dashed/assets/webpack.config.js @@ -8,7 +8,7 @@ var config = { 'css-theme': APP_DIR + '/javascripts/css-theme.js', dashboard: APP_DIR + '/javascripts/dashboard.js', explore: APP_DIR + '/javascripts/explore.js', - featured: APP_DIR + '/javascripts/featured.js', + welcome: APP_DIR + '/javascripts/welcome.js', sql: APP_DIR + '/javascripts/sql.js', standalone: APP_DIR + '/javascripts/standalone.js' }, diff --git a/dashed/migrations/versions/1d2ddd543133_log_dt.py b/dashed/migrations/versions/1d2ddd543133_log_dt.py new file mode 100644 index 0000000000..a5f50f4f64 --- /dev/null +++ b/dashed/migrations/versions/1d2ddd543133_log_dt.py @@ -0,0 +1,22 @@ +"""log dt + +Revision ID: 1d2ddd543133 +Revises: d2424a248d63 +Create Date: 2016-03-25 14:35:44.642576 + +""" + +# revision identifiers, used by Alembic. +revision = '1d2ddd543133' +down_revision = 'd2424a248d63' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('logs', sa.Column('dt', sa.Date(), nullable=True)) + + +def downgrade(): + op.drop_column('logs', 'dt') diff --git a/dashed/models.py b/dashed/models.py index db43ab9354..03a9e18629 100644 --- a/dashed/models.py +++ b/dashed/models.py @@ -2,7 +2,7 @@ from copy import deepcopy, copy from collections import namedtuple -from datetime import timedelta, datetime +from datetime import timedelta, datetime, date import functools import json import logging @@ -15,12 +15,13 @@ from flask import flash, request, g from flask.ext.appbuilder import Model from flask.ext.appbuilder.models.mixins import AuditMixin import pandas as pd +import humanize from pydruid import client from pydruid.utils.filters import Dimension, Filter import sqlalchemy as sqla from sqlalchemy import ( - Column, Integer, String, ForeignKey, Text, Boolean, DateTime, + Column, Integer, String, ForeignKey, Text, Boolean, DateTime, Date, Table, create_engine, MetaData, desc, select, and_, func) from sqlalchemy.engine import reflection from sqlalchemy.orm import relationship @@ -68,6 +69,22 @@ class AuditMixinNullable(AuditMixin): def changed_by_(self): return '{}'.format(self.changed_by or '') + @property + def modified(self): + s = humanize.naturaltime(datetime.now() - self.changed_on) + return '{}'.format(s) + + @property + def icons(self): + return """ + + + + """.format(**locals()) + class Url(Model, AuditMixinNullable): @@ -123,6 +140,13 @@ class Slice(Model, AuditMixinNullable): elif self.druid_datasource: return self.druid_datasource.link + @property + def datasource_edit_url(self): + if self.table: + return self.table.url + elif self.druid_datasource: + return self.druid_datasource.url + @property @utils.memoized def viz(self): @@ -1057,6 +1081,7 @@ class Log(Model): json = Column(Text) user = relationship('User', backref='logs', foreign_keys=[user_id]) dttm = Column(DateTime, default=func.now()) + dt = Column(Date, default=date.today()) @classmethod def log_this(cls, f): diff --git a/dashed/templates/dashed/featured.html b/dashed/templates/dashed/featured.html deleted file mode 100644 index 8b41f6c9bc..0000000000 --- a/dashed/templates/dashed/featured.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "dashed/basic.html" %} - -{% block head_js %} - {{ super() }} - -{% endblock %} - -{% block body %} -
-
-

Featured Datasets

-
-
- - - - - - - - - - - {% for dataset in featured_datasets %} - - - - - - - {% endfor %} - - -
-
-{% endblock %} - diff --git a/dashed/templates/dashed/welcome.html b/dashed/templates/dashed/welcome.html new file mode 100644 index 0000000000..6184a24815 --- /dev/null +++ b/dashed/templates/dashed/welcome.html @@ -0,0 +1,39 @@ +{% extends "dashed/basic.html" %} + +{% block head_js %} +{{ super() }} + +{% endblock %} + +{% block title %}Welcome!{% endblock %} + +{% block body %} +
+
+

Welcome!

+
+
+
+
+
+
+
+

Dashboards

+
+
+
+
+
+
+
+

Slices

+
+
+
+
+
+
+
+
+{% endblock %} + diff --git a/dashed/utils.py b/dashed/utils.py index 7ea42112d0..1a7f8f4579 100644 --- a/dashed/utils.py +++ b/dashed/utils.py @@ -183,7 +183,7 @@ def init(dashed): for perm in perms: if perm.permission.name == 'datasource_access': continue - if perm.view_menu.name not in ( + if perm.view_menu and perm.view_menu.name not in ( 'UserDBModelView', 'RoleModelView', 'ResetPasswordView', 'Security'): sm.add_permission_role(alpha, perm) @@ -191,7 +191,7 @@ def init(dashed): gamma = sm.add_role("Gamma") for perm in perms: if( - perm.view_menu.name not in ( + perm.view_menu and perm.view_menu.name not in ( 'ResetPasswordView', 'RoleModelView', 'UserDBModelView', diff --git a/dashed/views.py b/dashed/views.py index b43b43243a..723d2b53cf 100644 --- a/dashed/views.py +++ b/dashed/views.py @@ -4,6 +4,7 @@ from datetime import datetime import json import logging import re +import time import traceback from flask import ( @@ -67,8 +68,8 @@ class TableColumnInlineView(CompactCRUDMixin, DashedModelView): # noqa appbuilder.add_view_no_menu(TableColumnInlineView) appbuilder.add_link( - "Featured Datasets", - href='/dashed/featured', + "Welcome!", + href='/dashed/welcome', category='Sources', category_icon='fa-table', icon="fa-star") @@ -220,6 +221,10 @@ if config['DRUID_IS_ACTIVE']: class SliceModelView(DashedModelView, DeleteMixin): # noqa datamodel = SQLAInterface(models.Slice) can_add = False + label_columns = { + 'created_by_': 'Creator', + 'datasource_link': 'Datasource', + } list_columns = [ 'slice_link', 'viz_type', 'datasource_link', 'created_by_', 'changed_on'] @@ -236,7 +241,6 @@ class SliceModelView(DashedModelView, DeleteMixin): # noqa "markdown"), } - appbuilder.add_view( SliceModelView, "Slices", @@ -245,8 +249,19 @@ appbuilder.add_view( category_icon='',) +class SliceAsync(SliceModelView): # noqa + list_columns = [ + 'slice_link', 'viz_type', + 'created_by_', 'modified', 'icons'] + +appbuilder.add_view_no_menu(SliceAsync) + + class DashboardModelView(DashedModelView, DeleteMixin): # noqa datamodel = SQLAInterface(models.Dashboard) + label_columns = { + 'created_by_': 'Creator', + } list_columns = ['dashboard_link', 'created_by_', 'changed_on'] order_columns = utils.list_minus(list_columns, ['created_by_']) edit_columns = [ @@ -285,6 +300,12 @@ appbuilder.add_view( category_icon='',) +class DashboardModelViewAsync(DashboardModelView): # noqa + list_columns = ['dashboard_link', 'created_by_', 'modified'] + +appbuilder.add_view_no_menu(DashboardModelViewAsync) + + class LogModelView(DashedModelView): datamodel = SQLAInterface(models.Log) list_columns = ('user', 'action', 'dttm') @@ -525,6 +546,22 @@ class Dashed(BaseView): db.session.commit() return Response("OK", mimetype="application/json") + @has_access + @expose("/activity_per_day") + def activity_per_day(self): + """endpoint to power the calendar heatmap on the welcome page""" + Log = models.Log # noqa + qry = ( + db.session + .query( + Log.dt, + sqla.func.count()) + .group_by(Log.dt) + .all() + ) + payload = {str(time.mktime(dt.timetuple())): ccount for dt, ccount in qry if dt} + return Response(json.dumps(payload), mimetype="application/json") + @has_access @expose("/save_dash//", methods=['GET', 'POST']) def save_dash(self, dashboard_id): @@ -760,25 +797,11 @@ class Dashed(BaseView): art=ascii_art.error), 500 @has_access - @expose("/featured", methods=['GET']) - def featured(self): - """views that shows the Featured Datasets""" - session = db.session() - datasets_sqla = ( - session.query(models.SqlaTable) - .filter_by(is_featured=True) - .all() - ) - datasets_druid = ( - session.query(models.DruidDatasource) - .filter_by(is_featured=True) - .all() - ) - featured_datasets = datasets_sqla + datasets_druid - return self.render_template( - 'dashed/featured.html', - featured_datasets=featured_datasets, - utils=utils) + @expose("/welcome") + def welcome(self): + """Personalized welcome page""" + return self.render_template('dashed/welcome.html', utils=utils) + appbuilder.add_view_no_menu(Dashed) diff --git a/setup.py b/setup.py index a4b0cb3071..8a014af816 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ setup( 'flask-script>=2.0.5, <3.0.0', 'flask-testing>=0.4.2, <0.5.0', 'flask>=0.10.1, <1.0.0', + 'humanize>=0.5.1, <0.6.0', 'gunicorn>=19.3.0, <20.0.0', 'markdown>=2.6.2, <3.0.0', 'numpy>=1.9, <2',