[#138] editCtrl inheriting viewCtrl

This commit is contained in:
Amir Nissim 2014-03-13 16:25:23 +02:00
parent 2ba4bcd98e
commit af9318fbd1
6 changed files with 333 additions and 198 deletions

View File

@ -121,6 +121,7 @@
<script src="/scripts/controllers/controllers.js"></script> <script src="/scripts/controllers/controllers.js"></script>
<script src="/scripts/controllers/admin_controllers.js"></script> <script src="/scripts/controllers/admin_controllers.js"></script>
<script src="/scripts/controllers/query_view.js"></script> <script src="/scripts/controllers/query_view.js"></script>
<script src="/scripts/controllers/query_edit.js"></script>
<script src="/scripts/visualizations/base.js"></script> <script src="/scripts/visualizations/base.js"></script>
<script src="/scripts/visualizations/chart.js"></script> <script src="/scripts/visualizations/chart.js"></script>
<script src="/scripts/visualizations/cohort.js"></script> <script src="/scripts/visualizations/cohort.js"></script>

View File

@ -42,35 +42,25 @@ angular.module('redash', [
controller: 'QueriesCtrl', controller: 'QueriesCtrl',
reloadOnSearch: false reloadOnSearch: false
}); });
$routeProvider.when('/queries/new', {
templateUrl: '/views/queryview.html',
controller: 'QueryViewCtrl',
reloadOnSearch: false,
resolve: {
'viewSource': function isViewSource() {
return true;
}
}
});
// TODO
// we should have 2 controllers: queryViewCtrl and queryEditCtrl
$routeProvider.when('/queries/:queryId', { $routeProvider.when('/queries/:queryId', {
templateUrl: '/views/queryview.html', templateUrl: '/views/query.html',
controller: 'QueryViewCtrl', controller: 'QueryViewCtrl',
reloadOnSearch: false, reloadOnSearch: false,
resolve: { resolve: {
'query': ['Query', '$q', '$route', getQuery] 'query': ['Query', '$q', '$route', getQuery]
} }
}); });
$routeProvider.when('/queries/new', {
templateUrl: '/views/query.html',
controller: 'QueryEditCtrl',
reloadOnSearch: false
});
$routeProvider.when('/queries/:queryId/source', { $routeProvider.when('/queries/:queryId/source', {
templateUrl: '/views/queryview.html', templateUrl: '/views/query.html',
controller: 'QueryViewCtrl', controller: 'QueryEditCtrl',
reloadOnSearch: false, reloadOnSearch: false,
resolve: { resolve: {
'query': ['Query', '$q', '$route', getQuery], 'query': ['Query', '$q', '$route', getQuery]
'viewSource': function isViewSource() {
return true;
}
} }
}); });
$routeProvider.when('/admin/status', { $routeProvider.when('/admin/status', {

View File

@ -0,0 +1,122 @@
(function () {
'use strict';
function QueryEditCtrl($controller, $scope, $window, $route, $http, $location, growl, notifications, Query, Visualization) {
var pristineHash = "";
var leavingPageText = "You will lose your changes if you leave";
$controller('QueryViewCtrl', {$scope: $scope});
$scope.dirty = undefined;
$scope.isNewQuery = false;
$scope.isSourceVisible = true;
$scope.newVisualization = undefined;
$window.onbeforeunload = function () {
if ($scope.canEdit && $scope.dirty) {
return leavingPageText;
}
}
function getQuerySourceUrl(queryId) {
return '/queries/' + queryId + '/source#' + $location.hash();
};
Mousetrap.bindGlobal("meta+s", function (e) {
e.preventDefault();
if ($scope.canEdit) {
$scope.saveQuery();
}
});
$scope.$on('$locationChangeStart', function (event, next, current) {
if (next.split("#")[0] == current.split("#")[0]) {
return;
}
if (!$scope.canEdit) {
return;
}
if ($scope.dirty && !confirm(leavingPageText + "\n\nAre you sure you want to leave this page?")) {
event.preventDefault();
} else {
Mousetrap.unbind("meta+s");
}
});
$scope.saveQuery = function (duplicate, oldId) {
if (!oldId) {
oldId = $scope.query.id;
}
delete $scope.query.latest_query_data;
$scope.query.$save(function (q) {
pristineHash = q.getHash();
$scope.dirty = false;
if (duplicate) {
growl.addSuccessMessage("Query forked");
} else {
growl.addSuccessMessage("Query saved");
}
if (oldId != q.id) {
$location.url(getQuerySourceUrl(q.id)).replace();
}
}, function (httpResponse) {
growl.addErrorMessage("Query could not be saved");
});
};
// new query
if (!$route.current.locals.query) {
$scope.query = new Query({
query: "",
name: "New Query",
ttl: -1,
user: currentUser
});
$scope.lockButton(false);
$scope.isOwner = $scope.canEdit = true;
$scope.isNewQuery = true;
var unbind = $scope.$watch('selectedTab == "add"', function (newPanel) {
if (newPanel && route.params.queryId == undefined) {
unbind();
$scope.saveQuery();
}
});
}
$scope.$watch(function () {
return $scope.query.getHash();
}, function (newHash) {
$scope.dirty = (newHash !== pristineHash);
});
$scope.deleteVisualization = function ($e, vis) {
$e.preventDefault();
if (confirm('Are you sure you want to delete ' + vis.name + ' ?')) {
Visualization.delete(vis);
if ($scope.selectedTab == vis.id) {
$scope.selectedTab = DEFAULT_TAB;
$location.hash($scope.selectedTab);
}
$scope.query.visualizations =
$scope.query.visualizations.filter(function (v) {
return vis.id !== v.id;
});
}
};
};
angular.module('redash.controllers')
.controller('QueryEditCtrl', ['$controller', '$scope', '$window', '$route', '$http', '$location', 'growl', 'notifications', 'Query', 'Visualization', QueryEditCtrl]);
})();

View File

@ -1,78 +1,24 @@
(function () { (function () {
'use strict'; 'use strict';
var QueryViewCtrl = function ($scope, $window, $route, $http, $location, growl, notifications, Query, Visualization, DataSource) { function QueryViewCtrl($scope, $window, $route, $http, $location, growl, notifications, Query, Visualization) {
var DEFAULT_TAB = 'table'; var DEFAULT_TAB = 'table';
var pristineHash = "";
var leavingPageText = "You will lose your changes if you leave";
var route = $route.current; var route = $route.current;
$scope.dataSources = DataSource.get(function(dataSources) { $scope.query = route.locals.query;
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].id; $scope.queryResult = $scope.query.getQueryResult();
});
$scope.dirty = undefined;
$scope.isNewQuery = false;
$scope.isOwner = false;
$scope.canEdit = false;
$scope.canFork = false;
$scope.isSourceVisible = route.locals.viewSource;
$scope.queryExecuting = false; $scope.queryExecuting = false;
$scope.newVisualization = undefined; $scope.isOwner = currentUser.canEdit($scope.query);
$scope.canEdit = $scope.isSourceVisible && $scope.isOwner;
$scope.canFork = true;
updateSourceHref(); $scope.sourceHref = getQuerySourceUrl();
$window.onbeforeunload = function () {
if ($scope.canEdit && $scope.dirty) {
return leavingPageText;
}
}
function updateSourceHref() {
if ($scope.query && $scope.query.id) {
$scope.sourceHref = $scope.isSourceVisible ?
$location.url().replace('source', '') : getQuerySourceUrl($scope.query.id);
}
};
function getQuerySourceUrl(queryId) { function getQuerySourceUrl(queryId) {
return '/queries/' + queryId + '/source#' + $location.hash(); return '/queries/' + $scope.query.id + '/source#' + $location.hash();
}; };
Mousetrap.bindGlobal("meta+s", function (e) {
e.preventDefault();
if ($scope.canEdit) {
$scope.saveQuery();
}
});
$scope.$on('$locationChangeStart', function (event, next, current) {
if (next.split("#")[0] == current.split("#")[0]) {
return;
}
if (!$scope.canEdit) {
return;
}
if ($scope.dirty && !confirm(leavingPageText + "\n\nAre you sure you want to leave this page?")) {
event.preventDefault();
} else {
Mousetrap.unbind("meta+s");
}
});
$scope.$watch(function () {
return $location.hash()
}, function (hash) {
$scope.selectedTab = hash || DEFAULT_TAB;
updateSourceHref();
});
$scope.lockButton = function (lock) { $scope.lockButton = function (lock) {
$scope.queryExecuting = lock; $scope.queryExecuting = lock;
}; };
@ -88,31 +34,6 @@
}) })
} }
$scope.saveQuery = function (duplicate, oldId) {
if (!oldId) {
oldId = $scope.query.id;
}
delete $scope.query.latest_query_data;
$scope.query.$save(function (q) {
pristineHash = q.getHash();
$scope.dirty = false;
if (duplicate) {
growl.addSuccessMessage("Query forked");
} else {
growl.addSuccessMessage("Query saved");
}
if (oldId != q.id) {
$location.url(getQuerySourceUrl(q.id)).replace();
}
}, function (httpResponse) {
growl.addErrorMessage("Query could not be saved");
});
};
$scope.duplicateQuery = function () { $scope.duplicateQuery = function () {
var oldId = $scope.query.id; var oldId = $scope.query.id;
$scope.query.id = null; $scope.query.id = null;
@ -121,7 +42,17 @@
$scope.saveQuery(true, oldId); $scope.saveQuery(true, oldId);
}; };
// Query Editor: $scope.executeQuery = function () {
$scope.queryResult = $scope.query.getQueryResult(0);
$scope.lockButton(true);
$scope.cancelling = false;
};
$scope.cancelExecution = function () {
$scope.cancelling = true;
$scope.queryResult.cancelExecution();
};
$scope.editorOptions = { $scope.editorOptions = {
mode: 'text/x-sql', mode: 'text/x-sql',
lineWrapping: true, lineWrapping: true,
@ -204,100 +135,17 @@
} }
}); });
// view or source pages: controller is instantiated with a query
if (route.locals.query) {
$scope.query = route.locals.query;
pristineHash = $scope.query.getHash();
$scope.dirty = false;
$scope.queryResult = $scope.query.getQueryResult();
$scope.isOwner = currentUser.canEdit($scope.query);
$scope.canEdit = $scope.isSourceVisible && $scope.isOwner;
$scope.canFork = true;
} else {
// new query
$scope.query = new Query({
query: "",
name: "New Query",
ttl: -1,
user: currentUser
});
$scope.lockButton(false);
$scope.isOwner = $scope.canEdit = true;
$scope.isNewQuery = true;
}
$scope.$watch('query.name', function () { $scope.$watch('query.name', function () {
$scope.$parent.pageTitle = $scope.query.name; $scope.$parent.pageTitle = $scope.query.name;
}); });
$scope.$watch(function () { $scope.$watch(function () {
return $scope.query.getHash(); return $location.hash()
}, function (newHash) { }, function (hash) {
$scope.dirty = (newHash !== pristineHash); $scope.selectedTab = hash || DEFAULT_TAB;
}); $scope.sourceHref = getQuerySourceUrl();
$scope.updateDataSource = function() {
$scope.query.latest_query_data = null;
$scope.query.latest_query_data_id = null;
Query.save({
'id': $scope.query.id,
'data_source_id': $scope.query.data_source_id,
'latest_query_data_id': null
});
$scope.executeQuery();
};
$scope.executeQuery = function () {
$scope.queryResult = $scope.query.getQueryResult(0);
$scope.lockButton(true);
$scope.cancelling = false;
};
$scope.cancelExecution = function () {
$scope.cancelling = true;
$scope.queryResult.cancelExecution();
};
$scope.deleteVisualization = function ($e, vis) {
$e.preventDefault();
if (confirm('Are you sure you want to delete ' + vis.name + ' ?')) {
Visualization.delete(vis);
if ($scope.selectedTab == vis.id) {
$scope.selectedTab = DEFAULT_TAB;
$location.hash($scope.selectedTab);
}
$scope.query.visualizations =
$scope.query.visualizations.filter(function (v) {
return vis.id !== v.id;
});
}
};
var unbind = $scope.$watch('selectedTab == "add"', function (newPanel) {
if (newPanel && route.params.queryId == undefined) {
unbind();
$scope.saveQuery();
}
}); });
}; };
angular.module('redash.controllers').controller('QueryViewCtrl', angular.module('redash.controllers')
[ .controller('QueryViewCtrl', ['$scope', '$window', '$route', '$http', '$location', 'growl', 'notifications', 'Query', 'Visualization', QueryViewCtrl]);
'$scope',
'$window',
'$route',
'$http',
'$location',
'growl',
'notifications',
'Query',
'Visualization',
'DataSource',
QueryViewCtrl
]);
})(); })();

