diff --git a/rd_ui/app/scripts/controllers/admin_controllers.js b/rd_ui/app/scripts/controllers/admin_controllers.js index dd8c11cea9c..ec7cead6690 100644 --- a/rd_ui/app/scripts/controllers/admin_controllers.js +++ b/rd_ui/app/scripts/controllers/admin_controllers.js @@ -1,4 +1,11 @@ (function() { + var dateFormatter = function(date) { + value = moment(date); + if (!value) return "-"; + return value.format("DD/MM/YY HH:mm"); + } + + var AdminStatusCtrl = function($scope, Events, $http, $timeout) { Events.record(currentUser, "view", "page", "admin/status"); $scope.$parent.pageTitle = "System Status"; @@ -118,10 +125,37 @@ // tables $scope.groups = groups; + + user.queries().$promise.then(function(result){ + $scope.queries = result; + }); + + user.dashboards().$promise.then(function(result){ + $scope.dashboards = result; + }) + }) } } + $scope.queryColumns = [ + {label: "Name", map: "name"}, + {label: "Created At", map: "created_at", formatFunction: dateFormatter}, + {label: "Actions", cellTemplateUrl: "/views/admin_user_form_query_actions_cell.html"} + ]; + $scope.dashboardColumns = [ + {label: "Name", map: "name"}, + {label: "Created At", map: "created_at", formatFunction: dateFormatter}, + {label: "Actions", cellTemplateUrl: "/views/admin_user_form_dashboard_actions_cell.html"} + ]; + + $scope.tableConfig = { + isPaginationEnabled: true, + itemsByPage: 50, + maxSize: 8, + isGlobalSearchActivated: false + } + var groups = new Groups(); groups.get().$promise.then(function(result) { @@ -225,12 +259,6 @@ admin_users: false }; - var dateFormatter = function(date) { - value = moment(date); - if (!value) return "-"; - return value.format("DD/MM/YY HH:mm"); - } - var permissionsFormatter = function(permissions) { value = permissions.join(', '); if (!value) return "-"; @@ -514,12 +542,6 @@ $location.path("/"); } - var dateFormatter = function(date) { - value = moment(date); - if (!value) return "-"; - return value.format("DD/MM/YY HH:mm"); - } - var permissionsFormatter = function(permissions) { value = permissions.join(', '); if (!value) return "-"; diff --git a/rd_ui/app/scripts/services/resources.js b/rd_ui/app/scripts/services/resources.js index 1dc9fba25d3..9e355209d8a 100644 --- a/rd_ui/app/scripts/services/resources.js +++ b/rd_ui/app/scripts/services/resources.js @@ -449,6 +449,12 @@ User.new = function (data) { return new User(data); }; + User.prototype.queries = function(){ + return $resource('/api/users/:id/queries', {id: '@id'}).query({id: this.id}); + } + User.prototype.dashboards = function(){ + return $resource('/api/users/:id/dashboards', {id: '@id'}).query({id: this.id}); + } return User; } @@ -481,7 +487,7 @@ .factory('Group', ['$resource', Group]) .factory('Users', ['$resource', Users]) .factory('User', ['$resource', User]) - .factory('Widget', ['$resource', 'Query', Widget]) + .factory('Widget', ['$resource', 'Query', Widget]) .factory('Table', ['$resource', Table]); })(); diff --git a/rd_ui/app/views/admin_user_form.html b/rd_ui/app/views/admin_user_form.html index 093b7fbb470..2771e772775 100644 --- a/rd_ui/app/views/admin_user_form.html +++ b/rd_ui/app/views/admin_user_form.html @@ -2,7 +2,7 @@

Edit User

-
+
@@ -33,5 +33,17 @@

Edit User

+
+
+

{{name}}'s queries

+ +
+
+

{{name}}'s dashboards

+ +
+
+ + -
\ No newline at end of file +
diff --git a/rd_ui/app/views/admin_user_form_dashboard_actions_cell.html b/rd_ui/app/views/admin_user_form_dashboard_actions_cell.html new file mode 100644 index 00000000000..c3164886cc2 --- /dev/null +++ b/rd_ui/app/views/admin_user_form_dashboard_actions_cell.html @@ -0,0 +1 @@ +View \ No newline at end of file diff --git a/rd_ui/app/views/admin_user_form_query_actions_cell.html b/rd_ui/app/views/admin_user_form_query_actions_cell.html new file mode 100644 index 00000000000..e999415618f --- /dev/null +++ b/rd_ui/app/views/admin_user_form_query_actions_cell.html @@ -0,0 +1 @@ +View \ No newline at end of file diff --git a/redash/controllers.py b/redash/controllers.py index b8a38395ed9..97235535f70 100644 --- a/redash/controllers.py +++ b/redash/controllers.py @@ -28,12 +28,12 @@ import logging + @app.route('/ping', methods=['GET']) def ping(): return 'PONG.' - @app.route('/admin/') @app.route('/admin//') @app.route('/dashboard/') @@ -41,7 +41,6 @@ def ping(): @app.route('/queries/') @app.route('/queries//') @app.route('/') - @auth.required def index(**kwargs): email_md5 = hashlib.md5(current_user.email.lower()).hexdigest() @@ -65,7 +64,6 @@ def index(**kwargs): analytics=settings.ANALYTICS) - # @app.route('/admin/groups/') # def admin_group(): @@ -86,6 +84,7 @@ def logout(): return redirect('/login') + @app.route('/status.json') @auth.required @require_permission('admin') @@ -136,12 +135,12 @@ def dispatch_request(self, *args, **kwargs): class TableAPI(BaseResource): - @require_permission('admin_groups') def get(self): source = models.DataSource.select().where(models.DataSource.type == "pg")[0] qr = data.query_runner.get_query_runner(source.type, source.options) - tablenames = qr("SELECT table_name FROM information_schema.tables WHERE table_schema='public' ORDER BY table_name") + tablenames = qr( + "SELECT table_name FROM information_schema.tables WHERE table_schema='public' ORDER BY table_name") result = {} result["tablenames"] = [t["table_name"] for t in json.loads(tablenames[0])["rows"]] return result @@ -149,8 +148,8 @@ def get(self): api.add_resource(TableAPI, '/api/tables', endpoint='tables') -class GroupListAPI(BaseResource): +class GroupListAPI(BaseResource): @require_permission('admin_groups') def get(self): groups = [g.to_dict() for g in models.Group.select()] @@ -165,14 +164,13 @@ def post(self): class GroupAPI(BaseResource): - @require_permission('admin_groups') def get(self, group_id): try: g = models.Group.get(models.Group.id == group_id) except models.Group.DoesNotExist: abort(404, message="Group not found.") - + return g.to_dict() @require_permission('admin_groups') @@ -190,8 +188,8 @@ def post(self, group_id): return g.to_dict() -class UserListAPI(BaseResource): +class UserListAPI(BaseResource): @require_permission('admin_users') def get(self): users = [u.to_dict() for u in models.User.select()] @@ -206,8 +204,6 @@ def post(self): class UserAPI(BaseResource): - - @require_permission('admin_users') def get(self, user_id): try: @@ -237,14 +233,27 @@ def post(self, user_id): u.groups = json["groups"] u.save() - return u.to_dict() + return u.to_dict() +class UserQueriesAPI(BaseResource): + def get(self, user_id): + user = models.User.get_by_id(user_id) + queries = [q.to_dict(with_result=False) for q in user.queries()] + return queries +class UserDashboardsAPI(BaseResource): + def get(self, user_id): + user = models.User.get_by_id(user_id) + dashboards = [d.to_dict() for d in user.dashboards()] + return dashboards + api.add_resource(UserListAPI, '/api/users', endpoint='users') -api.add_resource(UserAPI, '/api/users/', endpoint='user') +api.add_resource(UserAPI, '/api/users/', endpoint='user') +api.add_resource(UserQueriesAPI, '/api/users//queries', endpoint='user_queries') +api.add_resource(UserDashboardsAPI, '/api/users//dashboards', endpoint='user_dashboards') api.add_resource(GroupListAPI, '/api/groups', endpoint='groups') api.add_resource(GroupAPI, '/api/groups/', endpoint='group') @@ -267,6 +276,7 @@ def post(self): return "OK." + api.add_resource(MetricsAPI, '/api/metrics/v1/send', endpoint='metrics') @@ -275,13 +285,14 @@ def get(self): data_sources = [ds.to_dict() for ds in models.DataSource.select()] return data_sources + api.add_resource(DataSourceListAPI, '/api/data_sources', endpoint='data_sources') class DashboardListAPI(BaseResource): def get(self): dashboards = [d.to_dict() for d in - models.Dashboard.select().where(models.Dashboard.is_archived==False)] + models.Dashboard.select().where(models.Dashboard.is_archived == False)] return dashboards @@ -321,6 +332,7 @@ def delete(self, dashboard_slug): dashboard.is_archived = True dashboard.save() + api.add_resource(DashboardListAPI, '/api/dashboards', endpoint='dashboards') api.add_resource(DashboardAPI, '/api/dashboards/', endpoint='dashboard') @@ -356,6 +368,7 @@ def post(self): return {'widget': widget.to_dict(), 'layout': layout, 'new_row': new_row} + class WidgetCheckApi(BaseResource): def get(self, query_id): try: @@ -367,6 +380,7 @@ def get(self, query_id): api.add_resource(WidgetCheckApi, '/api/widget_check/', endpoint='widget_check') + class WidgetAPI(BaseResource): @require_permission('edit_dashboard') def delete(self, widget_id): @@ -380,6 +394,7 @@ def delete(self, widget_id): widget.delete_instance() + api.add_resource(WidgetListAPI, '/api/widgets', endpoint='widgets') api.add_resource(WidgetAPI, '/api/widgets/', endpoint='widget') @@ -402,19 +417,18 @@ def post(self): @require_permission('view_query') def get(self): - - queries = [q.to_dict(with_result=False, with_stats=True)for q in models.Query.all_queries().where(models.Query.is_archived==False)] + queries = [q.to_dict(with_result=False, with_stats=True) for q in + models.Query.all_queries().where(models.Query.is_archived == False)] return queries - class QueryAPI(BaseResource): @require_permission('edit_query') - def post(self, query_id): + def post(self, query_id): query_def = request.get_json(force=True) for field in ['id', 'created_at', 'api_key', 'visualizations', 'latest_query_data']: query_def.pop(field, None) - + if 'user' in query_def: user = query_def.pop('user') query_def['user'] = models.User.get_by_id(user["id"]) @@ -432,7 +446,7 @@ def post(self, query_id): return query.to_dict(with_result=False, with_visualizations=True) @require_permission('view_query') - def get(self, query_id): + def get(self, query_id): q = models.Query.get(models.Query.id == query_id) if q: return q.to_dict(with_visualizations=True) @@ -483,6 +497,7 @@ def delete(self, visualization_id): vis = models.Visualization.get(models.Visualization.id == visualization_id) vis.delete_instance() + api.add_resource(VisualizationListAPI, '/api/visualizations', endpoint='visualizations') api.add_resource(VisualizationAPI, '/api/visualizations/', endpoint='visualization') @@ -503,13 +518,14 @@ def post(self): } if len(metadata.used_tables - current_user.allowed_tables) > 0 and '*' not in current_user.allowed_tables: - logging.warning('Permission denied for user %s to table %s', self.current_user.name, metadata.used_tables) + logging.warning('Permission denied for user %s to table %s', self.current_user.name, + metadata.used_tables) return { 'job': { 'error': 'Access denied for table(s): %s' % (metadata.used_tables) } } - + models.ActivityLog( user=self.current_user, type=models.ActivityLog.QUERY_EXECUTION, @@ -538,18 +554,16 @@ def get(self, query_result_id): else: abort(404) -def from_utc(utcTime,fmt="%Y-%m-%dT%H:%M:%S"): + +def from_utc(utcTime, fmt="%Y-%m-%dT%H:%M:%S"): """ Convert UTC time string to time.struct_time """ return datetime.datetime.strptime(utcTime, fmt) + class CsvQueryResultsAPI(BaseResource): @require_permission('view_query') - - - - def get(self, query_id, query_result_id=None): if not query_result_id: query = models.Query.get(models.Query.id == query_id) @@ -566,9 +580,9 @@ def get(self, query_id, query_result_id=None): writer.writeheader() for row in query_data['rows']: for k, v in row.iteritems(): - try: - date = from_utc(v) - row[k] = date + try: + date = from_utc(v) + row[k] = date except: False writer.writerow(row) @@ -577,6 +591,7 @@ def get(self, query_id, query_result_id=None): else: abort(404) + api.add_resource(CsvQueryResultsAPI, '/api/queries//results/.csv', '/api/queries//results.csv', endpoint='csv_query_results') @@ -594,8 +609,10 @@ def delete(self, job_id): job = data.Job.load(data_manager.redis_connection, job_id) job.cancel() + api.add_resource(JobAPI, '/api/jobs/', endpoint='job') + @app.route('/') def send_static(filename): return send_from_directory(settings.STATIC_ASSETS_PATH, filename) @@ -603,6 +620,3 @@ def send_static(filename): if __name__ == '__main__': app.run(debug=True) - - - diff --git a/redash/models.py b/redash/models.py index a5472405307..41987c320ea 100644 --- a/redash/models.py +++ b/redash/models.py @@ -31,8 +31,8 @@ def __init__(self, api_key): def permissions(self): return ['view_query'] -class Table(BaseModel): +class Table(BaseModel): class Meta: db_table = 'pg_tables' @@ -43,10 +43,11 @@ def to_dict(self): 'tablename': self.tablename, } + class Group(BaseModel): DEFAULT_PERMISSIONS = ['create_dashboard', 'create_query', 'edit_dashboard', 'edit_query', 'view_query', 'view_source', 'execute_query'] - + id = peewee.PrimaryKeyField() name = peewee.CharField(max_length=100) permissions = ArrayField(peewee.CharField, default=DEFAULT_PERMISSIONS) @@ -102,10 +103,17 @@ def allowed_tables(self): # TODO: cache this as weel if self._allowed_tables is None: self._allowed_tables = set([t.lower() for t in itertools.chain(*[g.tables for g in - Group.select().where(Group.name << self.groups)])]) + Group.select().where( + Group.name << self.groups)])]) return self._allowed_tables + def queries(self): + return Query.select().where(Query.user == self) + + def dashboards(self): + return Dashboard.select().where(Dashboard.user == self) + def __unicode__(self): return '%r, %r' % (self.name, self.email) @@ -118,9 +126,10 @@ def verify_password(self, password): def is_active(self): return True + class ActivityLog(BaseModel): QUERY_EXECUTION = 1 - + id = peewee.PrimaryKeyField() user = peewee.ForeignKeyField(User) type = peewee.IntegerField() @@ -142,6 +151,7 @@ def to_dict(self): def __unicode__(self): return unicode(self.id) + class DataSource(BaseModel): id = peewee.PrimaryKeyField() name = peewee.CharField() @@ -221,7 +231,7 @@ class Query(BaseModel): ttl = peewee.IntegerField() user_email = peewee.CharField(max_length=360, null=True) user = peewee.ForeignKeyField(User) - created_at = peewee.DateTimeField(default=datetime.datetime.now) + created_at = peewee.DateTimeField(default=datetime.datetime.now) is_archived = peewee.BooleanField(default=False, index=True) class Meta: @@ -241,7 +251,7 @@ def to_dict(self, with_result=True, with_stats=False, with_visualizations=False, 'description': self.description, 'query': self.query, 'query_hash': self.query_hash, - 'ttl': self.ttl, + 'ttl': self.ttl, 'api_key': self.api_key, 'created_at': self.created_at, 'data_source_id': self._data.get('data_source', None), @@ -272,13 +282,13 @@ def to_dict(self, with_result=True, with_stats=False, with_visualizations=False, @classmethod def all_queries(cls): q = Query.select(Query, User, - peewee.fn.Count(QueryResult.id).alias('times_retrieved'), - peewee.fn.Avg(QueryResult.runtime).alias('avg_runtime'), - peewee.fn.Min(QueryResult.runtime).alias('min_runtime'), - peewee.fn.Max(QueryResult.runtime).alias('max_runtime'), - peewee.fn.Max(QueryResult.retrieved_at).alias('last_retrieved_at'))\ - .join(QueryResult, join_type=peewee.JOIN_LEFT_OUTER)\ - .switch(Query).join(User)\ + peewee.fn.Count(QueryResult.id).alias('times_retrieved'), + peewee.fn.Avg(QueryResult.runtime).alias('avg_runtime'), + peewee.fn.Min(QueryResult.runtime).alias('min_runtime'), + peewee.fn.Max(QueryResult.runtime).alias('max_runtime'), + peewee.fn.Max(QueryResult.retrieved_at).alias('last_retrieved_at')) \ + .join(QueryResult, join_type=peewee.JOIN_LEFT_OUTER) \ + .switch(Query).join(User) \ .group_by(Query.id, User.id) return q @@ -299,7 +309,8 @@ def save(self, *args, **kwargs): def _set_api_key(self): if not self.api_key: self.api_key = hashlib.sha1( - u''.join((str(time.time()), self.query, str(self._data['user']), self.name)).encode('utf-8')).hexdigest() + u''.join((str(time.time()), self.query, str(self._data['user']), self.name)).encode( + 'utf-8')).hexdigest() def __unicode__(self): return unicode(self.id) @@ -323,12 +334,12 @@ def to_dict(self, with_widgets=False): layout = json.loads(self.layout) if with_widgets: - widgets = Widget.select(Widget, Visualization, Query, QueryResult, User)\ - .where(Widget.dashboard == self.id)\ - .join(Visualization, join_type=peewee.JOIN_LEFT_OUTER)\ - .join(Query, join_type=peewee.JOIN_LEFT_OUTER)\ - .join(User, join_type=peewee.JOIN_LEFT_OUTER)\ - .switch(Query)\ + widgets = Widget.select(Widget, Visualization, Query, QueryResult, User) \ + .where(Widget.dashboard == self.id) \ + .join(Visualization, join_type=peewee.JOIN_LEFT_OUTER) \ + .join(Query, join_type=peewee.JOIN_LEFT_OUTER) \ + .join(User, join_type=peewee.JOIN_LEFT_OUTER) \ + .switch(Query) \ .join(QueryResult, join_type=peewee.JOIN_LEFT_OUTER) widgets = {w.id: w.to_dict() for w in widgets} @@ -347,7 +358,7 @@ def to_dict(self, with_widgets=False): widgets_layout.append(new_row) - # widgets_layout = map(lambda row: map(lambda widget_id: widgets.get(widget_id, None), row), layout) + # widgets_layout = map(lambda row: map(lambda widget_id: widgets.get(widget_id, None), row), layout) else: widgets_layout = None @@ -442,6 +453,7 @@ def to_dict(self): def __unicode__(self): return u"%s" % self.id + all_models = (DataSource, User, QueryResult, Query, Dashboard, Visualization, Widget, ActivityLog, Group) @@ -457,9 +469,9 @@ def create_db(create_tables, drop_tables): if drop_tables and model.table_exists(): # TODO: submit PR to peewee to allow passing cascade option to drop_table. db.database.execute_sql('DROP TABLE %s CASCADE' % model._meta.db_table) - #model.drop_table() + # model.drop_table() if create_tables and not model.table_exists(): model.create_table() - db.close_db(None) \ No newline at end of file + db.close_db(None)