mirror of
https://github.com/getredash/redash.git
synced 2025-12-25 01:03:20 -05:00
Query view/edit screens
This commit is contained in:
@@ -7,7 +7,7 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
// allow debugger during development
|
||||
'no-param-reassign': ['error', { "props": false }],
|
||||
'no-param-reassign': 0,
|
||||
'no-mixed-operators': 0,
|
||||
'no-underscore-dangle': 0,
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
|
||||
|
||||
@@ -15,3 +15,4 @@ export { default as editInPlace } from './edit-in-place';
|
||||
export { default as dynamicForm } from './dynamic-form';
|
||||
export { default as rdTimer } from './rd-timer';
|
||||
export { default as rdTimeAgo } from './rd-time-ago';
|
||||
export { default as overlay } from './overlay';
|
||||
|
||||
16
frontend/app/components/overlay.js
Normal file
16
frontend/app/components/overlay.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const Overlay = {
|
||||
template: `
|
||||
<div>
|
||||
<div class="overlay"></div>
|
||||
<div style="width: 100%; position:absolute; top:50px; z-index:2000">
|
||||
<div class="well well-lg" style="width: 70%; margin: auto;" ng-transclude>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
transclude: true,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
ngModule.component('overlay', Overlay);
|
||||
}
|
||||
26
frontend/app/directives/index.js
Normal file
26
frontend/app/directives/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
function compareTo() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
scope: {
|
||||
otherModelValue: '=compareTo',
|
||||
},
|
||||
link(scope, element, attributes, ngModel) {
|
||||
const validate = (value) => {
|
||||
ngModel.$setValidity('compareTo', value === scope.otherModelValue);
|
||||
};
|
||||
|
||||
scope.$watch('otherModelValue', () => {
|
||||
validate(ngModel.$modelValue);
|
||||
});
|
||||
|
||||
ngModel.$parsers.push((value) => {
|
||||
validate(value);
|
||||
return value;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
ngModule.directive('compareTo', compareTo);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'material-design-iconic-font/dist/css/material-design-iconic-font.css';
|
||||
import 'font-awesome/css/font-awesome.css';
|
||||
import 'ui-select/dist/select.css';
|
||||
import 'angular-toastr/dist/angular-toastr.css';
|
||||
import 'angular-resizable/src/angular-resizable.css';
|
||||
|
||||
import debug from 'debug';
|
||||
import angular from 'angular';
|
||||
@@ -13,12 +14,11 @@ import uiSelect from 'ui-select';
|
||||
import ngMessages from 'angular-messages';
|
||||
import toastr from 'angular-toastr';
|
||||
import ngUpload from 'angular-base64-upload';
|
||||
import vsRepeat from 'angular-vs-repeat';
|
||||
import 'angular-moment';
|
||||
|
||||
import 'brace';
|
||||
import 'angular-ui-ace';
|
||||
import 'angular-resizable';
|
||||
|
||||
import { ngTable } from 'ng-table';
|
||||
import { each } from 'underscore';
|
||||
|
||||
@@ -29,6 +29,7 @@ import * as pages from './pages';
|
||||
import * as components from './components';
|
||||
import * as filters from './filters';
|
||||
import * as services from './services';
|
||||
import registerDirectives from './directives';
|
||||
import registerVisualizations from './visualizations';
|
||||
import markdownFilter from './filters/markdown';
|
||||
|
||||
@@ -36,7 +37,7 @@ const logger = debug('redash');
|
||||
|
||||
const requirements = [
|
||||
ngRoute, ngResource, ngSanitize, uiBootstrap, ngMessages, uiSelect, ngTable.name, 'angularMoment', toastr, 'ui.ace',
|
||||
ngUpload, 'angularResizable',
|
||||
ngUpload, 'angularResizable', vsRepeat,
|
||||
];
|
||||
|
||||
const ngModule = angular.module('app', requirements);
|
||||
@@ -112,6 +113,7 @@ function registerFilters() {
|
||||
});
|
||||
}
|
||||
|
||||
registerDirectives(ngModule);
|
||||
registerServices();
|
||||
registerFilters();
|
||||
markdownFilter(ngModule);
|
||||
|
||||
37
frontend/app/pages/queries/alert-unsaved-changes.js
Normal file
37
frontend/app/pages/queries/alert-unsaved-changes.js
Normal file
@@ -0,0 +1,37 @@
|
||||
function alertUnsavedChanges($window) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
isDirty: '=',
|
||||
},
|
||||
link($scope) {
|
||||
const unloadMessage = 'You will lose your changes if you leave';
|
||||
const confirmMessage = `${unloadMessage}\n\nAre you sure you want to leave this page?`;
|
||||
// store original handler (if any)
|
||||
const _onbeforeunload = $window.onbeforeunload;
|
||||
|
||||
$window.onbeforeunload = function onbeforeunload() {
|
||||
return $scope.isDirty ? unloadMessage : null;
|
||||
};
|
||||
|
||||
$scope.$on('$locationChangeStart', (event, next, current) => {
|
||||
if (next.split('?')[0] === current.split('?')[0] || next.split('#')[0] === current.split('#')[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.isDirty && !$window.confirm(confirmMessage)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
$window.onbeforeunload = _onbeforeunload;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
ngModule.directive('alertUnsavedChanges', alertUnsavedChanges);
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="modal-body">
|
||||
<h5>IFrame Embed</h5>
|
||||
<div>
|
||||
<code><iframe src="{{ embedUrl }}" width="720" height="391"></iframe></code>
|
||||
<code><iframe src="{{ $ctrl.embedUrl }}" width="720" height="391"></iframe></code>
|
||||
</div>
|
||||
<span class="text-muted">(height should be adjusted)</span>
|
||||
<div ng-if="snapshotUrl">
|
||||
24
frontend/app/pages/queries/embed-code-dialog.js
Normal file
24
frontend/app/pages/queries/embed-code-dialog.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import template from './embed-code-dialog.html';
|
||||
|
||||
const EmbedCodeDialog = {
|
||||
controller(clientConfig) {
|
||||
this.query = this.resolve.query;
|
||||
this.visualization = this.resolve.visualization;
|
||||
|
||||
this.embedUrl = `${clientConfig.basePath}embed/query/${this.query.id}/visualization/${this.visualization.id}?api_key=${this.query.api_key}`;
|
||||
console.log(window.snapshotUrl);
|
||||
if (window.snapshotUrlBuilder) {
|
||||
this.snapshotUrl = window.snapshotUrlBuilder(this.query, this.visualization);
|
||||
}
|
||||
},
|
||||
bindings: {
|
||||
resolve: '<',
|
||||
close: '&',
|
||||
dismiss: '&',
|
||||
},
|
||||
template,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
ngModule.component('embedCodeDialog', EmbedCodeDialog);
|
||||
}
|
||||
@@ -3,10 +3,16 @@ import registerView from './view';
|
||||
import registerQueryResultsLink from './query-results-link';
|
||||
import registerQueryEditor from './query-editor';
|
||||
import registerSchemaBrowser from './schema-browser';
|
||||
import registerEmbedCodeDialog from './embed-code-dialog';
|
||||
import registerScheduleDialog from './schedule-dialog';
|
||||
import registerAlertUnsavedChanges from './alert-unsaved-changes';
|
||||
|
||||
export default function (ngModule) {
|
||||
registerQueryResultsLink(ngModule);
|
||||
registerQueryEditor(ngModule);
|
||||
registerSchemaBrowser(ngModule);
|
||||
registerEmbedCodeDialog(ngModule);
|
||||
registerScheduleDialog(ngModule);
|
||||
registerAlertUnsavedChanges(ngModule);
|
||||
return Object.assign({}, registerSourceView(ngModule), registerView(ngModule));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ function queryEditor(QuerySnippet) {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
query: '=',
|
||||
lock: '=',
|
||||
schema: '=',
|
||||
syntax: '=',
|
||||
},
|
||||
|
||||
@@ -1,24 +1,3 @@
|
||||
<!-- modal for archive button -->
|
||||
<div class="modal fade" id="archive-confirmation-modal" tabindex="-1" role="dialog"
|
||||
aria-labelledby="archiveConfirmationModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Query Archive</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to archive this query?
|
||||
<br/> All alerts and dashboard widgets created with its visualizations will be deleted.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">No</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="archiveQuery()">Yes, archive.</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end of modal for archive button -->
|
||||
|
||||
<div class="container">
|
||||
<overlay ng-if="canCreateQuery === false && query.isNew()">
|
||||
You don't have permission to create new queries on any of the data sources available to you. You can either <a
|
||||
@@ -114,7 +93,10 @@
|
||||
ng-click="executeQuery()">
|
||||
<span class="zmdi zmdi-play"></span> Execute
|
||||
</button>
|
||||
<query-formatter></query-formatter>
|
||||
|
||||
<button type="button" class="btn btn-default btn-s" ng-click="formatQuery()">
|
||||
<span class="zmdi zmdi-format-indent-increase"></span> Format Query
|
||||
</button>
|
||||
|
||||
<i class="fa fa-database"></i>
|
||||
<span class="text-muted">Data Source</span>
|
||||
@@ -129,12 +111,12 @@
|
||||
<span class="fa fa-floppy-o"> </span> Save<span
|
||||
ng-show="isDirty">*</span>
|
||||
</button>
|
||||
<div class="btn-group" role="group" dropdown>
|
||||
<button class="btn btn-default btn-sm dropdown-toggle" dropdown-toggle>
|
||||
<div class="btn-group" role="group" uib-dropdown>
|
||||
<button class="btn btn-default btn-sm dropdown-toggle" uib-dropdown-toggle>
|
||||
<span class="zmdi zmdi-more"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu pull-right" dropdown-menu>
|
||||
<li ng-if="!query.is_archived && query.id != undefined && (isQueryOwner || currentUser.hasPermission('admin'))"><a hash-link hash="archive-confirmation-modal" data-toggle="modal">Archive Query</a></li>
|
||||
<ul class="dropdown-menu pull-right" uib-dropdown-menu>
|
||||
<li ng-if="!query.is_archived && query.id != undefined && (isQueryOwner || currentUser.hasPermission('admin'))"><a ng-click="archiveQuery()">Archive Query</a></li>
|
||||
<li ng-if="!query.is_archived && query.id != undefined && (isQueryOwner || currentUser.hasPermission('admin')) && showPermissionsControl"><a ng-click="showManagePermissionsModal()">Manage Permissions</a></li>
|
||||
<li ng-if="query.is_draft && query.id != undefined && (isQueryOwner || currentUser.hasPermission('admin'))"><a ng-click="togglePublished()">Publish Query</a></li>
|
||||
<li ng-if="!query.is_draft && query.id != undefined && (isQueryOwner || currentUser.hasPermission('admin'))"><a ng-click="togglePublished()">Unpublish Query</a></li>
|
||||
|
||||
18
frontend/app/pages/queries/schedule-dialog.html
Normal file
18
frontend/app/pages/queries/schedule-dialog.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.close()"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">Refresh Schedule</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" value="periodic" ng-model="$ctrl.refreshType">
|
||||
<query-refresh-select refresh-type="$ctrl.refreshType" query="$ctrl.query" save-query="$ctrl.saveQuery"></query-refresh-select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" value="daily" ng-model="$ctrl.refreshType">
|
||||
<query-time-picker refresh-type="$ctrl.refreshType" query="$ctrl.query" save-query="$ctrl.saveQuery"></query-time-picker>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
150
frontend/app/pages/queries/schedule-dialog.js
Normal file
150
frontend/app/pages/queries/schedule-dialog.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import moment from 'moment';
|
||||
import { map, range, partial } from 'underscore';
|
||||
|
||||
import template from './schedule-dialog.html';
|
||||
|
||||
function padWithZeros(size, v) {
|
||||
let str = String(v);
|
||||
if (str.length < size) {
|
||||
str = `0${str}`;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function queryTimePicker() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
refreshType: '=',
|
||||
query: '=',
|
||||
saveQuery: '=',
|
||||
},
|
||||
template: `
|
||||
<select ng-disabled="refreshType != 'daily'" ng-model="hour" ng-change="updateSchedule()" ng-options="c as c for c in hourOptions"></select> :
|
||||
<select ng-disabled="refreshType != 'daily'" ng-model="minute" ng-change="updateSchedule()" ng-options="c as c for c in minuteOptions"></select>
|
||||
`,
|
||||
link($scope) {
|
||||
$scope.hourOptions = map(range(0, 24), partial(padWithZeros, 2));
|
||||
$scope.minuteOptions = map(range(0, 60, 5), partial(padWithZeros, 2));
|
||||
|
||||
if ($scope.query.hasDailySchedule()) {
|
||||
const parts = $scope.query.scheduleInLocalTime().split(':');
|
||||
$scope.minute = parts[1];
|
||||
$scope.hour = parts[0];
|
||||
} else {
|
||||
$scope.minute = '15';
|
||||
$scope.hour = '00';
|
||||
}
|
||||
|
||||
$scope.updateSchedule = () => {
|
||||
const newSchedule = moment().hour($scope.hour)
|
||||
.minute($scope.minute)
|
||||
.utc()
|
||||
.format('HH:mm');
|
||||
|
||||
if (newSchedule !== $scope.query.schedule) {
|
||||
$scope.query.schedule = newSchedule;
|
||||
$scope.saveQuery();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('refreshType', () => {
|
||||
if ($scope.refreshType === 'daily') {
|
||||
$scope.updateSchedule();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function queryRefreshSelect() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
refreshType: '=',
|
||||
query: '=',
|
||||
saveQuery: '=',
|
||||
},
|
||||
template: `<select
|
||||
ng-disabled="refreshType != 'periodic'"
|
||||
ng-model="query.schedule"
|
||||
ng-change="saveQuery()"
|
||||
ng-options="c.value as c.name for c in refreshOptions">
|
||||
<option value="">No Refresh</option>
|
||||
</select>`,
|
||||
link($scope) {
|
||||
$scope.refreshOptions = [
|
||||
{
|
||||
value: '60',
|
||||
name: 'Every minute',
|
||||
},
|
||||
];
|
||||
|
||||
[5, 10, 15, 30].forEach((i) => {
|
||||
$scope.refreshOptions.push({
|
||||
value: String(i * 60),
|
||||
name: `Every ${i} minutes`,
|
||||
});
|
||||
});
|
||||
|
||||
range(1, 13).forEach((i) => {
|
||||
$scope.refreshOptions.push({
|
||||
value: String(i * 3600),
|
||||
name: `Every ${i}h`,
|
||||
});
|
||||
});
|
||||
|
||||
$scope.refreshOptions.push({
|
||||
value: String(24 * 3600),
|
||||
name: 'Every 24h',
|
||||
});
|
||||
$scope.refreshOptions.push({
|
||||
value: String(7 * 24 * 3600),
|
||||
name: 'Every 7 days',
|
||||
});
|
||||
$scope.refreshOptions.push({
|
||||
value: String(14 * 24 * 3600),
|
||||
name: 'Every 14 days',
|
||||
});
|
||||
$scope.refreshOptions.push({
|
||||
value: String(30 * 24 * 3600),
|
||||
name: 'Every 30 days',
|
||||
});
|
||||
|
||||
$scope.$watch('refreshType', () => {
|
||||
if ($scope.refreshType === 'periodic') {
|
||||
if ($scope.query.hasDailySchedule()) {
|
||||
$scope.query.schedule = null;
|
||||
$scope.saveQuery();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
const ScheduleForm = {
|
||||
controller() {
|
||||
this.query = this.resolve.query;
|
||||
this.saveQuery = this.resolve.saveQuery;
|
||||
|
||||
if (this.query.hasDailySchedule()) {
|
||||
this.refreshType = 'daily';
|
||||
} else {
|
||||
this.refreshType = 'periodic';
|
||||
}
|
||||
},
|
||||
bindings: {
|
||||
resolve: '<',
|
||||
close: '&',
|
||||
dismiss: '&',
|
||||
},
|
||||
template,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
ngModule.directive('queryTimePicker', queryTimePicker);
|
||||
ngModule.directive('queryRefreshSelect', queryRefreshSelect);
|
||||
ngModule.component('scheduleDialog', ScheduleForm);
|
||||
}
|
||||
@@ -6,10 +6,10 @@
|
||||
<div class="schema-browser" vs-repeat vs-size="getSize(table)">
|
||||
<div ng-repeat="table in schema | filter:schemaFilter track by table.name">
|
||||
<div class="table-name" ng-click="showTable(table)">
|
||||
<i class="fa fa-table"></i> <strong><span title="{{table.name}}">{{table.name}}</span><span
|
||||
ng-if="table.size !== undefined"> ({{table.size}})</span></strong>
|
||||
<i class="fa fa-table"></i> <strong><span title="{{table.name}}">{{table.name}}</span>
|
||||
<span ng-if="table.size !== undefined"> ({{table.size}})</span></strong>
|
||||
</div>
|
||||
<div collapse="table.collapsed">
|
||||
<div uib-collapse="table.collapsed">
|
||||
<div ng-repeat="column in table.columns track by column" style="padding-left:16px;">{{column}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import template from './query.html';
|
||||
|
||||
function QuerySourceCtrl(Events, toastr, $controller, $scope, $location, $http,
|
||||
function QuerySourceCtrl(Events, toastr, $controller, $scope, $location, $http, $q,
|
||||
currentUser, Query, Visualization, KeyboardShortcuts) {
|
||||
// extends QueryViewCtrl
|
||||
$controller('QueryViewCtrl', { $scope });
|
||||
@@ -13,7 +13,6 @@ function QuerySourceCtrl(Events, toastr, $controller, $scope, $location, $http,
|
||||
|
||||
const isNewQuery = !$scope.query.id;
|
||||
let queryText = $scope.query.query;
|
||||
// ref to QueryViewCtrl.saveQuery
|
||||
const saveQuery = $scope.saveQuery;
|
||||
|
||||
$scope.sourceMode = true;
|
||||
@@ -53,10 +52,6 @@ function QuerySourceCtrl(Events, toastr, $controller, $scope, $location, $http,
|
||||
$scope.saveQuery = (options, data) => {
|
||||
const savePromise = saveQuery(options, data);
|
||||
|
||||
if (!savePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
savePromise.then((savedQuery) => {
|
||||
queryText = savedQuery.query;
|
||||
$scope.isDirty = $scope.query.query !== queryText;
|
||||
@@ -67,16 +62,17 @@ function QuerySourceCtrl(Events, toastr, $controller, $scope, $location, $http,
|
||||
// redirect to new created query (keep hash)
|
||||
$location.path(savedQuery.getSourceLink());
|
||||
}
|
||||
}, (error) => {
|
||||
if (error.status == 409) {
|
||||
toastr.error('It seems like the query has been modified by another user. ' +
|
||||
'Please copy/backup your changes and reload this page.', { autoDismiss: false });
|
||||
}
|
||||
});
|
||||
|
||||
return savePromise;
|
||||
};
|
||||
|
||||
$scope.formatQuery = () => {
|
||||
Query.format($scope.dataSource.syntax, $scope.query.query)
|
||||
.then((query) => { $scope.query.query = query; })
|
||||
.catch(error => toastr.error(error));
|
||||
};
|
||||
|
||||
$scope.duplicateQuery = () => {
|
||||
Events.record(currentUser, 'fork', 'query', $scope.query.id);
|
||||
$scope.query.name = `Copy of (#${$scope.query.id}) ${$scope.query.name}`;
|
||||
@@ -97,14 +93,11 @@ function QuerySourceCtrl(Events, toastr, $controller, $scope, $location, $http,
|
||||
Events.record(currentUser, 'delete', 'visualization', vis.id);
|
||||
|
||||
Visualization.delete(vis, () => {
|
||||
if ($scope.selectedTab == vis.id) {
|
||||
if ($scope.selectedTab === vis.id) {
|
||||
$scope.selectedTab = DEFAULT_TAB;
|
||||
$location.hash($scope.selectedTab);
|
||||
}
|
||||
$scope.query.visualizations =
|
||||
$scope.query.visualizations.filter(v =>
|
||||
vis.id !== v.id
|
||||
);
|
||||
$scope.query.visualizations = $scope.query.visualizations.filter(v => vis.id !== v.id);
|
||||
}, () => {
|
||||
toastr.error("Error deleting visualization. Maybe it's used in a dashboard?");
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { pick, any, some, find } from 'underscore';
|
||||
import template from './query.html';
|
||||
|
||||
function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $window,
|
||||
Notifications, clientConfig, toastr, $uibModal, currentUser, Query, DataSource) {
|
||||
function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $window, $q,
|
||||
AlertDialog, Notifications, clientConfig, toastr, $uibModal, currentUser, Query, DataSource) {
|
||||
const DEFAULT_TAB = 'table';
|
||||
|
||||
function getQueryResult(maxAge) {
|
||||
@@ -123,24 +123,26 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $
|
||||
$window.alert(`API Key for this query:\n${$scope.query.api_key}`);
|
||||
};
|
||||
|
||||
$scope.saveQuery = (options, data) => {
|
||||
if (data) {
|
||||
$scope.saveQuery = (customOptions, data) => {
|
||||
let request = data;
|
||||
|
||||
if (request) {
|
||||
// Don't save new query with partial data
|
||||
if ($scope.query.isNew()) {
|
||||
return;
|
||||
return $q.reject();
|
||||
}
|
||||
data.id = $scope.query.id;
|
||||
data.version = $scope.query.version;
|
||||
request.id = $scope.query.id;
|
||||
request.version = $scope.query.version;
|
||||
} else {
|
||||
data = pick($scope.query, ['schedule', 'query', 'id', 'description', 'name', 'data_source_id', 'options', 'latest_query_data_id', 'version']);
|
||||
request = pick($scope.query, ['schedule', 'query', 'id', 'description', 'name', 'data_source_id', 'options', 'latest_query_data_id', 'version']);
|
||||
}
|
||||
|
||||
options = Object.assign({}, {
|
||||
const options = Object.assign({}, {
|
||||
successMessage: 'Query saved',
|
||||
errorMessage: 'Query could not be saved',
|
||||
}, options);
|
||||
}, customOptions);
|
||||
|
||||
return Query.save(data, (updatedQuery) => {
|
||||
return Query.save(request, (updatedQuery) => {
|
||||
toastr.success(options.successMessage);
|
||||
$scope.query.version = updatedQuery.version;
|
||||
}, (error) => {
|
||||
@@ -186,29 +188,22 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $
|
||||
Events.record(currentUser, 'cancel_execute', 'query', $scope.query.id);
|
||||
};
|
||||
|
||||
$scope.archiveQuery = (options, data) => {
|
||||
if (data) {
|
||||
data.id = $scope.query.id;
|
||||
} else {
|
||||
data = $scope.query;
|
||||
$scope.archiveQuery = () => {
|
||||
function archive() {
|
||||
Query.delete({ id: $scope.query.id }, () => {
|
||||
$scope.query.is_archived = true;
|
||||
$scope.query.schedule = null;
|
||||
toastr.success('Query archived.');
|
||||
}, () => {
|
||||
toastr.error('Query could not be archived.');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.isDirty = false;
|
||||
const title = 'Archive Query';
|
||||
const message = 'Are you sure you want to archive this query?<br/> All alerts and dashboard widgets created with its visualizations will be deleted.';
|
||||
const actions = [{ class: 'btn-warning', title: 'Archive', callback: archive }];
|
||||
|
||||
options = Object.assign({}, {
|
||||
successMessage: 'Query archived',
|
||||
errorMessage: 'Query could not be archived',
|
||||
}, options);
|
||||
|
||||
return Query.delete({ id: data.id }, () => {
|
||||
$scope.query.is_archived = true;
|
||||
$scope.query.schedule = null;
|
||||
toastr.success(options.successMessage);
|
||||
// This feels dirty.
|
||||
$('#archive-confirmation-modal').modal('hide');
|
||||
}, (httpResponse) => {
|
||||
toastr.error(options.errorMessage);
|
||||
}).$promise;
|
||||
AlertDialog.open(title, message, actions);
|
||||
};
|
||||
|
||||
$scope.updateDataSource = () => {
|
||||
@@ -274,16 +269,14 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $
|
||||
$scope.openVisualizationEditor = (visualization) => {
|
||||
function openModal() {
|
||||
$uibModal.open({
|
||||
templateUrl: '/views/directives/visualization_editor.html',
|
||||
windowClass: 'modal-xl',
|
||||
scope: $scope,
|
||||
controller: ['$scope', '$modalInstance', function ($scope, $modalInstance) {
|
||||
$scope.modalInstance = $modalInstance;
|
||||
$scope.visualization = visualization;
|
||||
$scope.close = function () {
|
||||
$modalInstance.close();
|
||||
};
|
||||
}],
|
||||
component: 'editVisualizationDialog',
|
||||
resolve: {
|
||||
query: $scope.query,
|
||||
visualization,
|
||||
queryResult: $scope.queryResult,
|
||||
onNewSuccess: () => $scope.setVisualizationTab,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -303,61 +296,43 @@ function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, $
|
||||
$scope.openVisualizationEditor();
|
||||
}
|
||||
|
||||
$scope.openScheduleForm = function () {
|
||||
$scope.openScheduleForm = () => {
|
||||
if (!$scope.isQueryOwner || !$scope.canScheduleQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uibModal.open({
|
||||
templateUrl: '/views/schedule_form.html',
|
||||
component: 'scheduleDialog',
|
||||
size: 'sm',
|
||||
scope: $scope,
|
||||
controller($scope, $modalInstance) {
|
||||
$scope.close = function () {
|
||||
$modalInstance.close();
|
||||
};
|
||||
if ($scope.query.hasDailySchedule()) {
|
||||
$scope.refreshType = 'daily';
|
||||
} else {
|
||||
$scope.refreshType = 'periodic';
|
||||
}
|
||||
resolve: {
|
||||
query: $scope.query,
|
||||
saveQuery: () => $scope.saveQuery,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showEmbedDialog = function (query, visualization) {
|
||||
$modal.open({
|
||||
templateUrl: '/views/dialogs/embed_code.html',
|
||||
controller: ['$scope', '$modalInstance', function ($scope, $modalInstance) {
|
||||
$scope.close = function () {
|
||||
$modalInstance.close();
|
||||
};
|
||||
$scope.embedUrl = `${basePath}embed/query/${query.id}/visualization/${visualization.id}?api_key=${query.api_key}`;
|
||||
if (window.snapshotUrlBuilder) {
|
||||
$scope.snapshotUrl = snapshotUrlBuilder(query, visualization);
|
||||
}
|
||||
}],
|
||||
$scope.showEmbedDialog = (query, visualization) => {
|
||||
$uibModal.open({
|
||||
component: 'embedCodeDialog',
|
||||
resolve: {
|
||||
query,
|
||||
visualization,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch(() =>
|
||||
$location.hash()
|
||||
, (hash) => {
|
||||
if (hash === 'pivot') {
|
||||
Events.record(currentUser, 'pivot', 'query', $scope.query && $scope.query.id);
|
||||
}
|
||||
$scope.selectedTab = hash || DEFAULT_TAB;
|
||||
});
|
||||
|
||||
$scope.showManagePermissionsModal = function () {
|
||||
// Create scope for share permissions dialog and pass api path to it
|
||||
const scope = $scope.$new();
|
||||
$scope.apiAccess = `api/queries/${$routeParams.queryId}/acl`;
|
||||
|
||||
$modal.open({
|
||||
scope,
|
||||
templateUrl: '/views/dialogs/manage_permissions.html',
|
||||
controller: 'ManagePermissionsCtrl',
|
||||
$scope.showManagePermissionsModal = () => {
|
||||
$uibModal.open({
|
||||
component: 'permissionsEditor',
|
||||
resolve: {
|
||||
aclUrl: { url: `api/queries/${$routeParams.queryId}/acl` },
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
55
frontend/app/services/alert-dialog.js
Normal file
55
frontend/app/services/alert-dialog.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { wrap } from 'underscore';
|
||||
|
||||
const AlertDialogComponent = {
|
||||
template: `
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{$ctrl.title}}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-bind-html="$ctrl.message"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-default" ng-click="$ctrl.close()">Cancel</button>
|
||||
<button class="btn" ng-class="action.class" ng-click="action.callback()" ng-repeat="action in $ctrl.actions">{{action.title}}</button>
|
||||
</div>
|
||||
`,
|
||||
bindings: {
|
||||
close: '&',
|
||||
dismiss: '&',
|
||||
resolve: '<',
|
||||
},
|
||||
controller() {
|
||||
this.title = this.resolve.title;
|
||||
this.message = this.resolve.message;
|
||||
this.actions = this.resolve.actions.map((action) => {
|
||||
action.callback = wrap(action.callback, (callback) => {
|
||||
callback();
|
||||
this.close();
|
||||
});
|
||||
return action;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function AlertDialog($uibModal) {
|
||||
const service = {
|
||||
open(title, message, actions) {
|
||||
$uibModal.open({
|
||||
// windowClass: 'modal-sm',
|
||||
component: 'alertDialog',
|
||||
resolve: {
|
||||
title: () => title,
|
||||
message: () => message,
|
||||
actions: () => actions,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
ngModule.component('alertDialog', AlertDialogComponent);
|
||||
ngModule.factory('AlertDialog', AlertDialog);
|
||||
}
|
||||
@@ -12,3 +12,4 @@ export { default as DataSource } from './data-source';
|
||||
export { default as QuerySnippet } from './query-snippet';
|
||||
export { default as Notifications } from './notifications';
|
||||
export { default as KeyboardShortcuts } from './keyboard-shortcuts';
|
||||
export { default as AlertDialog } from './alert-dialog';
|
||||
|
||||
@@ -114,7 +114,7 @@ class Parameters {
|
||||
}
|
||||
}
|
||||
|
||||
function QueryResource($resource, $location, currentUser, QueryResult) {
|
||||
function QueryResource($resource, $http, $q, $location, currentUser, QueryResult) {
|
||||
const Query = $resource('api/queries/:id', { id: '@id' },
|
||||
{
|
||||
search: {
|
||||
@@ -147,6 +147,23 @@ function QueryResource($resource, $location, currentUser, QueryResult) {
|
||||
});
|
||||
};
|
||||
|
||||
Query.format = function formatQuery(syntax, query) {
|
||||
if (syntax === 'json') {
|
||||
try {
|
||||
const formatted = JSON.stringify(JSON.parse(query), ' ', 4);
|
||||
return $q.resolve(formatted);
|
||||
} catch (err) {
|
||||
return $q.reject(String(err));
|
||||
}
|
||||
} else if (syntax === 'sql') {
|
||||
return $http.post('api/queries/format', { query }).then(response =>
|
||||
response.data
|
||||
);
|
||||
} else {
|
||||
return $q.reject('Query formatting is not supported for your data source syntax.');
|
||||
}
|
||||
};
|
||||
|
||||
Query.prototype.getSourceLink = function getSourceLink() {
|
||||
return `/queries/${this.id}/source`;
|
||||
};
|
||||
|
||||
@@ -203,8 +203,15 @@ function ChartEditor(ColorPalette) {
|
||||
};
|
||||
}
|
||||
|
||||
const ColorBox = {
|
||||
bindings: {
|
||||
color: '<',
|
||||
},
|
||||
template: "<span style='width: 12px; height: 12px; background-color: {{$ctrl.color}}; display: inline-block; margin-right: 5px;'></span>",
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
ngModule.component('colorBox', ColorBox);
|
||||
ngModule.directive('chartRenderer', ChartRenderer);
|
||||
ngModule.directive('chartEditor', ChartEditor);
|
||||
ngModule.config((VisualizationProvider) => {
|
||||
|
||||
42
frontend/app/visualizations/edit-visualization-dialog.html
Normal file
42
frontend/app/visualizations/edit-visualization-dialog.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<div>
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.closeDialog()"><span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Visualization Editor</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-5 p-r-5">
|
||||
<div>
|
||||
<form name="$ctrl.visForm">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Visualization Type</label>
|
||||
|
||||
<select required ng-model="$ctrl.visualization.type"
|
||||
ng-options="value as key for (key, value) in $ctrl.visTypes" class="form-control"
|
||||
ng-change="$ctrl.typeChanged('{{$ctrl.visualization.type}}')"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Visualization Name</label>
|
||||
<input name="name" type="text" class="form-control" ng-model="$ctrl.visualization.name"
|
||||
placeholder="{{$ctrl.visualization.type | capitalize}}">
|
||||
</div>
|
||||
|
||||
<visualization-options-editor visualization="$ctrl.visualization"
|
||||
query-result="$ctrl.queryResult"
|
||||
query="$ctrl.query">
|
||||
</visualization-options-editor>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7" style="border: 1px solid #eee">
|
||||
<visualization-renderer visualization="$ctrl.visualization" query-result="$ctrl.queryResult"></visualization-renderer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" ng-click="$ctrl.closeDialog()">Close</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="$ctrl.submit()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
91
frontend/app/visualizations/edit-visualization-dialog.js
Normal file
91
frontend/app/visualizations/edit-visualization-dialog.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { pluck } from 'underscore';
|
||||
import { copy } from 'angular';
|
||||
import template from './edit-visualization-dialog.html';
|
||||
|
||||
const EditVisualizationDialog = {
|
||||
template,
|
||||
bindings: {
|
||||
resolve: '<',
|
||||
close: '&',
|
||||
dismiss: '&',
|
||||
},
|
||||
controller($window, currentUser, Events, Visualization, toastr) {
|
||||
this.query = this.resolve.query;
|
||||
this.queryResult = this.resolve.queryResult;
|
||||
this.originalVisualization = this.resolve.visualization;
|
||||
this.onNewSuccess = this.resolve.onNewSuccess;
|
||||
this.visualization = copy(this.originalVisualization);
|
||||
this.visTypes = Visualization.visualizationTypes;
|
||||
|
||||
this.newVisualization = () =>
|
||||
({
|
||||
type: Visualization.defaultVisualization.type,
|
||||
name: Visualization.defaultVisualization.name,
|
||||
description: '',
|
||||
options: Visualization.defaultVisualization.defaultOptions,
|
||||
})
|
||||
;
|
||||
|
||||
if (!this.visualization) {
|
||||
this.visualization = this.newVisualization();
|
||||
}
|
||||
|
||||
this.typeChanged = (oldType) => {
|
||||
const type = this.visualization.type;
|
||||
// if not edited by user, set name to match type
|
||||
// todo: this is wrong, because he might have edited it before.
|
||||
if (type && oldType !== type && this.visualization && !this.visForm.name.$dirty) {
|
||||
this.visualization.name = Visualization.visualizations[this.visualization.type].name;
|
||||
}
|
||||
|
||||
// Bring default options
|
||||
if (type && oldType !== type && this.visualization) {
|
||||
this.visualization.options =
|
||||
Visualization.visualizations[this.visualization.type].defaultOptions;
|
||||
}
|
||||
};
|
||||
|
||||
this.submit = () => {
|
||||
if (this.visualization.id) {
|
||||
Events.record(currentUser, 'update', 'visualization', this.visualization.id, { type: this.visualization.type });
|
||||
} else {
|
||||
Events.record(currentUser, 'create', 'visualization', null, { type: this.visualization.type });
|
||||
}
|
||||
|
||||
this.visualization.query_id = this.query.id;
|
||||
|
||||
Visualization.save(this.visualization, (result) => {
|
||||
toastr.success('Visualization saved');
|
||||
|
||||
const visIds = pluck(this.query.visualizations, 'id');
|
||||
const index = visIds.indexOf(result.id);
|
||||
if (index > -1) {
|
||||
this.query.visualizations[index] = result;
|
||||
} else {
|
||||
// new visualization
|
||||
this.query.visualizations.push(result);
|
||||
if (this.onNewSuccess) {
|
||||
this.onNewSuccess(result);
|
||||
}
|
||||
}
|
||||
this.close();
|
||||
}, () => {
|
||||
toastr.error('Visualization could not be saved');
|
||||
});
|
||||
};
|
||||
|
||||
this.closeDialog = () => {
|
||||
if (this.visForm.$dirty) {
|
||||
if ($window.confirm('Are you sure you want to close the editor without saving?')) {
|
||||
this.close();
|
||||
}
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
ngModule.component('editVisualizationDialog', EditVisualizationDialog);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<div>
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" aria-label="Close" ng-click="close()"><span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Visualization Editor</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-5 p-r-5">
|
||||
<div>
|
||||
<form name="visForm">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Visualization Type</label>
|
||||
|
||||
<select required ng-model="visualization.type"
|
||||
ng-options="value as key for (key, value) in visTypes" class="form-control"
|
||||
ng-change="typeChanged()"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Visualization Name</label>
|
||||
<input name="name" type="text" class="form-control" ng-model="visualization.name"
|
||||
placeholder="{{visualization.type | capitalize}}">
|
||||
</div>
|
||||
|
||||
<visualization-options-editor></visualization-options-editor>
|
||||
|
||||
<div class="form-group" ng-if="editRawOptions">
|
||||
<label class="control-label">Advanced</label>
|
||||
<textarea json-text ng-model="visualization.options" class="form-control" rows="10"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-7" style="border: 1px solid #eee">
|
||||
<visualization-renderer visualization="visualization" query-result="queryResult"></visualization-renderer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" ng-click="close()">Close</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="submit()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,10 +1,9 @@
|
||||
import moment from 'moment';
|
||||
import { pluck, isEmpty, isArray, reduce } from 'underscore';
|
||||
import { copy } from 'angular';
|
||||
import { isEmpty, isArray, reduce } from 'underscore';
|
||||
|
||||
import filtersTemplate from './filters.html';
|
||||
import editVisualizationTemplate from './edit-visualization.html';
|
||||
|
||||
import registerEditVisualizationDialog from './edit-visualization-dialog';
|
||||
import counterVisualization from './counter';
|
||||
import tableVisualization from './table';
|
||||
import chartVisualization from './chart';
|
||||
@@ -110,6 +109,11 @@ function VisualizationOptionsEditor(Visualization) {
|
||||
restrict: 'E',
|
||||
template: Visualization.editorTemplate,
|
||||
replace: false,
|
||||
scope: {
|
||||
visualization: '=',
|
||||
query: '=',
|
||||
queryResult: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,96 +146,6 @@ function FilterValueFilter(clientConfig) {
|
||||
};
|
||||
}
|
||||
|
||||
function EditVisualizationForm($window, currentUser, Events, Visualization, toastr) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: editVisualizationTemplate,
|
||||
replace: true,
|
||||
scope: {
|
||||
query: '=',
|
||||
queryResult: '=',
|
||||
originalVisualization: '=?',
|
||||
onNewSuccess: '=?',
|
||||
modalInstance: '=?',
|
||||
},
|
||||
link(scope) {
|
||||
scope.visualization = copy(scope.originalVisualization);
|
||||
scope.editRawOptions = currentUser.hasPermission('edit_raw_chart');
|
||||
scope.visTypes = Visualization.visualizationTypes;
|
||||
|
||||
scope.newVisualization = () =>
|
||||
({
|
||||
type: Visualization.defaultVisualization.type,
|
||||
name: Visualization.defaultVisualization.name,
|
||||
description: '',
|
||||
options: Visualization.defaultVisualization.defaultOptions,
|
||||
})
|
||||
;
|
||||
|
||||
if (!scope.visualization) {
|
||||
const unwatch = scope.$watch('query.id', (queryId) => {
|
||||
if (queryId) {
|
||||
unwatch();
|
||||
|
||||
scope.visualization = scope.newVisualization();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scope.$watch('visualization.type', (type, oldType) => {
|
||||
// if not edited by user, set name to match type
|
||||
if (type && oldType !== type && scope.visualization && !scope.visForm.name.$dirty) {
|
||||
scope.visualization.name = Visualization.visualizations[scope.visualization.type].name;
|
||||
}
|
||||
|
||||
if (type && oldType !== type && scope.visualization) {
|
||||
scope.visualization.options =
|
||||
Visualization.visualizations[scope.visualization.type].defaultOptions;
|
||||
}
|
||||
});
|
||||
|
||||
scope.submit = () => {
|
||||
if (scope.visualization.id) {
|
||||
Events.record(currentUser, 'update', 'visualization', scope.visualization.id, { type: scope.visualization.type });
|
||||
} else {
|
||||
Events.record(currentUser, 'create', 'visualization', null, { type: scope.visualization.type });
|
||||
}
|
||||
|
||||
scope.visualization.query_id = scope.query.id;
|
||||
|
||||
Visualization.save(scope.visualization, (result) => {
|
||||
toastr.success('Visualization saved');
|
||||
|
||||
const visIds = pluck(scope.query.visualizations, 'id');
|
||||
const index = visIds.indexOf(result.id);
|
||||
if (index > -1) {
|
||||
scope.query.visualizations[index] = result;
|
||||
} else {
|
||||
// new visualization
|
||||
scope.query.visualizations.push(result);
|
||||
if (scope.onNewSuccess) {
|
||||
scope.onNewSuccess(result);
|
||||
}
|
||||
}
|
||||
scope.modalInstance.close();
|
||||
}, () => {
|
||||
toastr.error('Visualization could not be saved');
|
||||
});
|
||||
};
|
||||
|
||||
scope.close = () => {
|
||||
if (scope.visForm.$dirty) {
|
||||
if ($window.confirm('Are you sure you want to close the editor without saving?')) {
|
||||
scope.modalInstance.close();
|
||||
}
|
||||
} else {
|
||||
scope.modalInstance.close();
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
ngModule.provider('Visualization', VisualizationProvider);
|
||||
ngModule.directive('visualizationRenderer', VisualizationRenderer);
|
||||
@@ -239,7 +153,7 @@ export default function (ngModule) {
|
||||
ngModule.directive('visualizationName', VisualizationName);
|
||||
ngModule.directive('filters', Filters);
|
||||
ngModule.filter('filterValue', FilterValueFilter);
|
||||
ngModule.directive('editVisulatizationForm', EditVisualizationForm);
|
||||
registerEditVisualizationDialog(ngModule);
|
||||
counterVisualization(ngModule);
|
||||
tableVisualization(ngModule);
|
||||
chartVisualization(ngModule);
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"angular-toastr": "^2.1.1",
|
||||
"angular-ui-ace": "^0.2.3",
|
||||
"angular-ui-bootstrap": "^2.2.0",
|
||||
"angular-vs-repeat": "^1.1.7",
|
||||
"babel-core": "^6.18.0",
|
||||
"babel-loader": "^6.2.7",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
|
||||
@@ -3,104 +3,6 @@
|
||||
|
||||
var directives = angular.module('redash.directives', []);
|
||||
|
||||
directives.directive('alertUnsavedChanges', ['$window', function ($window) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
'isDirty': '='
|
||||
},
|
||||
link: function ($scope) {
|
||||
var
|
||||
|
||||
unloadMessage = "You will lose your changes if you leave",
|
||||
confirmMessage = unloadMessage + "\n\nAre you sure you want to leave this page?",
|
||||
|
||||
// store original handler (if any)
|
||||
_onbeforeunload = $window.onbeforeunload;
|
||||
|
||||
$window.onbeforeunload = function () {
|
||||
return $scope.isDirty ? unloadMessage : null;
|
||||
}
|
||||
|
||||
$scope.$on('$locationChangeStart', function (event, next, current) {
|
||||
if (next.split("?")[0] == current.split("?")[0] || next.split("#")[0] == current.split("#")[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.isDirty && !confirm(confirmMessage)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
$window.onbeforeunload = _onbeforeunload;
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
directives.directive('hashLink', ['$location', function($location) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
'hash': '@'
|
||||
},
|
||||
link: function (scope, element) {
|
||||
var basePath = $location.path().substring(1);
|
||||
element[0].href = basePath + "#" + scope.hash;
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
directives.directive('rdTab', ['$location', function ($location) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
'tabId': '@',
|
||||
'name': '@',
|
||||
'basePath': '=?'
|
||||
},
|
||||
transclude: true,
|
||||
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="{{basePath}}#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
|
||||
replace: true,
|
||||
link: function (scope) {
|
||||
scope.basePath = scope.basePath || $location.path().substring(1);
|
||||
scope.$watch(function () {
|
||||
return scope.$parent.selectedTab
|
||||
}, function (tab) {
|
||||
scope.selectedTab = tab;
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
// http://stackoverflow.com/a/17904092/1559840
|
||||
directives.directive('jsonText', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function (scope, element, attr, ngModel) {
|
||||
function into(input) {
|
||||
return JSON.parse(input);
|
||||
}
|
||||
|
||||
function out(data) {
|
||||
return JSON.stringify(data, undefined, 2);
|
||||
}
|
||||
|
||||
ngModel.$parsers.push(into);
|
||||
ngModel.$formatters.push(out);
|
||||
|
||||
scope.$watch(attr.ngModel, function (newValue) {
|
||||
element[0].value = out(newValue);
|
||||
}, true);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Used instead of autofocus attribute, which doesn't work in Angular as there is no real page load.
|
||||
directives.directive('autofocus',
|
||||
['$timeout', function ($timeout) {
|
||||
@@ -114,162 +16,4 @@
|
||||
}]
|
||||
);
|
||||
|
||||
directives.directive('compareTo', function () {
|
||||
return {
|
||||
require: "ngModel",
|
||||
scope: {
|
||||
otherModelValue: "=compareTo"
|
||||
},
|
||||
link: function (scope, element, attributes, ngModel) {
|
||||
var validate = function(value) {
|
||||
ngModel.$setValidity("compareTo", value === scope.otherModelValue);
|
||||
};
|
||||
|
||||
scope.$watch("otherModelValue", function() {
|
||||
validate(ngModel.$modelValue);
|
||||
});
|
||||
|
||||
ngModel.$parsers.push(function(value) {
|
||||
validate(value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
directives.directive('onDestroy', function () {
|
||||
/* This directive can be used to invoke a callback when an element is destroyed,
|
||||
A useful example is the following:
|
||||
<div ng-if="includeText" on-destroy="form.text = null;">
|
||||
<input type="text" ng-model="form.text">
|
||||
</div>
|
||||
*/
|
||||
return {
|
||||
restrict: "A",
|
||||
scope: {
|
||||
onDestroy: "&",
|
||||
},
|
||||
link: function(scope, elem, attrs) {
|
||||
scope.$on('$destroy', function() {
|
||||
scope.onDestroy();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
directives.directive('colorBox', function () {
|
||||
return {
|
||||
restrict: "E",
|
||||
scope: {color: "="},
|
||||
template: "<span style='width: 12px; height: 12px; background-color: {{color}}; display: inline-block; margin-right: 5px;'></span>"
|
||||
};
|
||||
});
|
||||
|
||||
directives.directive('overlay', function() {
|
||||
return {
|
||||
restrict: "E",
|
||||
transclude: true,
|
||||
template: "" +
|
||||
'<div>' +
|
||||
'<div class="overlay"></div>' +
|
||||
'<div style="width: 100%; position:absolute; top:50px; z-index:2000">' +
|
||||
'<div class="well well-lg" style="width: 70%; margin: auto;" ng-transclude>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
}
|
||||
});
|
||||
|
||||
directives.directive('tabNav', ['$location', function($location) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
scope: {
|
||||
tabs: '='
|
||||
},
|
||||
template: '<ul class="tab-nav bg-white">' +
|
||||
'<li ng-repeat="tab in tabs" ng-class="{\'active\': tab.active }"><a ng-href="{{tab.path}}">{{tab.name}}</a></li>' +
|
||||
'</ul>',
|
||||
link: function($scope) {
|
||||
_.each($scope.tabs, function(tab) {
|
||||
if (tab.isActive) {
|
||||
tab.active = tab.isActive($location.path());
|
||||
} else {
|
||||
tab.active = _.string.startsWith($location.path(), "/" + tab.path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
||||
|
||||
directives.directive('queriesList', [function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
queries: '=',
|
||||
total: '=',
|
||||
selectPage: '=',
|
||||
page: '=',
|
||||
pageSize: '='
|
||||
},
|
||||
templateUrl: '/views/directives/queries_list.html',
|
||||
link: function ($scope) {
|
||||
function hasNext() {
|
||||
return !($scope.page * $scope.pageSize >= $scope.total);
|
||||
}
|
||||
|
||||
function hasPrevious() {
|
||||
return $scope.page !== 1;
|
||||
}
|
||||
|
||||
function updatePages() {
|
||||
if ($scope.total === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var maxSize = 5;
|
||||
var pageCount = Math.ceil($scope.total/$scope.pageSize);
|
||||
var pages = [];
|
||||
|
||||
function makePage(title, page, disabled) {
|
||||
return {title: title, page: page, active: page == $scope.page, disabled: disabled};
|
||||
}
|
||||
|
||||
// Default page limits
|
||||
var startPage = 1, endPage = pageCount;
|
||||
|
||||
// recompute if maxSize
|
||||
if (maxSize && maxSize < pageCount) {
|
||||
startPage = Math.max($scope.page - Math.floor(maxSize / 2), 1);
|
||||
endPage = startPage + maxSize - 1;
|
||||
|
||||
// Adjust if limit is exceeded
|
||||
if (endPage > pageCount) {
|
||||
endPage = pageCount;
|
||||
startPage = endPage - maxSize + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Add page number links
|
||||
for (var number = startPage; number <= endPage; number++) {
|
||||
var page = makePage(number, number, false);
|
||||
pages.push(page);
|
||||
}
|
||||
|
||||
// Add previous & next links
|
||||
var previousPage = makePage('<', $scope.page - 1, !hasPrevious());
|
||||
pages.unshift(previousPage);
|
||||
|
||||
var nextPage = makePage('>', $scope.page + 1, !hasNext());
|
||||
pages.push(nextPage);
|
||||
|
||||
$scope.pages = pages;
|
||||
}
|
||||
|
||||
$scope.$watch('total', updatePages);
|
||||
$scope.$watch('page', updatePages);
|
||||
}
|
||||
}
|
||||
}]);
|
||||
})();
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
(function() {
|
||||
'use strict'
|
||||
|
||||
function queryFormatter($http, growl) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
// don't create new scope to avoid ui-codemirror bug
|
||||
// seehttps://github.com/angular-ui/ui-codemirror/pull/37
|
||||
scope: false,
|
||||
template: '<button type="button" class="btn btn-default btn-s"\
|
||||
ng-click="formatQuery()">\
|
||||
<span class="zmdi zmdi-format-indent-increase"></span>\
|
||||
Format Query\
|
||||
</button>',
|
||||
link: function($scope) {
|
||||
$scope.formatQuery = function formatQuery() {
|
||||
if ($scope.dataSource.syntax == 'json') {
|
||||
try {
|
||||
$scope.query.query = JSON.stringify(JSON.parse($scope.query.query), ' ', 4);
|
||||
} catch(err) {
|
||||
growl.addErrorMessage(err);
|
||||
}
|
||||
} else if ($scope.dataSource.syntax =='sql') {
|
||||
|
||||
$scope.queryFormatting = true;
|
||||
$http.post('api/queries/format', {
|
||||
'query': $scope.query.query
|
||||
}).success(function (response) {
|
||||
$scope.query.query = response;
|
||||
}).finally(function () {
|
||||
$scope.queryFormatting = false;
|
||||
});
|
||||
} else {
|
||||
growl.addInfoMessage("Query formatting is not supported for your data source syntax.");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function queryTimePicker() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<select ng-disabled="refreshType != \'daily\'" ng-model="hour" ng-change="updateSchedule()" ng-options="c as c for c in hourOptions"></select> :\
|
||||
<select ng-disabled="refreshType != \'daily\'" ng-model="minute" ng-change="updateSchedule()" ng-options="c as c for c in minuteOptions"></select>',
|
||||
link: function($scope) {
|
||||
var padWithZeros = function(size, v) {
|
||||
v = String(v);
|
||||
if (v.length < size) {
|
||||
v = "0" + v;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
$scope.hourOptions = _.map(_.range(0, 24), _.partial(padWithZeros, 2));
|
||||
$scope.minuteOptions = _.map(_.range(0, 60, 5), _.partial(padWithZeros, 2));
|
||||
|
||||
if ($scope.query.hasDailySchedule()) {
|
||||
var parts = $scope.query.scheduleInLocalTime().split(':');
|
||||
$scope.minute = parts[1];
|
||||
$scope.hour = parts[0];
|
||||
} else {
|
||||
$scope.minute = "15";
|
||||
$scope.hour = "00";
|
||||
}
|
||||
|
||||
$scope.updateSchedule = function() {
|
||||
var newSchedule = moment().hour($scope.hour).minute($scope.minute).utc().format('HH:mm');
|
||||
if (newSchedule != $scope.query.schedule) {
|
||||
$scope.query.schedule = newSchedule;
|
||||
$scope.saveQuery();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('refreshType', function() {
|
||||
if ($scope.refreshType == 'daily') {
|
||||
$scope.updateSchedule();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function queryRefreshSelect() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<select\
|
||||
ng-disabled="refreshType != \'periodic\'"\
|
||||
ng-model="query.schedule"\
|
||||
ng-change="saveQuery()"\
|
||||
ng-options="c.value as c.name for c in refreshOptions">\
|
||||
<option value="">No Refresh</option>\
|
||||
</select>',
|
||||
link: function($scope) {
|
||||
$scope.refreshOptions = [
|
||||
{
|
||||
value: "60",
|
||||
name: 'Every minute'
|
||||
}
|
||||
];
|
||||
|
||||
_.each([5, 10, 15, 30], function(i) {
|
||||
$scope.refreshOptions.push({
|
||||
value: String(i*60),
|
||||
name: "Every " + i + " minutes"
|
||||
})
|
||||
});
|
||||
|
||||
_.each(_.range(1, 13), function (i) {
|
||||
$scope.refreshOptions.push({
|
||||
value: String(i * 3600),
|
||||
name: 'Every ' + i + 'h'
|
||||
});
|
||||
})
|
||||
|
||||
$scope.refreshOptions.push({
|
||||
value: String(24 * 3600),
|
||||
name: 'Every 24h'
|
||||
});
|
||||
$scope.refreshOptions.push({
|
||||
value: String(7 * 24 * 3600),
|
||||
name: 'Every 7 days'
|
||||
});
|
||||
$scope.refreshOptions.push({
|
||||
value: String(14 * 24 * 3600),
|
||||
name: 'Every 14 days'
|
||||
});
|
||||
$scope.refreshOptions.push({
|
||||
value: String(30 * 24 * 3600),
|
||||
name: 'Every 30 days'
|
||||
});
|
||||
|
||||
$scope.$watch('refreshType', function() {
|
||||
if ($scope.refreshType == 'periodic') {
|
||||
if ($scope.query.hasDailySchedule()) {
|
||||
$scope.query.schedule = null;
|
||||
$scope.saveQuery();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('redash.directives')
|
||||
.directive('queryLink', queryLink)
|
||||
.directive('querySourceLink', ['$location', querySourceLink])
|
||||
.directive('queryResultLink', queryResultLink)
|
||||
.directive('queryEditor', ['QuerySnippet', queryEditor])
|
||||
.directive('queryRefreshSelect', queryRefreshSelect)
|
||||
.directive('queryTimePicker', queryTimePicker)
|
||||
.directive('schemaBrowser', schemaBrowser)
|
||||
.directive('queryFormatter', ['$http', 'growl', queryFormatter]);
|
||||
})();
|
||||
@@ -1,133 +0,0 @@
|
||||
var durationHumanize = function (duration) {
|
||||
var humanized = "";
|
||||
if (duration == undefined) {
|
||||
humanized = "-";
|
||||
} else if (duration < 60) {
|
||||
humanized = Math.round(duration) + "s";
|
||||
} else if (duration > 3600 * 24) {
|
||||
var days = Math.round(parseFloat(duration) / 60.0 / 60.0 / 24.0);
|
||||
humanized = days + "days";
|
||||
} else if (duration >= 3600) {
|
||||
var hours = Math.round(parseFloat(duration) / 60.0 / 60.0);
|
||||
humanized = hours + "h";
|
||||
} else {
|
||||
var minutes = Math.round(parseFloat(duration) / 60.0);
|
||||
humanized = minutes + "m";
|
||||
}
|
||||
return humanized;
|
||||
};
|
||||
|
||||
var urlPattern = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
|
||||
|
||||
angular.module('redash.filters', []).
|
||||
filter('durationHumanize', function () {
|
||||
return durationHumanize;
|
||||
})
|
||||
|
||||
.filter('scheduleHumanize', function() {
|
||||
return function (schedule) {
|
||||
if (schedule === null) {
|
||||
return "Never";
|
||||
} else if (schedule.match(/\d\d:\d\d/) !== null) {
|
||||
var parts = schedule.split(':');
|
||||
var localTime = moment.utc().hour(parts[0]).minute(parts[1]).local().format('HH:mm');
|
||||
return "Every day at " + localTime;
|
||||
}
|
||||
|
||||
return "Every " + durationHumanize(parseInt(schedule));
|
||||
}
|
||||
})
|
||||
|
||||
.filter('toHuman', function () {
|
||||
return function (text) {
|
||||
return text.replace(/_/g, ' ').replace(/(?:^|\s)\S/g, function (a) {
|
||||
return a.toUpperCase();
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
.filter('colWidth', function () {
|
||||
return function (widgetWidth) {
|
||||
if (widgetWidth === 0) {
|
||||
return 0;
|
||||
} else if (widgetWidth === 1) {
|
||||
return 6;
|
||||
} else if (widgetWidth === 2) {
|
||||
return 12;
|
||||
}
|
||||
return widgetWidth;
|
||||
};
|
||||
})
|
||||
|
||||
.filter('capitalize', function () {
|
||||
return function (text) {
|
||||
if (text) {
|
||||
return _.str.capitalize(text);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
.filter('dateTime', function() {
|
||||
return function(value) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
return moment(value).format(clientConfig.dateTimeFormat);
|
||||
}
|
||||
})
|
||||
|
||||
.filter('linkify', function () {
|
||||
return function (text) {
|
||||
return text.replace(urlPattern, "$1<a href='$2' target='_blank'>$2</a>");
|
||||
};
|
||||
})
|
||||
|
||||
.filter('markdown', ['$sce', function ($sce) {
|
||||
return function (text) {
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var html = marked(text);
|
||||
if (clientConfig.allowScriptsInUserInput) {
|
||||
html = $sce.trustAsHtml(html);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
}])
|
||||
|
||||
.filter('trustAsHtml', ['$sce', function ($sce) {
|
||||
return function (text) {
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
return $sce.trustAsHtml(text);
|
||||
}
|
||||
}])
|
||||
|
||||
.filter('remove', function() {
|
||||
return function(items, item) {
|
||||
if (items == undefined)
|
||||
return items;
|
||||
if (item instanceof Array) {
|
||||
var notEquals = function(other) { return item.indexOf(other) == -1; }
|
||||
} else {
|
||||
var notEquals = function(other) { return item != other; }
|
||||
}
|
||||
var filtered = [];
|
||||
for (var i = 0; i < items.length; i++)
|
||||
if (notEquals(items[i]))
|
||||
filtered.push(items[i])
|
||||
return filtered;
|
||||
};
|
||||
})
|
||||
|
||||
.filter('notEmpty', function() {
|
||||
return function(collection) {
|
||||
return !_.isEmpty(collection);
|
||||
}
|
||||
});
|
||||
@@ -1,977 +0,0 @@
|
||||
/* Column module */
|
||||
|
||||
function getNestedValue (obj, keys) {
|
||||
if (keys.length == 1) {
|
||||
return obj[keys[0]];
|
||||
}
|
||||
return getNestedValue(obj[keys[0]], keys.splice(1));
|
||||
}
|
||||
|
||||
function getKeyFromObject(obj, key) {
|
||||
var value = obj[key];
|
||||
|
||||
if ((!_.has(obj, key) && _.string.include(key, '.'))) {
|
||||
var keys = key.split(".");
|
||||
|
||||
value = getNestedValue(obj, keys);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
(function (global, angular) {
|
||||
"use strict";
|
||||
|
||||
var smartTableColumnModule = angular.module('smartTable.column', ['smartTable.templateUrlList']).constant('DefaultColumnConfiguration', {
|
||||
isSortable: true,
|
||||
isEditable: false,
|
||||
type: 'text',
|
||||
|
||||
|
||||
//it is useless to have that empty strings, but it reminds what is available
|
||||
headerTemplateUrl: '',
|
||||
map: '',
|
||||
label: '',
|
||||
sortPredicate: '',
|
||||
formatFunction: '',
|
||||
formatParameter: '',
|
||||
filterPredicate: undefined,
|
||||
cellTemplateUrl: '',
|
||||
headerClass: '',
|
||||
cellClass: ''
|
||||
});
|
||||
|
||||
function ColumnProvider(DefaultColumnConfiguration, templateUrlList) {
|
||||
|
||||
function Column(config) {
|
||||
if (!(this instanceof Column)) {
|
||||
return new Column(config);
|
||||
}
|
||||
angular.extend(this, config);
|
||||
}
|
||||
|
||||
this.setDefaultOption = function (option) {
|
||||
angular.extend(Column.prototype, option);
|
||||
};
|
||||
|
||||
DefaultColumnConfiguration.headerTemplateUrl = templateUrlList.defaultHeader;
|
||||
this.setDefaultOption(DefaultColumnConfiguration);
|
||||
|
||||
this.$get = function () {
|
||||
return Column;
|
||||
};
|
||||
}
|
||||
|
||||
ColumnProvider.$inject = ['DefaultColumnConfiguration', 'templateUrlList'];
|
||||
smartTableColumnModule.provider('Column', ColumnProvider);
|
||||
|
||||
//make it global so it can be tested
|
||||
global.ColumnProvider = ColumnProvider;
|
||||
})(window, angular);
|
||||
|
||||
|
||||
/* Directives */
|
||||
(function (angular) {
|
||||
"use strict";
|
||||
|
||||
angular.module('smartTable.directives', ['smartTable.templateUrlList', 'smartTable.templates'])
|
||||
.directive('smartTable', ['templateUrlList', 'DefaultTableConfiguration', function (templateList, defaultConfig) {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
scope: {
|
||||
columnCollection: '=columns',
|
||||
dataCollection: '=rows',
|
||||
config: '='
|
||||
},
|
||||
replace: 'true',
|
||||
templateUrl: templateList.smartTable,
|
||||
controller: 'TableCtrl',
|
||||
link: function (scope, element, attr, ctrl) {
|
||||
|
||||
var templateObject;
|
||||
|
||||
scope.$watch('config', function (config) {
|
||||
var newConfig = angular.extend({}, defaultConfig, config),
|
||||
length = scope.columns !== undefined ? scope.columns.length : 0;
|
||||
|
||||
ctrl.setGlobalConfig(newConfig);
|
||||
|
||||
//remove the checkbox column if needed
|
||||
if (newConfig.selectionMode !== 'multiple' || newConfig.displaySelectionCheckbox !== true) {
|
||||
for (var i = length - 1; i >= 0; i--) {
|
||||
if (scope.columns[i].isSelectionColumn === true) {
|
||||
ctrl.removeColumn(i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//add selection box column if required
|
||||
ctrl.insertColumn({
|
||||
cellTemplateUrl: templateList.selectionCheckbox,
|
||||
headerTemplateUrl: templateList.selectAllCheckbox,
|
||||
isSelectionColumn: true
|
||||
}, 0);
|
||||
}
|
||||
}, true);
|
||||
|
||||
//insert columns from column config
|
||||
//TODO add a way to clean all columns
|
||||
scope.$watchCollection('columnCollection', function (oldValue, newValue) {
|
||||
if (scope.columnCollection) {
|
||||
scope.columns.length = 0;
|
||||
for (var i = 0, l = scope.columnCollection.length; i < l; i++) {
|
||||
|
||||
ctrl.insertColumn(scope.columnCollection[i]);
|
||||
}
|
||||
} else {
|
||||
//or guess data Structure
|
||||
if (scope.dataCollection && scope.dataCollection.length > 0) {
|
||||
templateObject = scope.dataCollection[0];
|
||||
angular.forEach(templateObject, function (value, key) {
|
||||
if (key[0] != '$') {
|
||||
ctrl.insertColumn({label: key, map: key});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
//if item are added or removed into the data model from outside the grid
|
||||
scope.$watch('dataCollection', function (oldValue, newValue) {
|
||||
// evme:
|
||||
// reset sorting when data updates (executing query again)
|
||||
if (newValue) {
|
||||
ctrl.resetSort();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}])
|
||||
//just to be able to select the row
|
||||
.directive('smartTableDataRow', function () {
|
||||
|
||||
return {
|
||||
require: '^smartTable',
|
||||
restrict: 'C',
|
||||
link: function (scope, element, attr, ctrl) {
|
||||
|
||||
element.bind('click', function () {
|
||||
scope.$apply(function () {
|
||||
ctrl.toggleSelection(scope.dataRow);
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
//header cell with sorting functionality or put a checkbox if this column is a selection column
|
||||
.directive('smartTableHeaderCell', function () {
|
||||
return {
|
||||
restrict: 'C',
|
||||
require: '^smartTable',
|
||||
link: function (scope, element, attr, ctrl) {
|
||||
element.bind('click', function () {
|
||||
scope.$apply(function () {
|
||||
ctrl.sortBy(scope.column);
|
||||
});
|
||||
})
|
||||
}
|
||||
};
|
||||
}).directive('smartTableSelectAll', function () {
|
||||
return {
|
||||
restrict: 'C',
|
||||
require: '^smartTable',
|
||||
link: function (scope, element, attr, ctrl) {
|
||||
element.bind('click', function (event) {
|
||||
ctrl.toggleSelectionAll(element[0].checked === true);
|
||||
})
|
||||
}
|
||||
};
|
||||
})
|
||||
//credit to Valentyn shybanov : http://stackoverflow.com/questions/14544741/angularjs-directive-to-stoppropagation
|
||||
.directive('stopEvent', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attr) {
|
||||
element.bind(attr.stopEvent, function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
//the global filter
|
||||
.directive('smartTableGlobalSearch', ['templateUrlList', function (templateList) {
|
||||
return {
|
||||
restrict: 'C',
|
||||
require: '^smartTable',
|
||||
scope: {
|
||||
columnSpan: '@'
|
||||
},
|
||||
templateUrl: templateList.smartTableGlobalSearch,
|
||||
replace: false,
|
||||
link: function (scope, element, attr, ctrl) {
|
||||
|
||||
scope.searchValue = undefined;
|
||||
|
||||
scope.$watch('searchValue', function (value) {
|
||||
//todo perf improvement only filter on blur ?
|
||||
ctrl.search(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}])
|
||||
//a customisable cell (see templateUrl) and editable
|
||||
//TODO check with the ng-include strategy
|
||||
.directive('smartTableDataCell', ['$filter', '$http', '$templateCache', '$compile', '$parse', '$sanitize', function (filter, http, templateCache, compile, parse, sanitize) {
|
||||
return {
|
||||
restrict: 'C',
|
||||
link: function (scope, element) {
|
||||
var
|
||||
column = scope.column,
|
||||
row = scope.dataRow,
|
||||
format = filter('format'),
|
||||
childScope;
|
||||
|
||||
var value = getKeyFromObject(row, column.map);
|
||||
|
||||
//can be useful for child directives
|
||||
scope.formatedValue = format(value, column.formatFunction, column.formatParameter);
|
||||
|
||||
function defaultContent() {
|
||||
//clear content
|
||||
if (column.isEditable) {
|
||||
element.html('<div editable-cell="" row="dataRow" column="column" type="column.type"></div>');
|
||||
compile(element.contents())(scope);
|
||||
} else if (column.cellTemplate) {
|
||||
//create a scope
|
||||
childScope = scope.$new();
|
||||
//compile the element with its new content and new scope
|
||||
element.html(column.cellTemplate);
|
||||
compile(element.contents())(childScope);
|
||||
} else {
|
||||
if (typeof scope.formatedValue === 'string' || scope.formatedValue instanceof String) {
|
||||
element.html(sanitize(scope.formatedValue));
|
||||
} else {
|
||||
element.text(scope.formatedValue);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
scope.$watch('column.cellTemplateUrl', function (value) {
|
||||
|
||||
if (value) {
|
||||
//we have to load the template (and cache it) : a kind of ngInclude
|
||||
http.get(value, {cache: templateCache}).success(function (response) {
|
||||
|
||||
//create a scope
|
||||
childScope = scope.$new();
|
||||
//compile the element with its new content and new scope
|
||||
element.html(response);
|
||||
compile(element.contents())(childScope);
|
||||
}).error(defaultContent);
|
||||
|
||||
} else {
|
||||
defaultContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
//directive that allows type to be bound in input
|
||||
.directive('inputType', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
priority: 1,
|
||||
link: function (scope, ielement, iattr) {
|
||||
//force the type to be set before inputDirective is called
|
||||
var type = scope.$eval(iattr.type);
|
||||
iattr.$set('type', type);
|
||||
}
|
||||
};
|
||||
})
|
||||
//an editable content in the context of a cell (see row, column)
|
||||
.directive('editableCell', ['templateUrlList', '$parse', function (templateList, parse) {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
require: '^smartTable',
|
||||
templateUrl: templateList.editableCell,
|
||||
scope: {
|
||||
row: '=',
|
||||
column: '=',
|
||||
type: '='
|
||||
},
|
||||
replace: true,
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
var form = angular.element(element.children()[1]),
|
||||
input = angular.element(form.children()[0]);
|
||||
|
||||
//init values
|
||||
scope.isEditMode = false;
|
||||
scope.value = scope.row[scope.column.map];
|
||||
|
||||
|
||||
scope.submit = function () {
|
||||
//update model if valid
|
||||
if (scope.myForm.$valid === true) {
|
||||
ctrl.updateDataRow(scope.row, scope.column.map, scope.value);
|
||||
ctrl.sortBy();//it will trigger the refresh... (ie it will sort, filter, etc with the new value)
|
||||
}
|
||||
scope.toggleEditMode();
|
||||
};
|
||||
|
||||
scope.toggleEditMode = function () {
|
||||
scope.value = scope.row[scope.column.map];
|
||||
scope.isEditMode = scope.isEditMode !== true;
|
||||
};
|
||||
|
||||
scope.$watch('isEditMode', function (newValue, oldValue) {
|
||||
if (newValue) {
|
||||
input[0].select();
|
||||
input[0].focus();
|
||||
}
|
||||
});
|
||||
|
||||
input.bind('blur', function () {
|
||||
scope.$apply(function () {
|
||||
scope.submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
})(angular);
|
||||
|
||||
/* Filters */
|
||||
(function (angular) {
|
||||
"use strict";
|
||||
angular.module('smartTable.filters', []).
|
||||
constant('DefaultFilters', ['currency', 'date', 'json', 'lowercase', 'number', 'uppercase']).
|
||||
filter('format', ['$filter', 'DefaultFilters', function (filter, defaultfilters) {
|
||||
return function (value, formatFunction, filterParameter) {
|
||||
|
||||
var returnFunction;
|
||||
|
||||
if (formatFunction && angular.isFunction(formatFunction)) {
|
||||
returnFunction = formatFunction;
|
||||
} else {
|
||||
returnFunction = defaultfilters.indexOf(formatFunction) !== -1 ? filter(formatFunction) : function (value) {
|
||||
return value;
|
||||
};
|
||||
}
|
||||
return returnFunction(value, filterParameter);
|
||||
};
|
||||
}]);
|
||||
})(angular);
|
||||
|
||||
|
||||
/*table module */
|
||||
|
||||
(function (angular) {
|
||||
"use strict";
|
||||
angular.module('smartTable.table', ['smartTable.column', 'smartTable.utilities', 'smartTable.directives', 'smartTable.filters', 'ui.bootstrap.pagination.smartTable'])
|
||||
.constant('DefaultTableConfiguration', {
|
||||
selectionMode: 'none',
|
||||
isGlobalSearchActivated: false,
|
||||
displaySelectionCheckbox: false,
|
||||
isPaginationEnabled: true,
|
||||
itemsByPage: 10,
|
||||
maxSize: 5,
|
||||
|
||||
//just to remind available option
|
||||
sortAlgorithm: '',
|
||||
filterAlgorithm: ''
|
||||
})
|
||||
.controller('TableCtrl', ['$scope', 'Column', '$filter', '$parse', 'ArrayUtility', 'DefaultTableConfiguration', function (scope, Column, filter, parse, arrayUtility, defaultConfig) {
|
||||
|
||||
scope.columns = [];
|
||||
scope.dataCollection = scope.dataCollection || [];
|
||||
scope.displayedCollection = []; //init empty array so that if pagination is enabled, it does not spoil performances
|
||||
scope.numberOfPages = calculateNumberOfPages(scope.dataCollection);
|
||||
scope.currentPage = 1;
|
||||
scope.holder = {isAllSelected: false};
|
||||
|
||||
var predicate = {},
|
||||
lastColumnSort;
|
||||
|
||||
function isAllSelected() {
|
||||
var i,
|
||||
l = scope.displayedCollection.length;
|
||||
for (i = 0; i < l; i++) {
|
||||
if (scope.displayedCollection[i].isSelected !== true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function calculateNumberOfPages(array) {
|
||||
|
||||
if (!angular.isArray(array)) {
|
||||
return 1;
|
||||
}
|
||||
if (array.length === 0 || scope.itemsByPage < 1) {
|
||||
return 1;
|
||||
}
|
||||
return Math.ceil(array.length / scope.itemsByPage);
|
||||
}
|
||||
|
||||
function sortDataRow(array, column) {
|
||||
var sortAlgo = (scope.sortAlgorithm && angular.isFunction(scope.sortAlgorithm)) === true ? scope.sortAlgorithm : filter('orderBy');
|
||||
if (column) {
|
||||
var predicate = function (o) {
|
||||
return getKeyFromObject(o, column.sortPredicate);
|
||||
};
|
||||
return arrayUtility.sort(array, sortAlgo, predicate, column.reverse);
|
||||
} else {
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
function selectDataRow(array, selectionMode, index, select) {
|
||||
|
||||
var dataRow, oldValue;
|
||||
|
||||
if ((!angular.isArray(array)) || (selectionMode !== 'multiple' && selectionMode !== 'single')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index >= 0 && index < array.length) {
|
||||
dataRow = array[index];
|
||||
if (selectionMode === 'single') {
|
||||
//unselect all the others
|
||||
for (var i = 0, l = array.length; i < l; i++) {
|
||||
oldValue = array[i].isSelected;
|
||||
array[i].isSelected = false;
|
||||
if (oldValue === true) {
|
||||
scope.$emit('selectionChange', {item: array[i]});
|
||||
}
|
||||
}
|
||||
}
|
||||
dataRow.isSelected = select;
|
||||
scope.holder.isAllSelected = isAllSelected();
|
||||
scope.$emit('selectionChange', {item: dataRow});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* set the config (config parameters will be available through scope
|
||||
* @param config
|
||||
*/
|
||||
this.setGlobalConfig = function (config) {
|
||||
angular.extend(scope, defaultConfig, config);
|
||||
};
|
||||
|
||||
/**
|
||||
* change the current page displayed
|
||||
* @param page
|
||||
*/
|
||||
this.changePage = function (page) {
|
||||
var oldPage = scope.currentPage;
|
||||
if (angular.isNumber(page.page)) {
|
||||
scope.currentPage = page.page;
|
||||
scope.displayedCollection = this.pipe(scope.dataCollection);
|
||||
scope.holder.isAllSelected = isAllSelected();
|
||||
scope.$emit('changePage', {oldValue: oldPage, newValue: scope.currentPage});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* set column as the column used to sort the data (if it is already the case, it will change the reverse value)
|
||||
* @method sortBy
|
||||
* @param column
|
||||
*/
|
||||
this.sortBy = function (column) {
|
||||
var index = scope.columns.indexOf(column);
|
||||
if (index !== -1) {
|
||||
if (column.isSortable === true) {
|
||||
// reset the last column used
|
||||
if (lastColumnSort && lastColumnSort !== column) {
|
||||
lastColumnSort.reverse = 'none';
|
||||
}
|
||||
|
||||
column.sortPredicate = column.sortPredicate || column.map;
|
||||
column.reverse = column.reverse !== true;
|
||||
lastColumnSort = column;
|
||||
}
|
||||
}
|
||||
|
||||
scope.displayedCollection = this.pipe(scope.dataCollection);
|
||||
};
|
||||
|
||||
/**
|
||||
* set the filter predicate used for searching
|
||||
* @param input
|
||||
* @param column
|
||||
*/
|
||||
this.search = function (input, column) {
|
||||
//update column and global predicate
|
||||
if (column && scope.columns.indexOf(column) !== -1) {
|
||||
predicate.$ = '';
|
||||
column.filterPredicate = input;
|
||||
} else {
|
||||
for (var j = 0, l = scope.columns.length; j < l; j++) {
|
||||
scope.columns[j].filterPredicate = undefined;
|
||||
}
|
||||
predicate.$ = input;
|
||||
}
|
||||
|
||||
for (var j = 0, l = scope.columns.length; j < l; j++) {
|
||||
predicate[scope.columns[j].map] = scope.columns[j].filterPredicate;
|
||||
}
|
||||
scope.displayedCollection = this.pipe(scope.dataCollection);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* combine sort, search and limitTo operations on an array,
|
||||
* @param array
|
||||
* @returns Array, an array result of the operations on input array
|
||||
*/
|
||||
this.pipe = function (array) {
|
||||
var filterAlgo = (scope.filterAlgorithm && angular.isFunction(scope.filterAlgorithm)) === true ? scope.filterAlgorithm : filter('filter'),
|
||||
output;
|
||||
//filter and sort are commutative
|
||||
output = sortDataRow(arrayUtility.filter(array, filterAlgo, predicate), lastColumnSort);
|
||||
scope.numberOfPages = calculateNumberOfPages(output);
|
||||
return scope.isPaginationEnabled ? arrayUtility.fromTo(output, (scope.currentPage - 1) * scope.itemsByPage, scope.itemsByPage) : output;
|
||||
};
|
||||
|
||||
this.resetSort = function () {
|
||||
lastColumnSort = null;
|
||||
predicate = {};
|
||||
this.sortBy();
|
||||
};
|
||||
|
||||
/*////////////
|
||||
Column API
|
||||
///////////*/
|
||||
|
||||
|
||||
/**
|
||||
* insert a new column in scope.collection at index or push at the end if no index
|
||||
* @param columnConfig column configuration used to instantiate the new Column
|
||||
* @param index where to insert the column (at the end if not specified)
|
||||
*/
|
||||
this.insertColumn = function (columnConfig, index) {
|
||||
var column = new Column(columnConfig);
|
||||
arrayUtility.insertAt(scope.columns, index, column);
|
||||
};
|
||||
|
||||
/**
|
||||
* remove the column at columnIndex from scope.columns
|
||||
* @param columnIndex index of the column to be removed
|
||||
*/
|
||||
this.removeColumn = function (columnIndex) {
|
||||
arrayUtility.removeAt(scope.columns, columnIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* move column located at oldIndex to the newIndex in scope.columns
|
||||
* @param oldIndex index of the column before it is moved
|
||||
* @param newIndex index of the column after the column is moved
|
||||
*/
|
||||
this.moveColumn = function (oldIndex, newIndex) {
|
||||
arrayUtility.moveAt(scope.columns, oldIndex, newIndex);
|
||||
};
|
||||
|
||||
|
||||
/*///////////
|
||||
ROW API
|
||||
*/
|
||||
|
||||
/**
|
||||
* select or unselect the item of the displayedCollection with the selection mode set in the scope
|
||||
* @param dataRow
|
||||
*/
|
||||
this.toggleSelection = function (dataRow) {
|
||||
var index = scope.dataCollection.indexOf(dataRow);
|
||||
if (index !== -1) {
|
||||
selectDataRow(scope.dataCollection, scope.selectionMode, index, dataRow.isSelected !== true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* select/unselect all the currently displayed rows
|
||||
* @param value if true select, else unselect
|
||||
*/
|
||||
this.toggleSelectionAll = function (value) {
|
||||
var i = 0,
|
||||
l = scope.displayedCollection.length;
|
||||
|
||||
if (scope.selectionMode !== 'multiple') {
|
||||
return;
|
||||
}
|
||||
for (; i < l; i++) {
|
||||
selectDataRow(scope.displayedCollection, scope.selectionMode, i, value === true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* remove the item at index rowIndex from the displayed collection
|
||||
* @param rowIndex
|
||||
* @returns {*} item just removed or undefined
|
||||
*/
|
||||
this.removeDataRow = function (rowIndex) {
|
||||
var toRemove = arrayUtility.removeAt(scope.displayedCollection, rowIndex);
|
||||
arrayUtility.removeAt(scope.dataCollection, scope.dataCollection.indexOf(toRemove));
|
||||
};
|
||||
|
||||
/**
|
||||
* move an item from oldIndex to newIndex in displayedCollection
|
||||
* @param oldIndex
|
||||
* @param newIndex
|
||||
*/
|
||||
this.moveDataRow = function (oldIndex, newIndex) {
|
||||
arrayUtility.moveAt(scope.displayedCollection, oldIndex, newIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* update the model, it can be a non existing yet property
|
||||
* @param dataRow the dataRow to update
|
||||
* @param propertyName the property on the dataRow ojbect to update
|
||||
* @param newValue the value to set
|
||||
*/
|
||||
this.updateDataRow = function (dataRow, propertyName, newValue) {
|
||||
var index = scope.displayedCollection.indexOf(dataRow),
|
||||
oldValue;
|
||||
if (index !== -1) {
|
||||
oldValue = scope.displayedCollection[index][propertyName];
|
||||
if (oldValue !== newValue) {
|
||||
scope.displayedCollection[index][propertyName] = newValue;
|
||||
scope.$emit('updateDataRow', {item: scope.displayedCollection[index]});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}]);
|
||||
|
||||
})(angular);
|
||||
|
||||
|
||||
angular.module('smartTable.templates', ['partials/defaultCell.html', 'partials/defaultHeader.html', 'partials/editableCell.html', 'partials/globalSearchCell.html', 'partials/pagination.html', 'partials/selectAllCheckbox.html', 'partials/selectionCheckbox.html', 'partials/smartTable.html']);
|
||||
|
||||
angular.module("partials/defaultCell.html", []).run(["$templateCache", function ($templateCache) {
|
||||
$templateCache.put("partials/defaultCell.html",
|
||||
"{{formatedValue}}");
|
||||
}]);
|
||||
|
||||
angular.module("partials/defaultHeader.html", []).run(["$templateCache", function ($templateCache) {
|
||||
$templateCache.put("partials/defaultHeader.html",
|
||||
"<span class=\"header-content\" ng-class=\"{'sort-ascent':column.reverse==true,'sort-descent':column.reverse==false}\">{{column.label}}</span>");
|
||||
}]);
|
||||
|
||||
angular.module("partials/editableCell.html", []).run(["$templateCache", function ($templateCache) {
|
||||
$templateCache.put("partials/editableCell.html",
|
||||
"<div ng-dblclick=\"toggleEditMode($event)\">\n" +
|
||||
" <span ng-hide=\"isEditMode\">{{value | format:column.formatFunction:column.formatParameter}}</span>\n" +
|
||||
"\n" +
|
||||
" <form ng-submit=\"submit()\" ng-show=\"isEditMode\" name=\"myForm\">\n" +
|
||||
" <input name=\"myInput\" ng-model=\"value\" type=\"type\" input-type/>\n" +
|
||||
" </form>\n" +
|
||||
"</div>");
|
||||
}]);
|
||||
|
||||
angular.module("partials/globalSearchCell.html", []).run(["$templateCache", function ($templateCache) {
|
||||
$templateCache.put("partials/globalSearchCell.html",
|
||||
"<label>Search :</label>\n" +
|
||||
"<input type=\"text\" ng-model=\"searchValue\"/>");
|
||||
}]);
|
||||
|
||||
angular.module("partials/pagination.html", []).run(["$templateCache", function ($templateCache) {
|
||||
$templateCache.put("partials/pagination.html",
|
||||
"<ul class=\"pagination\">\n" +
|
||||
" <li ng-repeat=\"page in pages\" ng-class=\"{active: page.active, disabled: page.disabled}\"><a\n" +
|
||||
" ng-click=\"selectPage(page.number)\">{{page.text}}</a></li>\n" +
|
||||
"</ul> ");
|
||||
}]);
|
||||
|
||||
angular.module("partials/selectAllCheckbox.html", []).run(["$templateCache", function ($templateCache) {
|
||||
$templateCache.put("partials/selectAllCheckbox.html",
|
||||
"<input class=\"smart-table-select-all\" type=\"checkbox\" ng-model=\"holder.isAllSelected\"/>");
|
||||
}]);
|
||||
|
||||
angular.module("partials/selectionCheckbox.html", []).run(["$templateCache", function ($templateCache) {
|
||||
$templateCache.put("partials/selectionCheckbox.html",
|
||||
"<input type=\"checkbox\" ng-model=\"dataRow.isSelected\" stop-event=\"click\"/>");
|
||||
}]);
|
||||
|
||||
angular.module("partials/smartTable.html", []).run(["$templateCache", function ($templateCache) {
|
||||
$templateCache.put("partials/smartTable.html",
|
||||
"<table class=\"smart-table\">\n" +
|
||||
" <thead>\n" +
|
||||
" <tr class=\"smart-table-global-search-row\" ng-show=\"isGlobalSearchActivated\">\n" +
|
||||
" <td class=\"smart-table-global-search\" column-span=\"{{columns.length}}\" colspan=\"{{columnSpan}}\">\n" +
|
||||
" </td>\n" +
|
||||
" </tr>\n" +
|
||||
" <tr class=\"smart-table-header-row\">\n" +
|
||||
" <th ng-repeat=\"column in columns\" ng-include=\"column.headerTemplateUrl\"\n" +
|
||||
" class=\"smart-table-header-cell {{column.headerClass}}\" scope=\"col\">\n" +
|
||||
" </th>\n" +
|
||||
" </tr>\n" +
|
||||
" </thead>\n" +
|
||||
" <tbody>\n" +
|
||||
" <tr ng-repeat=\"dataRow in displayedCollection\" ng-class=\"{selected:dataRow.isSelected}\"\n" +
|
||||
" class=\"smart-table-data-row\">\n" +
|
||||
" <td ng-repeat=\"column in columns\" class=\"smart-table-data-cell {{column.cellClass}}\"></td>\n" +
|
||||
" </tr>\n" +
|
||||
" </tbody>\n" +
|
||||
" <tfoot ng-show=\"isPaginationEnabled\">\n" +
|
||||
" <tr class=\"smart-table-footer-row\">\n" +
|
||||
" <td class=\"text-center\" colspan=\"{{columns.length}}\">\n" +
|
||||
" <div pagination-smart-table=\"\" num-pages=\"numberOfPages\" max-size=\"maxSize\" current-page=\"currentPage\"></div>\n" +
|
||||
" </td>\n" +
|
||||
" </tr>\n" +
|
||||
" </tfoot>\n" +
|
||||
"</table>\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"");
|
||||
}]);
|
||||
|
||||
(function (angular) {
|
||||
"use strict";
|
||||
angular.module('smartTable.templateUrlList', [])
|
||||
.constant('templateUrlList', {
|
||||
smartTable: 'partials/smartTable.html',
|
||||
smartTableGlobalSearch: 'partials/globalSearchCell.html',
|
||||
editableCell: 'partials/editableCell.html',
|
||||
selectionCheckbox: 'partials/selectionCheckbox.html',
|
||||
selectAllCheckbox: 'partials/selectAllCheckbox.html',
|
||||
defaultHeader: 'partials/defaultHeader.html',
|
||||
pagination: 'partials/pagination.html'
|
||||
});
|
||||
})(angular);
|
||||
|
||||
|
||||
(function (angular) {
|
||||
"use strict";
|
||||
angular.module('smartTable.utilities', [])
|
||||
|
||||
.factory('ArrayUtility', function () {
|
||||
|
||||
/**
|
||||
* remove the item at index from arrayRef and return the removed item
|
||||
* @param arrayRef
|
||||
* @param index
|
||||
* @returns {*}
|
||||
*/
|
||||
var removeAt = function (arrayRef, index) {
|
||||
if (index >= 0 && index < arrayRef.length) {
|
||||
return arrayRef.splice(index, 1)[0];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* insert item in arrayRef at index or a the end if index is wrong
|
||||
* @param arrayRef
|
||||
* @param index
|
||||
* @param item
|
||||
*/
|
||||
insertAt = function (arrayRef, index, item) {
|
||||
if (index >= 0 && index < arrayRef.length) {
|
||||
arrayRef.splice(index, 0, item);
|
||||
} else {
|
||||
arrayRef.push(item);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* move the item at oldIndex to newIndex in arrayRef
|
||||
* @param arrayRef
|
||||
* @param oldIndex
|
||||
* @param newIndex
|
||||
*/
|
||||
moveAt = function (arrayRef, oldIndex, newIndex) {
|
||||
var elementToMove;
|
||||
if (oldIndex >= 0 && oldIndex < arrayRef.length && newIndex >= 0 && newIndex < arrayRef.length) {
|
||||
elementToMove = arrayRef.splice(oldIndex, 1)[0];
|
||||
arrayRef.splice(newIndex, 0, elementToMove);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* sort arrayRef according to sortAlgorithm following predicate and reverse
|
||||
* @param arrayRef
|
||||
* @param sortAlgorithm
|
||||
* @param predicate
|
||||
* @param reverse
|
||||
* @returns {*}
|
||||
*/
|
||||
sort = function (arrayRef, sortAlgorithm, predicate, reverse) {
|
||||
|
||||
if (!sortAlgorithm || !angular.isFunction(sortAlgorithm)) {
|
||||
return arrayRef;
|
||||
} else {
|
||||
return sortAlgorithm(arrayRef, predicate, reverse === true);//excpet if reverse is true it will take it as false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* filter arrayRef according with filterAlgorithm and predicate
|
||||
* @param arrayRef
|
||||
* @param filterAlgorithm
|
||||
* @param predicate
|
||||
* @returns {*}
|
||||
*/
|
||||
filter = function (arrayRef, filterAlgorithm, predicate) {
|
||||
if (!filterAlgorithm || !angular.isFunction(filterAlgorithm)) {
|
||||
return arrayRef;
|
||||
} else {
|
||||
return filterAlgorithm(arrayRef, predicate);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* return an array, part of array ref starting at min and the size of length
|
||||
* @param arrayRef
|
||||
* @param min
|
||||
* @param length
|
||||
* @returns {*}
|
||||
*/
|
||||
fromTo = function (arrayRef, min, length) {
|
||||
|
||||
var out = [],
|
||||
limit,
|
||||
start;
|
||||
|
||||
if (!angular.isArray(arrayRef)) {
|
||||
return arrayRef;
|
||||
}
|
||||
|
||||
start = Math.max(min, 0);
|
||||
start = Math.min(start, (arrayRef.length - 1) > 0 ? arrayRef.length - 1 : 0);
|
||||
|
||||
length = Math.max(0, length);
|
||||
limit = Math.min(start + length, arrayRef.length);
|
||||
|
||||
for (var i = start; i < limit; i++) {
|
||||
out.push(arrayRef[i]);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
removeAt: removeAt,
|
||||
insertAt: insertAt,
|
||||
moveAt: moveAt,
|
||||
sort: sort,
|
||||
filter: filter,
|
||||
fromTo: fromTo
|
||||
};
|
||||
});
|
||||
})(angular);
|
||||
|
||||
|
||||
(function (angular) {
|
||||
angular.module('ui.bootstrap.pagination.smartTable', ['smartTable.templateUrlList'])
|
||||
|
||||
.constant('paginationConfig', {
|
||||
boundaryLinks: false,
|
||||
directionLinks: true,
|
||||
firstText: 'First',
|
||||
previousText: '<',
|
||||
nextText: '>',
|
||||
lastText: 'Last'
|
||||
})
|
||||
|
||||
.directive('paginationSmartTable', ['paginationConfig', 'templateUrlList', function (paginationConfig, templateUrlList) {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
require: '^smartTable',
|
||||
scope: {
|
||||
numPages: '=',
|
||||
currentPage: '=',
|
||||
maxSize: '='
|
||||
},
|
||||
templateUrl: templateUrlList.pagination,
|
||||
replace: true,
|
||||
link: function (scope, element, attrs, ctrl) {
|
||||
|
||||
// Setup configuration parameters
|
||||
var boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
|
||||
var directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
|
||||
var firstText = angular.isDefined(attrs.firstText) ? attrs.firstText : paginationConfig.firstText;
|
||||
var previousText = angular.isDefined(attrs.previousText) ? attrs.previousText : paginationConfig.previousText;
|
||||
var nextText = angular.isDefined(attrs.nextText) ? attrs.nextText : paginationConfig.nextText;
|
||||
var lastText = angular.isDefined(attrs.lastText) ? attrs.lastText : paginationConfig.lastText;
|
||||
|
||||
// Create page object used in template
|
||||
function makePage(number, text, isActive, isDisabled) {
|
||||
return {
|
||||
number: number,
|
||||
text: text,
|
||||
active: isActive,
|
||||
disabled: isDisabled
|
||||
};
|
||||
}
|
||||
|
||||
scope.$watch('numPages + currentPage + maxSize', function () {
|
||||
scope.pages = [];
|
||||
|
||||
// Default page limits
|
||||
var startPage = 1, endPage = scope.numPages;
|
||||
|
||||
// recompute if maxSize
|
||||
if (scope.maxSize && scope.maxSize < scope.numPages) {
|
||||
startPage = Math.max(scope.currentPage - Math.floor(scope.maxSize / 2), 1);
|
||||
endPage = startPage + scope.maxSize - 1;
|
||||
|
||||
// Adjust if limit is exceeded
|
||||
if (endPage > scope.numPages) {
|
||||
endPage = scope.numPages;
|
||||
startPage = endPage - scope.maxSize + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Add page number links
|
||||
for (var number = startPage; number <= endPage; number++) {
|
||||
var page = makePage(number, number, scope.isActive(number), false);
|
||||
scope.pages.push(page);
|
||||
}
|
||||
|
||||
// Add previous & next links
|
||||
if (directionLinks) {
|
||||
var previousPage = makePage(scope.currentPage - 1, previousText, false, scope.noPrevious());
|
||||
scope.pages.unshift(previousPage);
|
||||
|
||||
var nextPage = makePage(scope.currentPage + 1, nextText, false, scope.noNext());
|
||||
scope.pages.push(nextPage);
|
||||
}
|
||||
|
||||
// Add first & last links
|
||||
if (boundaryLinks) {
|
||||
var firstPage = makePage(1, firstText, false, scope.noPrevious());
|
||||
scope.pages.unshift(firstPage);
|
||||
|
||||
var lastPage = makePage(scope.numPages, lastText, false, scope.noNext());
|
||||
scope.pages.push(lastPage);
|
||||
}
|
||||
|
||||
|
||||
if (scope.currentPage > scope.numPages) {
|
||||
scope.selectPage(scope.numPages);
|
||||
}
|
||||
});
|
||||
scope.noPrevious = function () {
|
||||
return scope.currentPage === 1;
|
||||
};
|
||||
scope.noNext = function () {
|
||||
return scope.currentPage === scope.numPages;
|
||||
};
|
||||
scope.isActive = function (page) {
|
||||
return scope.currentPage === page;
|
||||
};
|
||||
|
||||
scope.selectPage = function (page) {
|
||||
if (!scope.isActive(page) && page > 0 && page <= scope.numPages) {
|
||||
scope.currentPage = page;
|
||||
ctrl.changePage({page: page});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
})(angular);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
function KeyboardShortcuts() {
|
||||
this.bind = function bind(keymap) {
|
||||
_.forEach(keymap, function (fn, key) {
|
||||
Mousetrap.bindGlobal(key, function (e) {
|
||||
e.preventDefault();
|
||||
fn();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
this.unbind = function unbind(keymap) {
|
||||
_.forEach(keymap, function (fn, key) {
|
||||
Mousetrap.unbind(key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('redash.services', [])
|
||||
.service('KeyboardShortcuts', [KeyboardShortcuts])
|
||||
})();
|
||||
@@ -1,30 +0,0 @@
|
||||
<div class="bg-white">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created By</th>
|
||||
<th>Created At</th>
|
||||
<th>Runtime</th>
|
||||
<th>Last Executed At</th>
|
||||
<th>Update Schedule</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="query in queries">
|
||||
<td><a href="queries/{{query.id}}">{{query.name}}</a></td>
|
||||
<td>{{query.user.name}}</td>
|
||||
<td>{{query.created_at | dateTime}}</td>
|
||||
<td>{{query.runtime | durationHumanize}}</td>
|
||||
<td>{{query.retrieved_at | dateTime}}</td>
|
||||
<td>{{query.schedule | scheduleHumanize}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="text-center">
|
||||
<ul class="pagination">
|
||||
<li ng-repeat="p in pages" ng-class="{active: p.active, disabled: p.disabled}"><a ng-click="selectPage(p.page)">{{p.title}}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,17 +0,0 @@
|
||||
<div class="schema-container">
|
||||
<div class="p-t-5 p-b-5">
|
||||
<input type="text" placeholder="Search schema..." class="form-control" ng-model="schemaFilter">
|
||||
</div>
|
||||
|
||||
<div class="schema-browser" vs-repeat vs-size="getSize(table)">
|
||||
<div ng-repeat="table in schema | filter:schemaFilter track by table.name">
|
||||
<div class="table-name" ng-click="showTable(table)">
|
||||
<i class="fa fa-table"></i> <strong><span title="{{table.name}}">{{table.name}}</span><span
|
||||
ng-if="table.size !== undefined"> ({{table.size}})</span></strong>
|
||||
</div>
|
||||
<div collapse="table.collapsed">
|
||||
<div ng-repeat="column in table.columns track by column" style="padding-left:16px;">{{column}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1 +0,0 @@
|
||||
<a ng-href="queries/{{dataRow.id}}">{{dataRow.name}}</a>
|
||||
@@ -1,18 +0,0 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" aria-label="Close" ng-click="close()"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">Refresh Schedule</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" value="periodic" ng-model="refreshType">
|
||||
<query-refresh-select ng-disabled="refreshType != 'periodic'"></query-refresh-select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" value="daily" ng-model="refreshType">
|
||||
<query-time-picker ng-disabled="refreshType != 'daily'"></query-time-picker>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user