171
rd_ui/app/views/query.html Normal file
View File

@ -0,0 +1,171 @@
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="row">
<div class="col-lg-10">
<h2>
<edit-in-place editable="isOwner" done="saveQuery" ignore-blanks='true' value="query.name"></edit-in-place>
</h2>
<p>
<em>
<edit-in-place editable="isOwner" done="saveQuery" editor="textarea" placeholder="No description" ignore-blanks='false' value="query.description"></edit-in-place>
</em>
</p>
</div>
<div class="col-lg-2" ng-hide="isNewQuery || !currentUser.hasPermission('view_source')">
<a ng-href="{{sourceHref}}" ng-click="toggleSource()" class="hidden-xs pull-right">
<span ng-show="isSourceVisible">Hide Source</span>
<span ng-show="!isSourceVisible">View Source</span>
</a>
</div>
</div>
<div class="visible-xs">
<p>
<span class="text-muted">Last update </span>
<strong>
<rd-time-ago value="queryResult.query_result.retrieved_at"></rd-time-ago>
</strong>
&nbsp;
<span class="text-muted">Created By </span>
<strong ng-hide="isOwner">{{query.user.name}}</strong>
<strong ng-show="isOwner">You</strong>
&nbsp;
<span class="text-muted">Runtime </span>
<strong ng-show="!queryExecuting">{{queryResult.getRuntime() | durationHumanize}}</strong>
<span ng-show="queryExecuting">Running&hellip;</span>
&nbsp;
<span class="text-muted">Rows </span>
<strong>{{queryResult.getData().length}}</strong>
</p>
<p>
<a ng-href="{{sourceHref}}" ng-click="toggleSource()">
<span ng-show="isSourceVisible">Hide Source</span>
<span ng-show="!isSourceVisible">View Source</span>
</a>
</p>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-lg-12">
<div ng-show="isSourceVisible">
<p>
<button type="button" class="btn btn-primary btn-xs" ng-disabled="queryExecuting" ng-click="executeQuery()">
<span class="glyphicon glyphicon-play"></span> Execute
</button>
<query-formatter query="query" lock="queryExecuting" editor-options="editorOptions"></query-formatter>
<span class="pull-right">
<button class="btn btn-xs btn-default rd-hidden-xs" ng-show="canFork" ng-click="duplicateQuery()">
<span class="glyphicon glyphicon-share-alt"></span> Fork
</button>
<button class="btn btn-success btn-xs" ng-show="canEdit" ng-click="saveQuery()">
<span class="glyphicon glyphicon-floppy-disk"> </span> Save<span ng-show="dirty">&#42;</span>
</button>
</span>
</p>
</div>
<!-- code editor -->
<p ng-show="isSourceVisible">
<query-editor query="query"></query-editor>
</p>
<hr ng-show="isSourceVisible">
</div>
</div>
<div class="row">
<div class="col-lg-3 rd-hidden-xs">
<p>
<span class="glyphicon glyphicon-time"></span>
<span class="text-muted">Last update </span>
<strong>
<rd-time-ago value="queryResult.query_result.retrieved_at"></rd-time-ago>
</strong>
</p>
<p>
<span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Created By </span>
<strong ng-hide="isOwner">{{query.user.name}}</strong>
<strong ng-show="isOwner">You</strong>
</p>
<p>
<span class="glyphicon glyphicon-play"></span>
<span class="text-muted">Runtime </span>
<strong ng-show="!queryExecuting">{{queryResult.getRuntime() | durationHumanize}}</strong>
<span ng-show="queryExecuting">Running&hellip;</span>
</p>
<p>
<span class="glyphicon glyphicon-align-justify"></span>
<span class="text-muted">Rows </span><strong>{{queryResult.getData().length}}</strong>
</p>
<p>
<span class="glyphicon glyphicon-refresh"></span>
<span class="text-muted">Refresh Interval</span>
<query-refresh-select></query-refresh-select>
</p>
<hr>
<p>
<a class="btn btn-primary btn-sm" ng-disabled="queryExecuting || !queryResult.getData()" ng-href="{{dataUri}}" download="{{dataFilename}}" target="_self">
<span class="glyphicon glyphicon-cloud-download"></span>
<span class="rd-hidden-xs">Download Dataset</span>
</a>
</p>
</div>
<div class="col-lg-9">
<!-- alerts -->
<div class="alert alert-info" ng-show="queryResult.getStatus() == 'processing'">
Executing query&hellip; <rd-timer timestamp="queryResult.getUpdatedAt()"></rd-timer>
<button type="button" class="btn btn-warning btn-xs pull-right" ng-disabled="cancelling" ng-click="cancelExecution()">Cancel</button>
</div>
<div class="alert alert-info" ng-show="queryResult.getStatus() == 'waiting'">
Query in queue&hellip; <rd-timer timestamp="queryResult.getUpdatedAt()"></rd-timer>
<button type="button" class="btn btn-warning btn-xs pull-right" ng-disabled="cancelling" ng-click="cancelExecution()">Cancel</button>
</div>
<div class="alert alert-danger" ng-show="queryResult.getError()">Error running query: <strong>{{queryResult.getError()}}</strong></div>
<!-- tabs and data -->
<div ng-show="queryResult.getStatus() == 'done'">
<div class="row">
<div class="col-lg-12">
<ul class="nav nav-tabs">
<rd-tab tab-id="table" name="Table"></rd-tab>
<rd-tab tab-id="pivot" name="Pivot Table"></rd-tab>
<rd-tab tab-id="{{vis.id}}" name="{{vis.name}}" ng-hide="vis.type=='TABLE'" ng-repeat="vis in query.visualizations">
<span class="remove" ng-click="deleteVisualization($event, vis)" ng-show="canEdit"> &times;</span>
</rd-tab>
<rd-tab tab-id="add" name="&plus; New" removeable="true" ng-show="canEdit"></rd-tab>
</ul>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<grid-renderer ng-show="selectedTab == 'table'" query-result="queryResult" items-per-page="50"></grid-renderer>
<pivot-table-renderer ng-show="selectedTab == 'pivot'" query-result="queryResult"></pivot-table-renderer>
<div ng-show="selectedTab == vis.id" ng-repeat="vis in query.visualizations">
<visualization-renderer visualization="vis" query-result="queryResult"></visualization-renderer>
<edit-visulatization-form visualization="vis" query="query" query-result="queryResult" ng-show="canEdit"></edit-visulatization-form>
</div>
<div ng-show="selectedTab == 'add'">
<visualization-renderer visualization="newVisualization" query-result="queryResult"></visualization-renderer>
<edit-visulatization-form visualization="newVisualization" query="query" ng-show="canEdit"></edit-visulatization-form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -31,7 +31,8 @@
</strong> </strong>
&nbsp; &nbsp;
<span class="text-muted">Created By </span> <span class="text-muted">Created By </span>
<strong>{{query.user.name}}</strong> <strong ng-hide="isOwner">{{query.user.name}}</strong>
<strong ng-show="isOwner">You</strong>
&nbsp; &nbsp;
<span class="text-muted">Runtime </span> <span class="text-muted">Runtime </span>
<strong ng-show="!queryExecuting">{{queryResult.getRuntime() | durationHumanize}}</strong> <strong ng-show="!queryExecuting">{{queryResult.getRuntime() | durationHumanize}}</strong>
@ -94,7 +95,9 @@
</p> </p>
<p> <p>
<span class="glyphicon glyphicon-user"></span> <span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Created By </span><strong>{{query.user.name}}</strong> <span class="text-muted">Created By </span>
<strong ng-hide="isOwner">{{query.user.name}}</strong>
<strong ng-show="isOwner">You</strong>
</p> </p>
<p> <p>
<span class="glyphicon glyphicon-play"></span> <span class="glyphicon glyphicon-play"></span>