Query view/edit screens

This commit is contained in:
Arik Fraimovich
2016-11-13 14:03:32 +02:00
parent d561904aa6
commit 416f7da75f
34 changed files with 579 additions and 1878 deletions

View File

@@ -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

View File

@@ -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';

View 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);
}

View 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);
}

View File

@@ -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);

View 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);
}

View File

@@ -5,7 +5,7 @@
<div class="modal-body">
<h5>IFrame Embed</h5>
<div>
<code>&lt;iframe src="{{ embedUrl }}" width="720" height="391"&gt;&lt;/iframe&gt;</code>
<code>&lt;iframe src="{{ $ctrl.embedUrl }}" width="720" height="391"&gt;&lt;/iframe&gt;</code>
</div>
<span class="text-muted">(height should be adjusted)</span>
<div ng-if="snapshotUrl">

View 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);
}

View File

@@ -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));
}

View File

@@ -23,7 +23,6 @@ function queryEditor(QuerySnippet) {
restrict: 'E',
scope: {
query: '=',
lock: '=',
schema: '=',
syntax: '=',
},

View File

@@ -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">&#42;</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>

View 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">&times;</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>

View 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);
}

View File

@@ -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>

View File

@@ -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?");
});

View File

@@ -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` },
},
});
};
}

View 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);
}

View File

@@ -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';

View File

@@ -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`;
};

View File

@@ -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) => {

View 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">&times;</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>

View 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);
}

View File

@@ -1,44 +0,0 @@
<div>
<div class="modal-header">
<button type="button" class="close" aria-label="Close" ng-click="close()"><span aria-hidden="true">&times;</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>

View File

@@ -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);

View File

@@ -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",

View File

@@ -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);
}
}
}]);
})();

View File

@@ -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]);
})();

View File

@@ -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);
}
});

View File

@@ -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);

View File

@@ -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])
})();

View File

@@ -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>

View File

@@ -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>

View File

@@ -1 +0,0 @@
<a ng-href="queries/{{dataRow.id}}">{{dataRow.name}}</a>

View File

@@ -1,18 +0,0 @@
<div class="modal-header">
<button type="button" class="close" aria-label="Close" ng-click="close()"><span aria-hidden="true">&times;</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>