diff --git a/frontend/app/components/index.js b/frontend/app/components/index.js
index 88b71d2b..23ec34b9 100644
--- a/frontend/app/components/index.js
+++ b/frontend/app/components/index.js
@@ -8,3 +8,5 @@ export { default as queryLink } from './query-link';
export { default as parameters } from './parameters';
export { default as permissionsEditor } from './permissions-editor';
export { default as dynamicTable } from './dynamic-table';
+export { default as paginator } from './paginator';
+export { default as settingsScreen } from './settings-screen';
diff --git a/frontend/app/components/paginator.js b/frontend/app/components/paginator.js
new file mode 100644
index 00000000..0b26c558
--- /dev/null
+++ b/frontend/app/components/paginator.js
@@ -0,0 +1,31 @@
+class PaginatorCtrl {
+ constructor() {
+ this.page = this.paginator.page;
+ }
+ pageChanged() {
+ this.paginator.setPage(this.page);
+ }
+}
+
+export default function (ngModule) {
+ ngModule.component('paginator', {
+ template: `
+
+
+
+ `,
+ bindings: {
+ paginator: '<',
+ },
+ controller: PaginatorCtrl,
+ });
+}
diff --git a/frontend/app/components/settings-screen.html b/frontend/app/components/settings-screen.html
new file mode 100644
index 00000000..e8f1c9ca
--- /dev/null
+++ b/frontend/app/components/settings-screen.html
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/frontend/app/components/settings-screen.js b/frontend/app/components/settings-screen.js
new file mode 100644
index 00000000..2181e7f4
--- /dev/null
+++ b/frontend/app/components/settings-screen.js
@@ -0,0 +1,24 @@
+import startsWith from 'underscore.string/startsWith';
+import template from './settings-screen.html';
+
+export default function (ngModule) {
+ ngModule.directive('settingsScreen', $location =>
+ ({
+ restrict: 'E',
+ transclude: true,
+ template,
+ controller($scope, currentUser) {
+ $scope.usersPage = startsWith($location.path(), '/users');
+ $scope.groupsPage = startsWith($location.path(), '/groups');
+ $scope.dsPage = startsWith($location.path(), '/data_sources');
+ $scope.destinationsPage = startsWith($location.path(), '/destinations');
+ $scope.snippetsPage = startsWith($location.path(), '/query_snippets');
+
+ $scope.showGroupsLink = currentUser.hasPermission('list_users');
+ $scope.showUsersLink = currentUser.hasPermission('list_users');
+ $scope.showDsLink = currentUser.hasPermission('admin');
+ $scope.showDestinationsLink = currentUser.hasPermission('admin');
+ },
+ })
+ );
+}
diff --git a/frontend/app/index.js b/frontend/app/index.js
index 99c55ddb..0f9dc3e0 100644
--- a/frontend/app/index.js
+++ b/frontend/app/index.js
@@ -11,6 +11,10 @@ import uiBootstrap from 'angular-ui-bootstrap';
import uiSelect from 'ui-select';
import toastr from 'angular-toastr';
import 'angular-moment';
+
+import 'brace';
+import 'angular-ui-ace';
+
import { ngTable } from 'ng-table';
import { each } from 'underscore';
@@ -25,7 +29,7 @@ import registerVisualizations from './visualizations';
import markdownFilter from './filters/markdown';
const requirements = [
- ngRoute, ngResource, ngSanitize, uiBootstrap, uiSelect, ngTable.name, 'angularMoment', toastr,
+ ngRoute, ngResource, ngSanitize, uiBootstrap, uiSelect, ngTable.name, 'angularMoment', toastr, 'ui.ace',
];
const ngModule = angular.module('app', requirements);
@@ -88,6 +92,7 @@ function registerPages() {
ngModule.config(($routeProvider) => {
each(routes, (route, path) => {
+ console.log('Route: ', path);
$routeProvider.when(path, route);
});
});
diff --git a/frontend/app/pages/index.js b/frontend/app/pages/index.js
index cdebbb75..a1d62ba7 100644
--- a/frontend/app/pages/index.js
+++ b/frontend/app/pages/index.js
@@ -4,3 +4,4 @@ export { default as alertsList } from './alerts-list';
export { default as alert } from './alert';
export { default as admin } from './admin';
export { default as dashboards } from './dashboards';
+export { default as querySnippets } from './query-snippets';
diff --git a/frontend/app/pages/query-snippets/edit.html b/frontend/app/pages/query-snippets/edit.html
new file mode 100644
index 00000000..9565c48e
--- /dev/null
+++ b/frontend/app/pages/query-snippets/edit.html
@@ -0,0 +1,29 @@
+
+
+
diff --git a/frontend/app/pages/query-snippets/edit.js b/frontend/app/pages/query-snippets/edit.js
new file mode 100644
index 00000000..acb52a05
--- /dev/null
+++ b/frontend/app/pages/query-snippets/edit.js
@@ -0,0 +1,64 @@
+import 'brace/mode/snippets';
+import template from './edit.html';
+
+function SnippetCtrl($routeParams, $http, $location, toastr, currentUser, Events, QuerySnippet) {
+ // $scope.$parent.pageTitle = 'Query Snippets';
+ this.snippetId = $routeParams.snippetId;
+ Events.record(currentUser, 'view', 'query_snippet', this.snippetId);
+
+ this.editorOptions = {
+ mode: 'snippets',
+ advanced: {
+ behavioursEnabled: true,
+ enableSnippets: false,
+ autoScrollEditorIntoView: true,
+ },
+ onLoad(editor) {
+ editor.$blockScrolling = Infinity;
+ editor.getSession().setUseWrapMode(true);
+ editor.setShowPrintMargin(false);
+ },
+ };
+
+ this.saveChanges = () => {
+ this.snippet.$save((snippet) => {
+ toastr.success('Saved.');
+ if (this.snippetId === 'new') {
+ $location.path(`/query_snippets/${snippet.id}`).replace();
+ }
+ }, () => {
+ toastr.error('Failed saving snippet.');
+ });
+ };
+
+ this.delete = () => {
+ this.snippet.$delete(() => {
+ $location.path('/query_snippets');
+ toastr.sucess('Query snippet deleted.');
+ }, () => {
+ toastr.error('Failed deleting query snippet.');
+ });
+ };
+
+ if (this.snippetId === 'new') {
+ this.snippet = new QuerySnippet({ description: '' });
+ this.canEdit = true;
+ } else {
+ this.snippet = QuerySnippet.get({ id: this.snippetId }, (snippet) => {
+ this.canEdit = currentUser.canEdit(snippet);
+ });
+ }
+}
+
+export default function (ngModule) {
+ ngModule.component('snippetPage', {
+ template,
+ controller: SnippetCtrl,
+ });
+
+ return {
+ '/query_snippets/:snippetId': {
+ template: '',
+ },
+ };
+}
diff --git a/frontend/app/pages/query-snippets/index.js b/frontend/app/pages/query-snippets/index.js
new file mode 100644
index 00000000..8f51f294
--- /dev/null
+++ b/frontend/app/pages/query-snippets/index.js
@@ -0,0 +1,8 @@
+import registerList from './list';
+import registerEdit from './edit';
+
+export default function (ngModule) {
+ const routes = Object.assign({}, registerList(ngModule),
+ registerEdit(ngModule));
+ return routes;
+}
diff --git a/frontend/app/pages/query-snippets/list.html b/frontend/app/pages/query-snippets/list.html
new file mode 100644
index 00000000..a3d105b0
--- /dev/null
+++ b/frontend/app/pages/query-snippets/list.html
@@ -0,0 +1,41 @@
+
+
+
+
+ New Snippet
+
+
+
+
+
+ Trigger |
+ Description |
+ Snippet |
+ Created By |
+ Updated At |
+
+
+
+
+
+ {{row.trigger}}
+ |
+
+ {{row.description}}
+ |
+
+ {{row.snippet}}
+ |
+
+ {{row.user.name}}
+ |
+
+
+ |
+
+
+
+
+
+
+
diff --git a/frontend/app/pages/query-snippets/list.js b/frontend/app/pages/query-snippets/list.js
new file mode 100644
index 00000000..d7a3b853
--- /dev/null
+++ b/frontend/app/pages/query-snippets/list.js
@@ -0,0 +1,25 @@
+import { Paginator } from '../../utils';
+import template from './list.html';
+
+function SnippetsCtrl($location, currentUser, Events, QuerySnippet) {
+ Events.record(currentUser, 'view', 'page', 'query_snippets');
+ // $scope.$parent.pageTitle = 'Query Snippets';
+
+ this.snippets = new Paginator([], { itemsPerPage: 20 });
+ QuerySnippet.query((snippets) => {
+ this.snippets.updateRows(snippets);
+ });
+}
+
+export default function (ngModule) {
+ ngModule.component('snippetsListPage', {
+ template,
+ controller: SnippetsCtrl,
+ });
+
+ return {
+ '/query_snippets': {
+ template: '',
+ },
+ };
+}
diff --git a/frontend/app/utils/index.js b/frontend/app/utils/index.js
new file mode 100644
index 00000000..eb5749b0
--- /dev/null
+++ b/frontend/app/utils/index.js
@@ -0,0 +1 @@
+export { default as Paginator } from './paginator';
diff --git a/frontend/app/utils/paginator.js b/frontend/app/utils/paginator.js
new file mode 100644
index 00000000..4d28c1fa
--- /dev/null
+++ b/frontend/app/utils/paginator.js
@@ -0,0 +1,27 @@
+export default class Paginator {
+ constructor(rows, { page = 1, itemsPerPage = 20, totalCount = undefined } = {}) {
+ this.page = page;
+ this.itemsPerPage = itemsPerPage;
+ this.updateRows(rows, totalCount);
+ }
+
+ setPage(page) {
+ this.page = page;
+ }
+
+ getPageRows() {
+ const first = this.itemsPerPage * (this.page - 1);
+ const last = this.itemsPerPage * (this.page);
+
+ return this.rows.slice(first, last);
+ }
+
+ updateRows(rows, totalCount = undefined) {
+ this.rows = rows;
+ if (this.rows) {
+ this.totalCount = totalCount || rows.length;
+ } else {
+ this.totalCount = 0;
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
index 423e749f..ee73826c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -25,11 +25,13 @@
"angular-route": "^1.5.8",
"angular-sanitize": "^1.5.8",
"angular-toastr": "^2.1.1",
+ "angular-ui-ace": "^0.2.3",
"angular-ui-bootstrap": "^2.2.0",
"babel-core": "^6.18.0",
"babel-loader": "^6.2.7",
"babel-preset-es2015": "^6.18.0",
"babel-preset-stage-2": "^6.18.0",
+ "brace": "^0.9.0",
"css-loader": "^0.25.0",
"d3": "^3.5.17",
"debug": "^2.2.0",