mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
900d558857 | ||
|
|
c6dc9affed | ||
|
|
96486b5c58 | ||
|
|
7c1565017f | ||
|
|
7197370ad4 | ||
|
|
1cbf09cbbe | ||
|
|
28b4450fa9 | ||
|
|
a799303f53 | ||
|
|
59d6eb662c | ||
|
|
4e4a3e13ab | ||
|
|
095d07bcb8 | ||
|
|
71a235c79b | ||
|
|
2bc3885977 | ||
|
|
97217f56c1 | ||
|
|
ba36f7395d | ||
|
|
ea7ca9e632 | ||
|
|
5e5fc736bf | ||
|
|
f38e76ad10 | ||
|
|
80a6f357e3 | ||
|
|
bd91288d1a | ||
|
|
38389a28ed | ||
|
|
9ef9f29213 | ||
|
|
a3c2082b7f | ||
|
|
bc5516e941 | ||
|
|
65ac8c715e | ||
|
|
9874361466 | ||
|
|
b28c8fa227 | ||
|
|
048bd53eac | ||
|
|
95c707d028 | ||
|
|
41ec4c857b | ||
|
|
e62acb1d99 | ||
|
|
a9dc00aaa6 | ||
|
|
38c6152aa0 | ||
|
|
fb723328d4 | ||
|
|
047475562d | ||
|
|
acd33ec852 | ||
|
|
340a23e71c | ||
|
|
3db1b7f265 | ||
|
|
845357fa02 | ||
|
|
f75e31fa8e | ||
|
|
38be723179 | ||
|
|
18bf44453d | ||
|
|
374f11252f | ||
|
|
2d3566abce | ||
|
|
17d6bfff63 | ||
|
|
73540175d8 | ||
|
|
8c693efb3e | ||
|
|
51392d0398 | ||
|
|
78888c2082 | ||
|
|
bc6bd1b316 | ||
|
|
4060344a72 | ||
|
|
6522325060 | ||
|
|
ae6564e912 | ||
|
|
2af70a6c2d | ||
|
|
a3a1dcf4ba | ||
|
|
eb979ef130 | ||
|
|
7f7fdbba54 | ||
|
|
fa213d72a7 | ||
|
|
d2bf935edb | ||
|
|
c4349f5c64 | ||
|
|
b5a6f4a166 | ||
|
|
79807dfa14 | ||
|
|
0b0ec90987 | ||
|
|
a9fc220ec8 | ||
|
|
ee9bbbaa7c | ||
|
|
12cc4e5ff9 | ||
|
|
b5b5643090 | ||
|
|
6718081a49 | ||
|
|
138087861c | ||
|
|
9a88cf1743 | ||
|
|
2ca93599ef | ||
|
|
ef85a06d60 | ||
|
|
f7ffc75ba4 | ||
|
|
f28eda4174 | ||
|
|
c5458af1a0 | ||
|
|
c28ced14c6 | ||
|
|
1110e17c4a | ||
|
|
3b9c31a056 | ||
|
|
38b655ce3a | ||
|
|
0ec9b73eb2 | ||
|
|
b67369daa4 | ||
|
|
cbc7eee592 | ||
|
|
d512cef5af | ||
|
|
c6d1fc103c | ||
|
|
bf5b31b252 | ||
|
|
0c404fa602 | ||
|
|
0ebb6ada3c | ||
|
|
d2e519cc3b | ||
|
|
9b38f1e81c | ||
|
|
f03c173c57 | ||
|
|
f89842801f | ||
|
|
56d4ad74a8 | ||
|
|
334e95afa0 | ||
|
|
0443d84848 | ||
|
|
d38f251688 | ||
|
|
890243eb20 | ||
|
|
9fed3266e6 | ||
|
|
8fb665be08 | ||
|
|
c19253648e | ||
|
|
b8d2df7567 | ||
|
|
4603152930 | ||
|
|
e33e90a69d | ||
|
|
f5dcb5d58d | ||
|
|
f2f6abe775 | ||
|
|
c33189a355 | ||
|
|
781d997e76 | ||
|
|
35e02d8043 | ||
|
|
720af7dabf | ||
|
|
487a8c798c | ||
|
|
0f580f4540 | ||
|
|
cb21024e5c | ||
|
|
df7b970ff7 | ||
|
|
ff4edb4fbd | ||
|
|
131c9ef036 | ||
|
|
a3071a3ba1 | ||
|
|
8d5ce85954 | ||
|
|
9d3ae2c34a | ||
|
|
6d2337b332 | ||
|
|
1ef2238d65 | ||
|
|
521d05279b | ||
|
|
01e85f218a | ||
|
|
8af028bc90 | ||
|
|
85da5fced1 | ||
|
|
038d3b1004 | ||
|
|
6cf2b94a10 | ||
|
|
c930c44e3a | ||
|
|
0753332ef8 | ||
|
|
ed9e409e17 | ||
|
|
c40fffa107 | ||
|
|
d597665a86 | ||
|
|
b0bec26138 | ||
|
|
0d44466967 | ||
|
|
f4cb62782a | ||
|
|
3cadd6731c | ||
|
|
fc18b84f69 | ||
|
|
f7fc679427 | ||
|
|
e674b715ef | ||
|
|
029f6335ed | ||
|
|
fb4153add7 | ||
|
|
ada8a1255b | ||
|
|
505f338da9 | ||
|
|
18d9b2eec9 | ||
|
|
41a03352b9 | ||
|
|
50f817e265 | ||
|
|
04ddb289ee | ||
|
|
0152250e14 | ||
|
|
f574cdd179 | ||
|
|
458f213ea7 | ||
|
|
f2caae6eb1 | ||
|
|
c01cd89de9 | ||
|
|
5ea3ed7308 | ||
|
|
50eb9a86c9 | ||
|
|
12cbfc5d12 | ||
|
|
ba7ed5c6f0 | ||
|
|
4fbfa682fe | ||
|
|
fb1139a2ea | ||
|
|
8d8ec1a5f8 | ||
|
|
7582b3174d | ||
|
|
154b554ecd | ||
|
|
316e014cfa | ||
|
|
048d8fcb5b | ||
|
|
8bbb1cdfd4 | ||
|
|
94175b8a52 | ||
|
|
c350b43a5a | ||
|
|
b379c13e8b | ||
|
|
7d91e9d173 | ||
|
|
1b15ea8af9 | ||
|
|
e76efc9cdf | ||
|
|
0a311bf63f | ||
|
|
5069edb9b1 | ||
|
|
90162b6331 | ||
|
|
398812a14f | ||
|
|
2e44872b49 | ||
|
|
e02fdb3e37 | ||
|
|
234edd339c | ||
|
|
e5cbdf3036 | ||
|
|
9b85890204 | ||
|
|
6295e88d43 | ||
|
|
7796a57d43 | ||
|
|
df7fd13bfd | ||
|
|
6a5a843478 | ||
|
|
7d4fb280ba | ||
|
|
2a22b98c77 | ||
|
|
6b56e4a3e3 | ||
|
|
47fc6612bf | ||
|
|
f3e5c22c07 | ||
|
|
b42d2c5784 | ||
|
|
478a86a892 | ||
|
|
9e0205d148 | ||
|
|
59b7961bcd | ||
|
|
5b54a777d9 | ||
|
|
3af9b333a8 | ||
|
|
dcaecdbe16 | ||
|
|
3aa7d86699 | ||
|
|
feab2a7e7b | ||
|
|
d18220c1af | ||
|
|
8074a91b29 | ||
|
|
72560d985f | ||
|
|
ff2c8524de | ||
|
|
1bdea11fe3 | ||
|
|
a7bed64707 | ||
|
|
dc969fe0b5 | ||
|
|
588c868060 | ||
|
|
e739f90405 | ||
|
|
a07135c638 | ||
|
|
974f69aecf | ||
|
|
1a8078ab03 | ||
|
|
1bc8d586c3 | ||
|
|
a795f1463b | ||
|
|
aae77a8b25 | ||
|
|
c278209883 | ||
|
|
6d8880c10d | ||
|
|
aacc4b7b46 | ||
|
|
605a70d554 | ||
|
|
73466dc0e0 | ||
|
|
3fd90c6289 | ||
|
|
53f0716aca | ||
|
|
b9e08897ac | ||
|
|
300421792c | ||
|
|
85f729260b | ||
|
|
8bf2c15db8 | ||
|
|
9ea4784f87 | ||
|
|
8be9613640 | ||
|
|
b611c98112 | ||
|
|
f852f935c5 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -9,14 +9,8 @@ celerybeat-schedule*
|
||||
\#*#
|
||||
*~
|
||||
_build
|
||||
|
||||
# Vagrant related
|
||||
.vagrant
|
||||
Berksfile.lock
|
||||
redash/dump.rdb
|
||||
.vscode
|
||||
.env
|
||||
.ruby-version
|
||||
venv
|
||||
|
||||
dump.rdb
|
||||
|
||||
|
||||
82
CHANGELOG.md
82
CHANGELOG.md
@@ -1,5 +1,87 @@
|
||||
# Change Log
|
||||
|
||||
## v3.0.0 - UNRELEASED
|
||||
|
||||
### Added
|
||||
|
||||
- Query Result data source (run queries on query results).
|
||||
- Athena: option to load schema from Glue catalog. @myouju
|
||||
- Allow running any command inside the container via the Docker entrypoint script. @jezdez
|
||||
- Make invitation token max age configurable. @hhamalai
|
||||
- Redshift: add support for the new ACM root CA.
|
||||
- Redshift: support for Spectrum (external) tables. @atharvai
|
||||
- MongoDB: option to set allowDiskUse in queries.
|
||||
- Option to disable SQLAlchemy connection pool.
|
||||
- Option to set a time limit on adhoc queries.
|
||||
- Option to disable sending an invite to a new user.
|
||||
- Azure SQL Data Warehouse query runner. @kitsuyui
|
||||
- Prometheus query runner. @yershalom
|
||||
- Option to set the Flask-Limiter storage engine.
|
||||
- Option to set UnicodeWriter's error handling method. @fan-t-endo
|
||||
- PostgreSQL: SSL configuration option. @TylerBrock
|
||||
- Counter visualization: additional formatting options. @deecay
|
||||
- Query based drop down parameter. @rohithmenon
|
||||
- MySQL: multiple queries support & connection timeout.
|
||||
- Ability to select all in multi-filter. @Posnet
|
||||
- LDAP (Active Directory) support. @amarjayr
|
||||
|
||||
### Changed
|
||||
|
||||
- Copy parameters when forking a query. @kyoshidajp
|
||||
- Prevent using Query API Key with refresh API (previously it was just failing).
|
||||
- Reduce boilerplate in frontend code.
|
||||
- Set auto focus in first input items. @kyoshidajp
|
||||
- Update gunicorn to latest version.
|
||||
- Make log format configurable.
|
||||
- Sort series by name.
|
||||
- Allow setting test file with Docker test run. @meinac
|
||||
- Use outdated queries count stored already in Redis.
|
||||
- Show links based on permissions the user have.
|
||||
- Cassandra: update driver version. @yershalom
|
||||
- Docker-Compose: update configuration to always restart services. @muddydixon
|
||||
- Modernize Python 2 code to get ready for Python 3. @cclauss
|
||||
- Cohort visualization: make it friendlier to use by better handle gaps in data, so it's easier to generate the data needed.
|
||||
- Use a different markdown library. @alexmuller
|
||||
- Salesforce: improve error messages we receive from the API. @akiray03
|
||||
- Custom JS code visualization improvements. @deecay
|
||||
- DQL: Update version to 0.5.24. @aterreno
|
||||
- Cassandra: get_schema support for both C* 2.x and 3.x, support for SortedSet type serialization. (@mfouilleul)
|
||||
- Replace deprecated ng-annotate with babel plugin. @44px
|
||||
- Update Python dependencies to recent versions. @alison985
|
||||
- Bootstrap script: create /opt/redash directory only if it doesn't exist. @isomura
|
||||
- Bootstrap script: make use of REDASH_BASE_PATH variable in setup script. @sylvain
|
||||
|
||||
### Fixed
|
||||
|
||||
- Require full data source access to fork a query.
|
||||
- API key of one query could be used to get results of another one.
|
||||
- Delete group id from user object when deleting the group. @kyoshidajp
|
||||
- Sorting of X axis wasn't working for Box plot type visualizations. @deecay
|
||||
- Exporting query results as excel was failing when one of the columns had array data. @kyoshidajp
|
||||
- Show query editor's Archive/Publish Query drop-down only on saved queries. @cyriac
|
||||
- Move misplaced configuration in docker-compose.production.yml. @yutannihilation
|
||||
- MySQL: support UTF8 schema.
|
||||
- TreasureData queries were failing when returning 0 rows.
|
||||
- Use series color for Boxplot. @deecay
|
||||
- Revoke permission should respect to given grantee and access type. @meinac
|
||||
- Fixed eslint "Cannot read property 'length' of undefined" error. @kravets-levko
|
||||
- Don't crash query editor when there are unclosed curly brackets.
|
||||
- Error value in charts wasn't displayed if it was 0.
|
||||
- Prevent line breaks in EditInPlace description when using Firefox. @alexmuller
|
||||
- Queries#all_queries was sometimes returning wrong number of queries.
|
||||
- record_event fails for API events.
|
||||
- Cancel button on tasks admin page was broken.
|
||||
- Remove deprecated cx_Oracle types. @queeno
|
||||
- Textbox widgets were updating their value even when editor was cancelled. @alison985
|
||||
- Collaborators couldn't edit visualizations or schedule.
|
||||
- Use series color for error bar. @deecay
|
||||
- Upgrade script was using the wrong restart command on new AMIs.
|
||||
|
||||
## v2.0.1 - 2017-10-22
|
||||
|
||||
This is a patch release, that adds support for Redshift ACM certificates (see #2044 for details).
|
||||
|
||||
|
||||
## v2.0.0 - 2017-08-08
|
||||
|
||||
### Added
|
||||
|
||||
3
Makefile
3
Makefile
@@ -4,6 +4,7 @@ FULL_VERSION=$(VERSION)+b$(CIRCLE_BUILD_NUM)
|
||||
BASE_VERSION=$(shell python ./manage.py version | cut -d + -f 1)
|
||||
# VERSION gets evaluated every time it's referenced, therefore we need to use VERSION here instead of FULL_VERSION.
|
||||
FILENAME=$(CIRCLE_ARTIFACTS)/$(NAME).$(VERSION).tar.gz
|
||||
TEST_ARGS?=--with-coverage --cover-package=redash tests/
|
||||
|
||||
deps:
|
||||
if [ -d "./client/app" ]; then npm install; fi
|
||||
@@ -17,4 +18,4 @@ upload:
|
||||
python bin/release_manager.py $(CIRCLE_SHA1) $(BASE_VERSION) $(FILENAME)
|
||||
|
||||
test:
|
||||
nosetests --with-coverage --cover-package=redash tests/
|
||||
nosetests $(TEST_ARGS)
|
||||
|
||||
@@ -43,7 +43,7 @@ You can try out the demo instance: http://demo.redash.io/ (login with any Google
|
||||
## Reporting Bugs and Contributing Code
|
||||
|
||||
* Want to report a bug or request a feature? Please open [an issue](https://github.com/getredash/redash/issues/new).
|
||||
* Want to help us build **_Redash_**? Fork the project, edit in a [dev environment](https://redash.io/help-onpremise/setup/setting-up-development-environment-using-vagrant.html), and make a pull request. We need all the help we can get!
|
||||
* Want to help us build **_Redash_**? Fork the project, edit in a [dev environment](https://redash.io/help-onpremise/dev/guide.html), and make a pull request. We need all the help we can get!
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -72,7 +72,10 @@ case "$1" in
|
||||
tests)
|
||||
tests
|
||||
;;
|
||||
*)
|
||||
help)
|
||||
help
|
||||
;;
|
||||
*)
|
||||
exec "$@"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
@@ -95,7 +96,7 @@ def get_changelog(commit_sha):
|
||||
try:
|
||||
pull_request = re.match("Merge pull request #(\d+)", subject).groups()[0]
|
||||
pull_request = " #{}".format(pull_request)
|
||||
except Exception, ex:
|
||||
except Exception as ex:
|
||||
pull_request = ""
|
||||
|
||||
author = subprocess.check_output(['git', 'log', '-1', '--pretty=format:"%an"', parents.split(' ')[-1]])[1:-1]
|
||||
@@ -124,7 +125,7 @@ def update_release(version, build_filepath, commit_sha):
|
||||
else:
|
||||
release = create_release(version, commit_sha)
|
||||
|
||||
print "Using release id: {}".format(release['id'])
|
||||
print("Using release id: {}".format(release['id']))
|
||||
|
||||
remove_previous_builds(release)
|
||||
response = upload_asset(release, build_filepath)
|
||||
@@ -135,8 +136,8 @@ def update_release(version, build_filepath, commit_sha):
|
||||
if response.status_code != 200:
|
||||
raise exception_from_error("Failed updating release description", response)
|
||||
|
||||
except Exception, ex:
|
||||
print ex
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
|
||||
if __name__ == '__main__':
|
||||
commit_sha = sys.argv[1]
|
||||
|
||||
@@ -18,7 +18,7 @@ test:
|
||||
- nosetests --with-xunit --xunit-file=$CIRCLE_TEST_REPORTS/junit.xml --with-coverage --cover-package=redash tests/
|
||||
deployment:
|
||||
github_and_docker:
|
||||
branch: master
|
||||
branch: [master, /release.*/]
|
||||
commands:
|
||||
- make pack
|
||||
# Skipping uploads for now, until master is stable.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["es2015", "stage-2"],
|
||||
"plugins": ["transform-object-assign"]
|
||||
"plugins": ["angularjs-annotate", "transform-object-assign"]
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
build/*.js
|
||||
config/*.js
|
||||
node_modules
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: 'airbnb-base',
|
||||
extends: "airbnb-base",
|
||||
settings: {
|
||||
"import/resolver": "webpack"
|
||||
},
|
||||
env: {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
rules: {
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
|
||||
'no-param-reassign': 0,
|
||||
'no-mixed-operators': 0,
|
||||
'no-underscore-dangle': 0,
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
|
||||
"prefer-destructuring": "off",
|
||||
"prefer-template": "off",
|
||||
"no-restricted-properties": "off",
|
||||
"no-restricted-globals": "off",
|
||||
"no-multi-assign": "off",
|
||||
"max-len": ['error', 120, 2, {
|
||||
ignoreUrls: true,
|
||||
ignoreComments: false,
|
||||
ignoreRegExpLiterals: true,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1781,6 +1781,9 @@ fieldset[disabled] .form-control {
|
||||
textarea.form-control {
|
||||
height: auto;
|
||||
}
|
||||
textarea.v-resizable {
|
||||
resize: vertical;
|
||||
}
|
||||
input[type="search"] {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
@@ -8592,6 +8595,7 @@ a.thumbnail.active {
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
margin-bottom: 10px;
|
||||
overflow: auto;
|
||||
box-shadow: inset 0 -2px 0 0 #eee;
|
||||
}
|
||||
|
||||
113
client/app/components/alerts/alert-subscriptions/index.js
Normal file
113
client/app/components/alerts/alert-subscriptions/index.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import { contains, without, compact } from 'underscore';
|
||||
import template from './alert-subscriptions.html';
|
||||
|
||||
function controller($scope, $q, $sce, currentUser, AlertSubscription, Destination, toastr) {
|
||||
'ngInject';
|
||||
|
||||
$scope.newSubscription = {};
|
||||
$scope.subscribers = [];
|
||||
$scope.destinations = [];
|
||||
$scope.currentUser = currentUser;
|
||||
|
||||
$q
|
||||
.all([
|
||||
Destination.query().$promise,
|
||||
AlertSubscription.query({ alertId: $scope.alertId }).$promise,
|
||||
])
|
||||
.then((responses) => {
|
||||
const destinations = responses[0];
|
||||
const subscribers = responses[1];
|
||||
|
||||
const mapF = s => s.destination && s.destination.id;
|
||||
const subscribedDestinations = compact(subscribers.map(mapF));
|
||||
|
||||
const subscribedUsers = compact(subscribers.map(s => !s.destination && s.user.id));
|
||||
|
||||
$scope.destinations = destinations.filter(d => !contains(subscribedDestinations, d.id));
|
||||
|
||||
if (!contains(subscribedUsers, currentUser.id)) {
|
||||
$scope.destinations.unshift({ user: { name: currentUser.name } });
|
||||
}
|
||||
|
||||
$scope.newSubscription.destination = $scope.destinations[0];
|
||||
$scope.subscribers = subscribers;
|
||||
});
|
||||
|
||||
$scope.destinationsDisplay = (d) => {
|
||||
if (!d) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let destination = d;
|
||||
if (d.destination) {
|
||||
destination = destination.destination;
|
||||
} else if (destination.user) {
|
||||
destination = {
|
||||
name: `${d.user.name} (Email)`,
|
||||
icon: 'fa-envelope',
|
||||
type: 'user',
|
||||
};
|
||||
}
|
||||
|
||||
return $sce.trustAsHtml(`<i class="fa ${destination.icon}"></i> ${destination.name}`);
|
||||
};
|
||||
|
||||
$scope.saveSubscriber = () => {
|
||||
const sub = new AlertSubscription({ alert_id: $scope.alertId });
|
||||
if ($scope.newSubscription.destination.id) {
|
||||
sub.destination_id = $scope.newSubscription.destination.id;
|
||||
}
|
||||
|
||||
sub.$save(
|
||||
() => {
|
||||
toastr.success('Subscribed.');
|
||||
$scope.subscribers.push(sub);
|
||||
$scope.destinations = without($scope.destinations, $scope.newSubscription.destination);
|
||||
if ($scope.destinations.length > 0) {
|
||||
$scope.newSubscription.destination = $scope.destinations[0];
|
||||
} else {
|
||||
$scope.newSubscription.destination = undefined;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
toastr.error('Failed saving subscription.');
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
$scope.unsubscribe = (subscriber) => {
|
||||
const destination = subscriber.destination;
|
||||
const user = subscriber.user;
|
||||
|
||||
subscriber.$delete(
|
||||
() => {
|
||||
toastr.success('Unsubscribed');
|
||||
$scope.subscribers = without($scope.subscribers, subscriber);
|
||||
if (destination) {
|
||||
$scope.destinations.push(destination);
|
||||
} else if (user.id === currentUser.id) {
|
||||
$scope.destinations.push({ user: { name: currentUser.name } });
|
||||
}
|
||||
|
||||
if ($scope.destinations.length === 1) {
|
||||
$scope.newSubscription.destination = $scope.destinations[0];
|
||||
}
|
||||
},
|
||||
() => {
|
||||
toastr.error('Failed unsubscribing.');
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('alertSubscriptions', () => ({
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
alertId: '=',
|
||||
},
|
||||
template,
|
||||
controller,
|
||||
}));
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
<li><a href="queries">Queries</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<li ng-if="$ctrl.showAlertsLink">
|
||||
<a href="alerts">Alerts</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -59,7 +59,7 @@
|
||||
<a ng-href="users/{{$ctrl.currentUser.id}}">
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
<img ng-src="{{$ctrl.currentUser.gravatar_url}}" size="40px" class="img-circle"/>
|
||||
<img ng-src="{{$ctrl.currentUser.gravatar_url}}" size="40px" class="img-circle" />
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<p><strong>{{$ctrl.currentUser.name}}</strong></p>
|
||||
@@ -68,15 +68,15 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider" ng-if="$ctrl.currentUser.hasPermission('super_admin')">
|
||||
<li ng-if="$ctrl.currentUser.hasPermission('super_admin')"><a href="admin/status">System Status</a></li>
|
||||
<li class="divider">
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="$ctrl.logout()">Log out</a>
|
||||
</li>
|
||||
<li ng-if="$ctrl.currentUser.hasPermission('super_admin')"><a href="admin/status">System Status</a></li>
|
||||
<li class="divider">
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="$ctrl.logout()">Log out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
@@ -1,7 +1,7 @@
|
||||
import debug from 'debug';
|
||||
|
||||
import logoUrl from '@/assets/images/redash_icon_small.png';
|
||||
import template from './app-header.html';
|
||||
import logoUrl from '../../assets/images/redash_icon_small.png';
|
||||
import './app-header.css';
|
||||
|
||||
const logger = debug('redash:appHeader');
|
||||
@@ -11,6 +11,7 @@ function controller($rootScope, $location, $uibModal, Auth, currentUser, clientC
|
||||
this.basePath = clientConfig.basePath;
|
||||
this.currentUser = currentUser;
|
||||
this.showQueriesMenu = currentUser.hasPermission('view_query');
|
||||
this.showAlertsLink = currentUser.hasPermission('list_alerts');
|
||||
this.showNewQueryMenu = currentUser.hasPermission('create_query');
|
||||
this.showSettingsMenu = currentUser.hasPermission('list_users');
|
||||
this.showDashboardsMenu = currentUser.hasPermission('list_dashboards');
|
||||
@@ -42,7 +43,7 @@ function controller($rootScope, $location, $uibModal, Auth, currentUser, clientC
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('appHeader', {
|
||||
template,
|
||||
controller,
|
||||
|
||||
@@ -27,6 +27,6 @@ function cancelQueryButton() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('cancelQueryButton', cancelQueryButton);
|
||||
}
|
||||
@@ -17,6 +17,8 @@ const AddWidgetDialog = {
|
||||
this.query = {};
|
||||
this.selected_query = undefined;
|
||||
this.text = '';
|
||||
this.existing_text = '';
|
||||
this.new_text = '';
|
||||
this.widgetSizes = [{
|
||||
name: 'Regular',
|
||||
value: 1,
|
||||
@@ -95,6 +97,6 @@ const AddWidgetDialog = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('addWidgetDialog', AddWidgetDialog);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<input type="text" class="form-control" placeholder="Dashboard Name" ng-model="$ctrl.dashboard.name">
|
||||
<input type="text" class="form-control" placeholder="Dashboard Name" ng-model="$ctrl.dashboard.name" autofocus>
|
||||
</p>
|
||||
|
||||
<p ng-if="$ctrl.dashboard.id">
|
||||
@@ -99,6 +99,6 @@ const EditDashboardDialog = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('editDashboardDialog', EditDashboardDialog);
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" ng-model="$ctrl.widget.text" rows="3"></textarea>
|
||||
<textarea class="form-control" ng-model="$ctrl.widget.new_text" rows="3"></textarea>
|
||||
</div>
|
||||
<div ng-show="$ctrl.widget.text">
|
||||
<div ng-show="$ctrl.widget.new_text">
|
||||
<strong>Preview:</strong>
|
||||
<p ng-bind-html="$ctrl.widget.text | markdown"></p>
|
||||
<p ng-bind-html="$ctrl.widget.new_text | markdown"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,13 +15,18 @@ const EditTextBoxComponent = {
|
||||
this.widget = this.resolve.widget;
|
||||
this.saveWidget = () => {
|
||||
this.saveInProgress = true;
|
||||
this.widget.$save().then(() => {
|
||||
if (this.widget.new_text !== this.widget.existing_text) {
|
||||
this.widget.text = this.widget.new_text;
|
||||
this.widget.$save().then(() => {
|
||||
this.close();
|
||||
}).catch(() => {
|
||||
toastr.error('Widget can not be updated');
|
||||
}).finally(() => {
|
||||
this.saveInProgress = false;
|
||||
});
|
||||
} else {
|
||||
this.close();
|
||||
}).catch(() => {
|
||||
toastr.error('Widget can not be updated');
|
||||
}).finally(() => {
|
||||
this.saveInProgress = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -30,6 +35,8 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
|
||||
this.canViewQuery = currentUser.hasPermission('view_query');
|
||||
|
||||
this.editTextBox = () => {
|
||||
this.widget.existing_text = this.widget.text;
|
||||
this.widget.new_text = this.widget.text;
|
||||
$uibModal.open({
|
||||
component: 'editTextBox',
|
||||
resolve: {
|
||||
@@ -92,7 +99,7 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
|
||||
}
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('editTextBox', EditTextBoxComponent);
|
||||
ngModule.component('dashboardWidget', {
|
||||
template,
|
||||
@@ -1,7 +1,7 @@
|
||||
<form name="dataSourceForm">
|
||||
<div class="form-group">
|
||||
<label for="type">Type</label>
|
||||
<select name="type" class="form-control" ng-options="type.type as type.name for type in types" ng-model="target.type"></select>
|
||||
<select name="type" class="form-control" ng-options="type.type as type.name for type in types" ng-model="target.type" autofocus></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dataSourceName">Name</label>
|
||||
|
||||
@@ -38,7 +38,7 @@ function DynamicForm($http, toastr, $q) {
|
||||
|
||||
$scope.fields = orderedInputs(
|
||||
configurationSchema.properties,
|
||||
configurationSchema.order || []
|
||||
configurationSchema.order || [],
|
||||
);
|
||||
|
||||
return type;
|
||||
@@ -137,13 +137,13 @@ function DynamicForm($http, toastr, $q) {
|
||||
} else {
|
||||
toastr.error('Failed saving.');
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('dynamicForm', DynamicForm);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ function DynamicTable($sanitize) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('dynamicTable', {
|
||||
template,
|
||||
controller: DynamicTable,
|
||||
|
||||
@@ -33,6 +33,8 @@ function EditInPlace() {
|
||||
link($scope, element) {
|
||||
// Let's get a reference to the input element, as we'll want to reference it.
|
||||
const inputElement = $(element.children()[2]);
|
||||
const keycodeEnter = 13;
|
||||
const keycodeEscape = 27;
|
||||
|
||||
// This directive should have a set class so we can style it.
|
||||
element.addClass('edit-in-place');
|
||||
@@ -74,9 +76,10 @@ function EditInPlace() {
|
||||
$(inputElement).keydown((e) => {
|
||||
// 'return' or 'enter' key pressed
|
||||
// allow 'shift' to break lines
|
||||
if (e.which === 13 && !e.shiftKey) {
|
||||
if (e.which === keycodeEnter && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
save();
|
||||
} else if (e.which === 27) {
|
||||
} else if (e.which === keycodeEscape) {
|
||||
$scope.value = $scope.oldValue;
|
||||
$scope.$apply(() => {
|
||||
$(inputElement[0]).blur();
|
||||
@@ -89,6 +92,6 @@ function EditInPlace() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('editInPlace', EditInPlace);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ function controller(clientConfig, currentUser) {
|
||||
this.showMailWarning = clientConfig.mailSettingsMissing && currentUser.isAdmin;
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('emailSettingsWarning', {
|
||||
bindings: {
|
||||
function: '<',
|
||||
|
||||
@@ -15,6 +15,6 @@ const ErrorMessagesComponent = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('errorMessages', ErrorMessagesComponent);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
<div class="container bg-white p-5" ng-show="$ctrl.filters | notEmpty">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 m-t-5" ng-repeat="filter in $ctrl.filters">
|
||||
<ui-select ng-model="filter.current" ng-if="!filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)" on-remove="$ctrl.filterChangeListener(filter, $model)">
|
||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected | filterValue:filter}}</ui-select-match>
|
||||
<label>{{filter.friendlyName}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 m-t-5" ng-repeat="filter in $ctrl.filters">
|
||||
<ui-select ng-model="filter.current" ng-if="!filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)" on-remove="$ctrl.filterChangeListener(filter, $model)"
|
||||
remove-selected="false">
|
||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{$select.selected | filterValue:filter}}</ui-select-match>
|
||||
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
||||
{{value | filterValue:filter }}
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
|
||||
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)" on-remove="$ctrl.filterChangeListener(filter, $model)">
|
||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$item | filterValue:filter}}</ui-select-match>
|
||||
<ui-select-choices repeat="value in filter.values | filter: $select.search">
|
||||
{{value | filterValue:filter }}
|
||||
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)"
|
||||
on-remove="$ctrl.filterChangeListener(filter, $model)" remove-selected="false">
|
||||
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{$item | filterValue:filter}}</ui-select-match>
|
||||
<ui-select-choices repeat="value in filter.values | filter: $select.search" group-by="$ctrl.itemGroup">
|
||||
<span ng-if="value == '*'">
|
||||
Select All
|
||||
</span>
|
||||
<span ng-if="value == '-'">
|
||||
Clear
|
||||
</span>
|
||||
<span ng-if="value != '*' && value != '-'">
|
||||
{{value | filterValue:filter }}
|
||||
</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -12,10 +12,18 @@ const FiltersComponent = {
|
||||
this.filterChangeListener = (filter, modal) => {
|
||||
this.onChange({ filter, $modal: modal });
|
||||
};
|
||||
|
||||
this.itemGroup = (item) => {
|
||||
if (item === '*' || item === '-') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'Values';
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('filters', FiltersComponent);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ function controller(clientConfig, currentUser) {
|
||||
this.newVersionAvailable = clientConfig.newVersionAvailable && currentUser.isAdmin;
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('footer', {
|
||||
template,
|
||||
controller,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form">
|
||||
<input type="text" ng-model="$ctrl.group.name" placeholder="Group Name" class="form-control"/>
|
||||
<input type="text" ng-model="$ctrl.group.name" placeholder="Group Name" class="form-control" autofocus/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -34,6 +34,6 @@ const EditGroupDialogComponent = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('editGroupDialog', EditGroupDialogComponent);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ function controller($window, $location, toastr, currentUser) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('groupName', {
|
||||
bindings: {
|
||||
group: '<',
|
||||
@@ -1,21 +0,0 @@
|
||||
export { default as appHeader } from './app-header';
|
||||
export { default as footer } from './footer';
|
||||
export { default as pageHeader } from './page-header';
|
||||
export { default as tabNav } from './tab-nav';
|
||||
export { default as emailSettingsWarning } from './email-settings-warning';
|
||||
export { default as rdTab } from './rd-tab';
|
||||
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';
|
||||
export { default as errorMessages } from './error-messages';
|
||||
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';
|
||||
export { default as routeStatus } from './route-status';
|
||||
export { default as filters } from './filters';
|
||||
export { default as sortIcon } from './sort-icon';
|
||||
@@ -11,6 +11,6 @@ const Overlay = {
|
||||
transclude: true,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('overlay', Overlay);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ function controller() {
|
||||
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('pageHeader', {
|
||||
template,
|
||||
controller,
|
||||
|
||||
@@ -7,7 +7,7 @@ class PaginatorCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('paginator', {
|
||||
template: `
|
||||
<div class="text-center">
|
||||
|
||||
@@ -14,18 +14,32 @@
|
||||
<option value="text">Text</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="enum">Dropdown List</option>
|
||||
<option value="query">Query Based Dropdown List</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="datetime-local">Date and Time</option>
|
||||
<option value="datetime-with-seconds">Date and Time (with seconds)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Global</label>
|
||||
<input type="checkbox" class="form-inline" ng-model="$ctrl.parameter.global">
|
||||
<label>
|
||||
<input type="checkbox" class="form-inline" ng-model="$ctrl.parameter.global">
|
||||
Global
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" ng-if="$ctrl.parameter.type === 'enum'">
|
||||
<label>Dropdown List Values (newline delimited)</label>
|
||||
<textarea class="form-control" rows="3" ng-model="$ctrl.parameter.enumOptions"></textarea>
|
||||
</div>
|
||||
<div class="form-group" ng-if="$ctrl.parameter.type === 'query'">
|
||||
<label>Query to load dropdown values from:</label>
|
||||
<ui-select ng-model="$ctrl.parameter.queryId" reset-search-input="false">
|
||||
<ui-select-match placeholder="Search a query by name">{{$select.selected.name}}</ui-select-match>
|
||||
<ui-select-choices repeat="q.id as q in $ctrl.queries"
|
||||
refresh="$ctrl.searchQueries($select.search)"
|
||||
refresh-delay="0">
|
||||
<div class="form-group" ng-bind-html="$ctrl.trustAsHtml(q.name | highlight: $select.search)"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<option ng-repeat="option in extractEnumOptions(param.enumOptions)" value="{{option}}">{{option}}</option>
|
||||
</select>
|
||||
</span>
|
||||
<span ng-switch-when="query">
|
||||
<query-based-parameter param="param" query-id="param.queryId"></query-based-parameter>
|
||||
</span>
|
||||
<input ng-switch-default type="{{param.type}}" class="form-control" ng-model="param.ngModel">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { find } from 'underscore';
|
||||
import template from './parameters.html';
|
||||
import queryBasedParameterTemplate from './query-based-parameter.html';
|
||||
import parameterSettingsTemplate from './parameter-settings.html';
|
||||
|
||||
const ParameterSettingsComponent = {
|
||||
@@ -8,10 +10,96 @@ const ParameterSettingsComponent = {
|
||||
close: '&',
|
||||
dismiss: '&',
|
||||
},
|
||||
controller() {
|
||||
controller($sce, Query) {
|
||||
'ngInject';
|
||||
|
||||
this.trustAsHtml = html => $sce.trustAsHtml(html);
|
||||
this.parameter = this.resolve.parameter;
|
||||
|
||||
if (this.parameter.queryId) {
|
||||
Query.get({ id: this.parameter.queryId }, (query) => {
|
||||
this.queries = [query];
|
||||
});
|
||||
}
|
||||
|
||||
this.searchQueries = (term) => {
|
||||
if (!term || term.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
Query.search({ q: term }, (results) => {
|
||||
this.queries = results;
|
||||
});
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function optionsFromQueryResult(queryResult) {
|
||||
const columns = queryResult.data.columns;
|
||||
const numColumns = columns.length;
|
||||
let options = [];
|
||||
// If there are multiple columns, check if there is a column
|
||||
// named 'name' and column named 'value'. If name column is present
|
||||
// in results, use name from name column. Similar for value column.
|
||||
// Default: Use first string column for name and value.
|
||||
if (numColumns > 0) {
|
||||
let nameColumn = null;
|
||||
let valueColumn = null;
|
||||
columns.forEach((column) => {
|
||||
const columnName = column.name.toLowerCase();
|
||||
if (columnName === 'name') {
|
||||
nameColumn = column.name;
|
||||
}
|
||||
if (columnName === 'value') {
|
||||
valueColumn = column.name;
|
||||
}
|
||||
// Assign first string column as name and value column.
|
||||
if (nameColumn === null) {
|
||||
nameColumn = column.name;
|
||||
}
|
||||
if (valueColumn === null) {
|
||||
valueColumn = column.name;
|
||||
}
|
||||
});
|
||||
if (nameColumn !== null && valueColumn !== null) {
|
||||
options = queryResult.data.rows.map((row) => {
|
||||
const queryResultOption = {
|
||||
name: row[nameColumn],
|
||||
value: row[valueColumn],
|
||||
};
|
||||
return queryResultOption;
|
||||
});
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function updateCurrentValue(param, options) {
|
||||
const found = find(options, option => option.value === param.value) !== undefined;
|
||||
|
||||
if (!found) {
|
||||
param.value = options[0].value;
|
||||
}
|
||||
}
|
||||
|
||||
const QueryBasedParameterComponent = {
|
||||
template: queryBasedParameterTemplate,
|
||||
bindings: {
|
||||
param: '<',
|
||||
queryId: '<',
|
||||
},
|
||||
controller(Query) {
|
||||
'ngInject';
|
||||
|
||||
this.$onChanges = (changes) => {
|
||||
if (changes.queryId) {
|
||||
Query.resultById({ id: this.queryId }, (result) => {
|
||||
const queryResult = result.query_result;
|
||||
this.queryResultOptions = optionsFromQueryResult(queryResult);
|
||||
updateCurrentValue(this.param, this.queryResultOptions);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,6 +128,7 @@ function ParametersDirective($location, $uibModal) {
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
// These are input as newline delimited values,
|
||||
// so we split them here.
|
||||
scope.extractEnumOptions = (enumOptions) => {
|
||||
@@ -60,7 +149,8 @@ function ParametersDirective($location, $uibModal) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('parameters', ParametersDirective);
|
||||
ngModule.component('queryBasedParameter', QueryBasedParameterComponent);
|
||||
ngModule.component('parameterSettings', ParameterSettingsComponent);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const PermissionsEditorComponent = {
|
||||
this.newGrantees = {};
|
||||
this.aclUrl = this.resolve.aclUrl.url;
|
||||
|
||||
// List users that are granted permissions
|
||||
// List users that are granted permissions
|
||||
const loadGrantees = () => {
|
||||
$http.get(this.aclUrl).success((result) => {
|
||||
this.grantees = [];
|
||||
@@ -31,7 +31,7 @@ const PermissionsEditorComponent = {
|
||||
|
||||
loadGrantees();
|
||||
|
||||
// Search for user
|
||||
// Search for user
|
||||
this.findUser = (search) => {
|
||||
if (search === '') {
|
||||
return;
|
||||
@@ -46,7 +46,7 @@ const PermissionsEditorComponent = {
|
||||
}
|
||||
};
|
||||
|
||||
// Add new user to grantees list
|
||||
// Add new user to grantees list
|
||||
this.addGrantee = (user) => {
|
||||
this.newGrantees.selected = undefined;
|
||||
const body = { access_type: 'modify', user_id: user.id };
|
||||
@@ -56,10 +56,11 @@ const PermissionsEditorComponent = {
|
||||
});
|
||||
};
|
||||
|
||||
// Remove user from grantees list
|
||||
// Remove user from grantees list
|
||||
this.removeGrantee = (user) => {
|
||||
const body = { access_type: 'modify', user_id: user.id };
|
||||
$http({ url: this.aclUrl,
|
||||
$http({
|
||||
url: this.aclUrl,
|
||||
method: 'DELETE',
|
||||
data: body,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -74,6 +75,6 @@ const PermissionsEditorComponent = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('permissionsEditor', PermissionsEditorComponent);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,6 @@ function alertUnsavedChanges($window) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('alertUnsavedChanges', alertUnsavedChanges);
|
||||
}
|
||||
@@ -32,6 +32,6 @@ const ApiKeyDialog = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('apiKeyDialog', ApiKeyDialog);
|
||||
}
|
||||
17
client/app/components/queries/embed-code-dialog.html
Normal file
17
client/app/components/queries/embed-code-dialog.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<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">Embed Code</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>IFrame Embed</h5>
|
||||
<div>
|
||||
<code><iframe src="{{ $ctrl.embedUrl }}" width="720" height="391"></iframe></code>
|
||||
</div>
|
||||
<span class="text-muted">(height should be adjusted)</span>
|
||||
<div ng-if="$ctrl.snapshotUrl">
|
||||
<h5>Image Embed</h5>
|
||||
<div>
|
||||
<code style="overflow-wrap:break-word;">{{$ctrl.snapshotUrl}}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -20,6 +20,6 @@ const EmbedCodeDialog = {
|
||||
template,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('embedCodeDialog', EmbedCodeDialog);
|
||||
}
|
||||
@@ -113,13 +113,12 @@ function queryEditor(QuerySnippet) {
|
||||
});
|
||||
|
||||
$scope.schema.keywords = map(keywords, (v, k) =>
|
||||
({
|
||||
name: k,
|
||||
value: k,
|
||||
score: 0,
|
||||
meta: v,
|
||||
})
|
||||
);
|
||||
({
|
||||
name: k,
|
||||
value: k,
|
||||
score: 0,
|
||||
meta: v,
|
||||
}));
|
||||
}
|
||||
callback(null, $scope.schema.keywords);
|
||||
},
|
||||
@@ -134,6 +133,6 @@ function queryEditor(QuerySnippet) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('queryEditor', queryEditor);
|
||||
}
|
||||
@@ -21,6 +21,6 @@ function queryResultLink() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('queryResultLink', queryResultLink);
|
||||
}
|
||||
@@ -38,9 +38,9 @@ function queryTimePicker() {
|
||||
|
||||
$scope.updateSchedule = () => {
|
||||
const newSchedule = moment().hour($scope.hour)
|
||||
.minute($scope.minute)
|
||||
.utc()
|
||||
.format('HH:mm');
|
||||
.minute($scope.minute)
|
||||
.utc()
|
||||
.format('HH:mm');
|
||||
|
||||
if (newSchedule !== $scope.query.schedule) {
|
||||
$scope.query.schedule = newSchedule;
|
||||
@@ -143,7 +143,7 @@ const ScheduleForm = {
|
||||
template,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('queryTimePicker', queryTimePicker);
|
||||
ngModule.directive('queryRefreshSelect', queryRefreshSelect);
|
||||
ngModule.component('scheduleDialog', ScheduleForm);
|
||||
@@ -28,6 +28,6 @@ const SchemaBrowser = {
|
||||
template,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('schemaBrowser', SchemaBrowser);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { find } from 'underscore';
|
||||
import logoUrl from '@/assets/images/redash_icon_small.png';
|
||||
import template from './visualization-embed.html';
|
||||
import logoUrl from '../../assets/images/redash_icon_small.png';
|
||||
|
||||
const VisualizationEmbed = {
|
||||
template,
|
||||
@@ -22,7 +22,7 @@ const VisualizationEmbed = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('visualizationEmbed', VisualizationEmbed);
|
||||
|
||||
function session($http, $route, Auth) {
|
||||
2
client/app/components/query-based-parameter.html
Normal file
2
client/app/components/query-based-parameter.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<select ng-model="$ctrl.param.value" class="form-control" ng-options="option.value as option.name for option in $ctrl.queryResultOptions">
|
||||
</select>
|
||||
@@ -13,7 +13,7 @@ function QueryLinkController() {
|
||||
this.link = this.query.getUrl(false, hash);
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('queryLink', {
|
||||
bindings: {
|
||||
query: '<',
|
||||
|
||||
@@ -11,15 +11,14 @@ function rdTab($location) {
|
||||
replace: true,
|
||||
link(scope) {
|
||||
scope.basePath = scope.basePath || $location.path().substring(1);
|
||||
scope.$watch(() =>
|
||||
scope.$parent.selectedTab
|
||||
, (tab) => {
|
||||
scope.selectedTab = tab;
|
||||
});
|
||||
scope.$watch(
|
||||
() => scope.$parent.selectedTab,
|
||||
(tab) => { scope.selectedTab = tab; },
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('rdTab', rdTab);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ const RdTimeAgo = {
|
||||
'</span>',
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('rdTimeAgo', RdTimeAgo);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ function rdTimer() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('rdTimer', rdTimer);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('routeStatus', {
|
||||
template: '<overlay ng-if="$ctrl.permissionDenied">You do not have permission to load this page.',
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
<li ng-class="{'active': usersPage }" ng-if="showUsersLink"><a href="users">Users</a></li>
|
||||
<li ng-class="{'active': groupsPage }" ng-if="showGroupsLink"><a href="groups">Groups</a></li>
|
||||
<li ng-class="{'active': destinationsPage }" ng-if="showDestinationsLink"><a href="destinations">Alert Destinations</a></li>
|
||||
<li ng-class="{'active': snippetsPage }"><a href="query_snippets">Query Snippets</a></li>
|
||||
<li ng-class="{'active': snippetsPage }" ng-if="showQuerySnippetsLink"><a href="query_snippets">Query Snippets</a></li>
|
||||
</ul>
|
||||
|
||||
<div ng-transclude>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,24 +1,23 @@
|
||||
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');
|
||||
export default function init(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');
|
||||
},
|
||||
})
|
||||
);
|
||||
$scope.showGroupsLink = currentUser.hasPermission('list_users');
|
||||
$scope.showUsersLink = currentUser.hasPermission('list_users');
|
||||
$scope.showDsLink = currentUser.hasPermission('admin');
|
||||
$scope.showDestinationsLink = currentUser.hasPermission('admin');
|
||||
$scope.showQuerySnippetsLink = currentUser.hasPermission('create_query');
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('sortIcon', {
|
||||
template: '<span ng-if="$ctrl.showIcon"><i class="fa fa-sort-{{$ctrl.icon}}"></i></span>',
|
||||
bindings: {
|
||||
|
||||
@@ -10,7 +10,7 @@ function controller($location) {
|
||||
});
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('tabNav', {
|
||||
template: '<ul class="tab-nav bg-white">' +
|
||||
'<li ng-repeat="tab in $ctrl.tabs" ng-class="{\'active\': tab.active }"><a ng-href="{{tab.path}}">{{tab.name}}</a></li>' +
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function VisualizationName(Visualization) {
|
||||
export default function VisualizationName(Visualization) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@@ -7,7 +7,10 @@ function VisualizationName(Visualization) {
|
||||
template: '{{name}}',
|
||||
replace: false,
|
||||
link(scope) {
|
||||
if (Visualization.visualizations[scope.visualization.type].name !== scope.visualization.name) {
|
||||
const currentType = scope.visualization.type;
|
||||
const nameByType = Visualization.visualizations[currentType].name;
|
||||
const currentName = scope.visualization.name;
|
||||
if (nameByType !== currentName) {
|
||||
scope.name = scope.visualization.name;
|
||||
}
|
||||
},
|
||||
|
||||
111
client/app/config/index.js
Normal file
111
client/app/config/index.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// This polyfill is needed to support PhantomJS which we use to generate PNGs from embeds.
|
||||
import 'core-js/fn/typed/array-buffer';
|
||||
|
||||
import 'pace-progress';
|
||||
import debug from 'debug';
|
||||
import angular from 'angular';
|
||||
import ngSanitize from 'angular-sanitize';
|
||||
import ngRoute from 'angular-route';
|
||||
import ngResource from 'angular-resource';
|
||||
import uiBootstrap from 'angular-ui-bootstrap';
|
||||
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 ngGridster from 'angular-gridster';
|
||||
import { each } from 'underscore';
|
||||
|
||||
import '@/lib/sortable';
|
||||
|
||||
import * as filters from '@/filters';
|
||||
import registerDirectives from '@/directives';
|
||||
import markdownFilter from '@/filters/markdown';
|
||||
import dateTimeFilter from '@/filters/datetime';
|
||||
|
||||
const logger = debug('redash:config');
|
||||
|
||||
const requirements = [
|
||||
ngRoute,
|
||||
ngResource,
|
||||
ngSanitize,
|
||||
uiBootstrap,
|
||||
ngMessages,
|
||||
uiSelect,
|
||||
'angularMoment',
|
||||
toastr,
|
||||
'ui.ace',
|
||||
ngUpload,
|
||||
'angularResizable',
|
||||
vsRepeat,
|
||||
'ui.sortable',
|
||||
ngGridster.name,
|
||||
];
|
||||
|
||||
const ngModule = angular.module('app', requirements);
|
||||
|
||||
function registerAll(context) {
|
||||
const modules = context
|
||||
.keys()
|
||||
.map(context)
|
||||
.map(module => module.default);
|
||||
|
||||
return modules.map(f => f(ngModule));
|
||||
}
|
||||
|
||||
function registerComponents() {
|
||||
// We repeat this code in other register functions, because if we don't use a literal for the path
|
||||
// Webpack won't be able to statcily analyze our imports.
|
||||
const context = require.context('@/components', true, /^((?![\\/]test[\\/]).)*\.js$/);
|
||||
registerAll(context);
|
||||
}
|
||||
|
||||
function registerServices() {
|
||||
const context = require.context('@/services', true, /^((?![\\/]test[\\/]).)*\.js$/);
|
||||
registerAll(context);
|
||||
}
|
||||
|
||||
function registerVisualizations() {
|
||||
const context = require.context('@/visualizations', true, /^((?![\\/]test[\\/]).)*\.js$/);
|
||||
registerAll(context);
|
||||
}
|
||||
|
||||
function registerPages() {
|
||||
const context = require.context('@/pages', true, /^((?![\\/]test[\\/]).)*\.js$/);
|
||||
const routesCollection = registerAll(context);
|
||||
routesCollection.forEach((routes) => {
|
||||
ngModule.config(($routeProvider) => {
|
||||
each(routes, (route, path) => {
|
||||
logger('Registering route: %s', path);
|
||||
// This is a workaround, to make sure app-header and footer are loaded only
|
||||
// for the authenticated routes.
|
||||
// We should look into switching to ui-router, that has built in support for
|
||||
// such things.
|
||||
route.template = `<app-header></app-header><route-status></route-status>${route.template}<footer></footer>`;
|
||||
route.authenticated = true;
|
||||
$routeProvider.when(path, route);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerFilters() {
|
||||
each(filters, (filter, name) => {
|
||||
ngModule.filter(name, () => filter);
|
||||
});
|
||||
}
|
||||
|
||||
registerDirectives(ngModule);
|
||||
registerServices();
|
||||
registerFilters();
|
||||
markdownFilter(ngModule);
|
||||
dateTimeFilter(ngModule);
|
||||
registerComponents();
|
||||
registerPages();
|
||||
registerVisualizations(ngModule);
|
||||
|
||||
export default ngModule;
|
||||
11
client/app/config/styles.js
Normal file
11
client/app/config/styles.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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 'angular-gridster/dist/angular-gridster.css';
|
||||
import 'pace-progress/themes/blue/pace-theme-minimal.css';
|
||||
|
||||
import '@/assets/css/superflat_redash.css';
|
||||
import '@/assets/css/redash.css';
|
||||
import '@/assets/css/main.scss';
|
||||
@@ -70,7 +70,7 @@ function title($rootScope, Title) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.factory('Title', TitleService);
|
||||
ngModule.directive('title', title);
|
||||
ngModule.directive('compareTo', compareTo);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.filter('toMilliseconds', () => value => value * 1000.0);
|
||||
|
||||
ngModule.filter('dateTime', clientConfig =>
|
||||
function dateTime(value) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
function dateTime(value) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return moment(value).format(clientConfig.dateTimeFormat);
|
||||
}
|
||||
);
|
||||
return moment(value).format(clientConfig.dateTimeFormat);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ export function scheduleHumanize(schedule) {
|
||||
} else if (schedule.match(/\d\d:\d\d/) !== null) {
|
||||
const parts = schedule.split(':');
|
||||
const localTime = moment.utc()
|
||||
.hour(parts[0])
|
||||
.minute(parts[1])
|
||||
.local()
|
||||
.format('HH:mm');
|
||||
.hour(parts[0])
|
||||
.minute(parts[1])
|
||||
.local()
|
||||
.format('HH:mm');
|
||||
|
||||
return `Every day at ${localTime}`;
|
||||
}
|
||||
@@ -45,8 +45,7 @@ export function scheduleHumanize(schedule) {
|
||||
|
||||
export function toHuman(text) {
|
||||
return text.replace(/_/g, ' ').replace(/(?:^|\s)\S/g, a =>
|
||||
a.toUpperCase()
|
||||
);
|
||||
a.toUpperCase());
|
||||
}
|
||||
|
||||
export function colWidth(widgetWidth) {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import marked from 'marked';
|
||||
import { markdown } from 'markdown';
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.filter('markdown', ($sce, clientConfig) =>
|
||||
function markdown(text) {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
function parseMarkdown(text) {
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let html = marked(String(text));
|
||||
if (clientConfig.allowScriptsInUserInput) {
|
||||
html = $sce.trustAsHtml(html);
|
||||
}
|
||||
let html = markdown.toHTML(String(text));
|
||||
if (clientConfig.allowScriptsInUserInput) {
|
||||
html = $sce.trustAsHtml(html);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
);
|
||||
return html;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,105 +1,7 @@
|
||||
// This polyfill is needed to support PhantomJS which we use to generate PNGs from embeds.
|
||||
import 'core-js/fn/typed/array-buffer';
|
||||
import '@/config/styles';
|
||||
import ngModule from '@/config';
|
||||
|
||||
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 'angular-gridster/dist/angular-gridster.css';
|
||||
import 'pace-progress/themes/blue/pace-theme-minimal.css';
|
||||
|
||||
import 'pace-progress';
|
||||
import debug from 'debug';
|
||||
import angular from 'angular';
|
||||
import ngSanitize from 'angular-sanitize';
|
||||
import ngRoute from 'angular-route';
|
||||
import ngResource from 'angular-resource';
|
||||
import uiBootstrap from 'angular-ui-bootstrap';
|
||||
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 ngGridster from 'angular-gridster';
|
||||
import { each } from 'underscore';
|
||||
|
||||
import './sortable';
|
||||
|
||||
import './assets/css/superflat_redash.css';
|
||||
import './assets/css/redash.css';
|
||||
import './assets/css/main.scss';
|
||||
|
||||
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';
|
||||
import dateTimeFilter from './filters/datetime';
|
||||
|
||||
const logger = debug('redash');
|
||||
|
||||
const requirements = [
|
||||
ngRoute, ngResource, ngSanitize, uiBootstrap, ngMessages, uiSelect, 'angularMoment', toastr, 'ui.ace',
|
||||
ngUpload, 'angularResizable', vsRepeat, 'ui.sortable', ngGridster.name,
|
||||
];
|
||||
|
||||
const ngModule = angular.module('app', requirements);
|
||||
|
||||
function registerComponents() {
|
||||
each(components, (register) => {
|
||||
register(ngModule);
|
||||
});
|
||||
}
|
||||
|
||||
function registerServices() {
|
||||
each(services, (register) => {
|
||||
register(ngModule);
|
||||
});
|
||||
}
|
||||
|
||||
function registerPages() {
|
||||
each(pages, (registerPage) => {
|
||||
const routes = registerPage(ngModule);
|
||||
|
||||
ngModule.config(($routeProvider) => {
|
||||
each(routes, (route, path) => {
|
||||
logger('Route: ', path);
|
||||
// This is a workaround, to make sure app-header and footer are loaded only
|
||||
// for the authenticated routes.
|
||||
// We should look into switching to ui-router, that has built in support for
|
||||
// such things.
|
||||
route.template = `<app-header></app-header><route-status></route-status>${route.template}<footer></footer>`;
|
||||
route.authenticated = true;
|
||||
$routeProvider.when(path, route);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerFilters() {
|
||||
each(filters, (filter, name) => {
|
||||
ngModule.filter(name, () => filter);
|
||||
});
|
||||
}
|
||||
|
||||
registerDirectives(ngModule);
|
||||
registerServices();
|
||||
registerFilters();
|
||||
markdownFilter(ngModule);
|
||||
dateTimeFilter(ngModule);
|
||||
registerComponents();
|
||||
registerPages();
|
||||
registerVisualizations(ngModule);
|
||||
|
||||
ngModule.config(($routeProvider, $locationProvider, $compileProvider,
|
||||
uiSelectConfig, toastrConfig) => {
|
||||
ngModule.config(($locationProvider, $compileProvider, uiSelectConfig, toastrConfig) => {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
|
||||
$locationProvider.html5Mode(true);
|
||||
@@ -112,7 +14,8 @@ ngModule.config(($routeProvider, $locationProvider, $compileProvider,
|
||||
});
|
||||
|
||||
// Update ui-select's template to use Font-Awesome instead of glyphicon.
|
||||
ngModule.run(($templateCache, OfflineListener) => { // eslint-disable-line no-unused-vars
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
ngModule.run(($templateCache, OfflineListener) => {
|
||||
const templateName = 'bootstrap/match.tpl.html';
|
||||
let template = $templateCache.get(templateName);
|
||||
template = template.replace('glyphicon glyphicon-remove', 'fa fa-remove');
|
||||
|
||||
328
client/app/lib/visualizations/d3box.js
vendored
Normal file
328
client/app/lib/visualizations/d3box.js
vendored
Normal file
@@ -0,0 +1,328 @@
|
||||
/* eslint-disable */
|
||||
// Inspired by http://informationandvisualization.de/blog/box-plot
|
||||
function box() {
|
||||
let width = 1,
|
||||
height = 1,
|
||||
duration = 0,
|
||||
domain = null,
|
||||
value = Number,
|
||||
whiskers = boxWhiskers,
|
||||
quartiles = boxQuartiles,
|
||||
tickFormat = null;
|
||||
|
||||
// For each small multiple…
|
||||
function box(g) {
|
||||
g.each(function(d, i) {
|
||||
d = d.map(value).sort(d3.ascending);
|
||||
let g = d3.select(this),
|
||||
n = d.length,
|
||||
min = d[0],
|
||||
max = d[n - 1];
|
||||
|
||||
// Compute quartiles. Must return exactly 3 elements.
|
||||
const quartileData = (d.quartiles = quartiles(d));
|
||||
|
||||
// Compute whiskers. Must return exactly 2 elements, or null.
|
||||
let whiskerIndices = whiskers && whiskers.call(this, d, i),
|
||||
whiskerData = whiskerIndices && whiskerIndices.map(i => d[i]);
|
||||
|
||||
// Compute outliers. If no whiskers are specified, all data are "outliers".
|
||||
// We compute the outliers as indices, so that we can join across transitions!
|
||||
const outlierIndices = whiskerIndices
|
||||
? d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n))
|
||||
: d3.range(n);
|
||||
|
||||
// Compute the new x-scale.
|
||||
const x1 = d3.scale
|
||||
.linear()
|
||||
.domain((domain && domain.call(this, d, i)) || [min, max])
|
||||
.range([height, 0]);
|
||||
|
||||
// Retrieve the old x-scale, if this is an update.
|
||||
const x0 =
|
||||
this.__chart__ ||
|
||||
d3.scale
|
||||
.linear()
|
||||
.domain([0, Infinity])
|
||||
.range(x1.range());
|
||||
|
||||
// Stash the new scale.
|
||||
this.__chart__ = x1;
|
||||
|
||||
// Note: the box, median, and box tick elements are fixed in number,
|
||||
// so we only have to handle enter and update. In contrast, the outliers
|
||||
// and other elements are variable, so we need to exit them! Variable
|
||||
// elements also fade in and out.
|
||||
|
||||
// Update center line: the vertical line spanning the whiskers.
|
||||
const center = g.selectAll('line.center').data(whiskerData ? [whiskerData] : []);
|
||||
|
||||
center
|
||||
.enter()
|
||||
.insert('line', 'rect')
|
||||
.attr('class', 'center')
|
||||
.attr('x1', width / 2)
|
||||
.attr('y1', d => x0(d[0]))
|
||||
.attr('x2', width / 2)
|
||||
.attr('y2', d => x0(d[1]))
|
||||
.style('opacity', 1e-6)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.style('opacity', 1)
|
||||
.attr('y1', d => x1(d[0]))
|
||||
.attr('y2', d => x1(d[1]));
|
||||
|
||||
center
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.style('opacity', 1)
|
||||
.attr('y1', d => x1(d[0]))
|
||||
.attr('y2', d => x1(d[1]));
|
||||
|
||||
center
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.style('opacity', 1e-6)
|
||||
.attr('y1', d => x1(d[0]))
|
||||
.attr('y2', d => x1(d[1]))
|
||||
.remove();
|
||||
|
||||
// Update innerquartile box.
|
||||
const box = g.selectAll('rect.box').data([quartileData]);
|
||||
|
||||
box
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('class', 'box')
|
||||
.attr('x', 0)
|
||||
.attr('y', d => x0(d[2]))
|
||||
.attr('width', width)
|
||||
.attr('height', d => x0(d[0]) - x0(d[2]))
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y', d => x1(d[2]))
|
||||
.attr('height', d => x1(d[0]) - x1(d[2]));
|
||||
|
||||
box
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y', d => x1(d[2]))
|
||||
.attr('height', d => x1(d[0]) - x1(d[2]));
|
||||
|
||||
box.exit().remove();
|
||||
|
||||
// Update median line.
|
||||
const medianLine = g.selectAll('line.median').data([quartileData[1]]);
|
||||
|
||||
medianLine
|
||||
.enter()
|
||||
.append('line')
|
||||
.attr('class', 'median')
|
||||
.attr('x1', 0)
|
||||
.attr('y1', x0)
|
||||
.attr('x2', width)
|
||||
.attr('y2', x0)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y1', x1)
|
||||
.attr('y2', x1);
|
||||
|
||||
medianLine
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y1', x1)
|
||||
.attr('y2', x1);
|
||||
|
||||
medianLine.exit().remove();
|
||||
|
||||
// Update whiskers.
|
||||
const whisker = g.selectAll('line.whisker').data(whiskerData || []);
|
||||
|
||||
whisker
|
||||
.enter()
|
||||
.insert('line', 'circle, text')
|
||||
.attr('class', 'whisker')
|
||||
.attr('x1', 0)
|
||||
.attr('y1', x0)
|
||||
.attr('x2', width)
|
||||
.attr('y2', x0)
|
||||
.style('opacity', 1e-6)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y1', x1)
|
||||
.attr('y2', x1)
|
||||
.style('opacity', 1);
|
||||
|
||||
whisker
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y1', x1)
|
||||
.attr('y2', x1)
|
||||
.style('opacity', 1);
|
||||
|
||||
whisker
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y1', x1)
|
||||
.attr('y2', x1)
|
||||
.style('opacity', 1e-6)
|
||||
.remove();
|
||||
|
||||
// Update outliers.
|
||||
const outlier = g.selectAll('circle.outlier').data(outlierIndices, Number);
|
||||
|
||||
outlier
|
||||
.enter()
|
||||
.insert('circle', 'text')
|
||||
.attr('class', 'outlier')
|
||||
.attr('r', 5)
|
||||
.attr('cx', width / 2)
|
||||
.attr('cy', i => x0(d[i]))
|
||||
.style('opacity', 1e-6)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('cy', i => x1(d[i]))
|
||||
.style('opacity', 1);
|
||||
|
||||
outlier
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('cy', i => x1(d[i]))
|
||||
.style('opacity', 1);
|
||||
|
||||
outlier
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('cy', i => x1(d[i]))
|
||||
.style('opacity', 1e-6)
|
||||
.remove();
|
||||
|
||||
// Compute the tick format.
|
||||
const format = tickFormat || x1.tickFormat(8);
|
||||
|
||||
// Update box ticks.
|
||||
const boxTick = g.selectAll('text.box').data(quartileData);
|
||||
|
||||
boxTick
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('class', 'box')
|
||||
.attr('dy', '.3em')
|
||||
.attr('dx', (d, i) => (i & 1 ? 6 : -6))
|
||||
.attr('x', (d, i) => (i & 1 ? width : 0))
|
||||
.attr('y', x0)
|
||||
.attr('text-anchor', (d, i) => (i & 1 ? 'start' : 'end'))
|
||||
.text(format)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y', x1);
|
||||
|
||||
boxTick
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.text(format)
|
||||
.attr('y', x1);
|
||||
|
||||
boxTick.exit().remove();
|
||||
|
||||
// Update whisker ticks. These are handled separately from the box
|
||||
// ticks because they may or may not exist, and we want don't want
|
||||
// to join box ticks pre-transition with whisker ticks post-.
|
||||
const whiskerTick = g.selectAll('text.whisker').data(whiskerData || []);
|
||||
|
||||
whiskerTick
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('class', 'whisker')
|
||||
.attr('dy', '.3em')
|
||||
.attr('dx', 6)
|
||||
.attr('x', width)
|
||||
.attr('y', x0)
|
||||
.text(format)
|
||||
.style('opacity', 1e-6)
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y', x1)
|
||||
.style('opacity', 1);
|
||||
|
||||
whiskerTick
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.text(format)
|
||||
.attr('y', x1)
|
||||
.style('opacity', 1);
|
||||
|
||||
whiskerTick
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(duration)
|
||||
.attr('y', x1)
|
||||
.style('opacity', 1e-6)
|
||||
.remove();
|
||||
});
|
||||
d3.timer.flush();
|
||||
}
|
||||
|
||||
box.width = function(x) {
|
||||
if (!arguments.length) return width;
|
||||
width = x;
|
||||
return box;
|
||||
};
|
||||
|
||||
box.height = function(x) {
|
||||
if (!arguments.length) return height;
|
||||
height = x;
|
||||
return box;
|
||||
};
|
||||
|
||||
box.tickFormat = function(x) {
|
||||
if (!arguments.length) return tickFormat;
|
||||
tickFormat = x;
|
||||
return box;
|
||||
};
|
||||
|
||||
box.duration = function(x) {
|
||||
if (!arguments.length) return duration;
|
||||
duration = x;
|
||||
return box;
|
||||
};
|
||||
|
||||
box.domain = function(x) {
|
||||
if (!arguments.length) return domain;
|
||||
domain = x == null ? x : d3.functor(x);
|
||||
return box;
|
||||
};
|
||||
|
||||
box.value = function(x) {
|
||||
if (!arguments.length) return value;
|
||||
value = x;
|
||||
return box;
|
||||
};
|
||||
|
||||
box.whiskers = function(x) {
|
||||
if (!arguments.length) return whiskers;
|
||||
whiskers = x;
|
||||
return box;
|
||||
};
|
||||
|
||||
box.quartiles = function(x) {
|
||||
if (!arguments.length) return quartiles;
|
||||
quartiles = x;
|
||||
return box;
|
||||
};
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
function boxWhiskers(d) {
|
||||
return [0, d.length - 1];
|
||||
}
|
||||
|
||||
function boxQuartiles(d) {
|
||||
return [d3.quantile(d, 0.25), d3.quantile(d, 0.5), d3.quantile(d, 0.75)];
|
||||
}
|
||||
|
||||
export default box;
|
||||
@@ -10,7 +10,7 @@ function value(link) {
|
||||
return link.value;
|
||||
}
|
||||
|
||||
export default function() {
|
||||
function Sankey() {
|
||||
const sankey = {};
|
||||
let nodeWidth = 24;
|
||||
let nodePadding = 8;
|
||||
@@ -21,11 +21,11 @@ export default function() {
|
||||
// Populate the sourceLinks and targetLinks for each node.
|
||||
// Also, if the source and target are not objects, assume they are indices.
|
||||
function computeNodeLinks() {
|
||||
nodes.forEach((node) => {
|
||||
nodes.forEach(node => {
|
||||
node.sourceLinks = [];
|
||||
node.targetLinks = [];
|
||||
});
|
||||
links.forEach((link) => {
|
||||
links.forEach(link => {
|
||||
let source = link.source;
|
||||
let target = link.target;
|
||||
if (typeof source === 'number') source = link.source = nodes[link.source];
|
||||
@@ -37,16 +37,13 @@ export default function() {
|
||||
|
||||
// Compute the value (size) of each node by summing the associated links.
|
||||
function computeNodeValues() {
|
||||
nodes.forEach((node) => {
|
||||
node.value = Math.max(
|
||||
d3.sum(node.sourceLinks, value),
|
||||
d3.sum(node.targetLinks, value)
|
||||
);
|
||||
nodes.forEach(node => {
|
||||
node.value = Math.max(d3.sum(node.sourceLinks, value), d3.sum(node.targetLinks, value));
|
||||
});
|
||||
}
|
||||
|
||||
function moveSinksRight(x) {
|
||||
nodes.forEach((node) => {
|
||||
nodes.forEach(node => {
|
||||
if (!node.sourceLinks.length) {
|
||||
node.x = x - 1;
|
||||
}
|
||||
@@ -54,7 +51,7 @@ export default function() {
|
||||
}
|
||||
|
||||
function scaleNodeBreadths(kx) {
|
||||
nodes.forEach((node) => {
|
||||
nodes.forEach(node => {
|
||||
node.x *= kx;
|
||||
});
|
||||
}
|
||||
@@ -71,7 +68,7 @@ export default function() {
|
||||
function assignBreadth(node) {
|
||||
node.x = x;
|
||||
node.dx = nodeWidth;
|
||||
node.sourceLinks.forEach((link) => {
|
||||
node.sourceLinks.forEach(link => {
|
||||
if (nextNodes.indexOf(link.target) < 0) {
|
||||
nextNodes.push(link.target);
|
||||
}
|
||||
@@ -91,7 +88,7 @@ export default function() {
|
||||
}
|
||||
|
||||
function moveSourcesRight() {
|
||||
nodes.forEach((node) => {
|
||||
nodes.forEach(node => {
|
||||
if (!node.targetLinks.length) {
|
||||
node.x = d3.min(node.sourceLinks, d => d.target.x) - 1;
|
||||
}
|
||||
@@ -99,25 +96,27 @@ export default function() {
|
||||
}
|
||||
|
||||
function computeNodeDepths(iterations) {
|
||||
const nodesByBreadth = d3.nest()
|
||||
.key(d => d.x)
|
||||
.sortKeys(d3.ascending)
|
||||
.entries(nodes)
|
||||
.map(d => d.values);
|
||||
const nodesByBreadth = d3
|
||||
.nest()
|
||||
.key(d => d.x)
|
||||
.sortKeys(d3.ascending)
|
||||
.entries(nodes)
|
||||
.map(d => d.values);
|
||||
|
||||
function initializeNodeDepth() {
|
||||
const ky = d3.min(nodesByBreadth, n =>
|
||||
(size[1] - (n.length - 1) * nodePadding) / d3.sum(n, value)
|
||||
const ky = d3.min(
|
||||
nodesByBreadth,
|
||||
n => (size[1] - (n.length - 1) * nodePadding) / d3.sum(n, value),
|
||||
);
|
||||
|
||||
nodesByBreadth.forEach((n) => {
|
||||
nodesByBreadth.forEach(n => {
|
||||
n.forEach((node, i) => {
|
||||
node.y = i;
|
||||
node.dy = node.value * ky;
|
||||
});
|
||||
});
|
||||
|
||||
links.forEach((link) => {
|
||||
links.forEach(link => {
|
||||
link.dy = link.value * ky;
|
||||
});
|
||||
}
|
||||
@@ -127,8 +126,8 @@ export default function() {
|
||||
return center(link.source) * link.value;
|
||||
}
|
||||
|
||||
nodesByBreadth.forEach((n) => {
|
||||
n.forEach((node) => {
|
||||
nodesByBreadth.forEach(n => {
|
||||
n.forEach(node => {
|
||||
if (node.targetLinks.length) {
|
||||
const y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
|
||||
node.y += (y - center(node)) * alpha;
|
||||
@@ -138,7 +137,7 @@ export default function() {
|
||||
}
|
||||
|
||||
function resolveCollisions() {
|
||||
nodesByBreadth.forEach((nodes) => {
|
||||
nodesByBreadth.forEach(nodes => {
|
||||
const n = nodes.length;
|
||||
let node;
|
||||
let dy;
|
||||
@@ -171,7 +170,7 @@ export default function() {
|
||||
}
|
||||
|
||||
function resolveCollisions() {
|
||||
nodesByBreadth.forEach((nodes) => {
|
||||
nodesByBreadth.forEach(nodes => {
|
||||
let node,
|
||||
dy,
|
||||
y0 = 0,
|
||||
@@ -203,26 +202,28 @@ export default function() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
initializeNodeDepth();
|
||||
resolveCollisions();
|
||||
|
||||
for (let alpha = 1; iterations > 0; iterations -= 1) {
|
||||
relaxRightToLeft(alpha *= 0.99);
|
||||
relaxRightToLeft((alpha *= 0.99));
|
||||
resolveCollisions();
|
||||
relaxLeftToRight(alpha);
|
||||
resolveCollisions();
|
||||
}
|
||||
|
||||
function relaxRightToLeft(alpha) {
|
||||
nodesByBreadth.slice().reverse().forEach((nodes) => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.sourceLinks.length) {
|
||||
const y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
|
||||
node.y += (y - center(node)) * alpha;
|
||||
}
|
||||
nodesByBreadth
|
||||
.slice()
|
||||
.reverse()
|
||||
.forEach(nodes => {
|
||||
nodes.forEach(node => {
|
||||
if (node.sourceLinks.length) {
|
||||
const y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
|
||||
node.y += (y - center(node)) * alpha;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function weightedTarget(link) {
|
||||
return center(link.target) * link.value;
|
||||
@@ -235,18 +236,18 @@ export default function() {
|
||||
}
|
||||
|
||||
function computeLinkDepths() {
|
||||
nodes.forEach((node) => {
|
||||
nodes.forEach(node => {
|
||||
node.sourceLinks.sort(ascendingTargetDepth);
|
||||
node.targetLinks.sort(ascendingSourceDepth);
|
||||
});
|
||||
nodes.forEach((node) => {
|
||||
nodes.forEach(node => {
|
||||
let sy = 0,
|
||||
ty = 0;
|
||||
node.sourceLinks.forEach((link) => {
|
||||
node.sourceLinks.forEach(link => {
|
||||
link.sy = sy;
|
||||
sy += link.dy;
|
||||
});
|
||||
node.targetLinks.forEach((link) => {
|
||||
node.targetLinks.forEach(link => {
|
||||
link.ty = ty;
|
||||
ty += link.dy;
|
||||
});
|
||||
@@ -261,37 +262,37 @@ export default function() {
|
||||
}
|
||||
}
|
||||
|
||||
sankey.nodeWidth = function (_) {
|
||||
sankey.nodeWidth = function(_) {
|
||||
if (!arguments.length) return nodeWidth;
|
||||
nodeWidth = +_;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.nodePadding = function (_) {
|
||||
sankey.nodePadding = function(_) {
|
||||
if (!arguments.length) return nodePadding;
|
||||
nodePadding = +_;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.nodes = function (_) {
|
||||
sankey.nodes = function(_) {
|
||||
if (!arguments.length) return nodes;
|
||||
nodes = _;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.links = function (_) {
|
||||
sankey.links = function(_) {
|
||||
if (!arguments.length) return links;
|
||||
links = _;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.size = function (_) {
|
||||
sankey.size = function(_) {
|
||||
if (!arguments.length) return size;
|
||||
size = _;
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.layout = function (iterations) {
|
||||
sankey.layout = function(iterations) {
|
||||
computeNodeLinks();
|
||||
computeNodeValues();
|
||||
computeNodeBreadths();
|
||||
@@ -300,12 +301,12 @@ export default function() {
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.relayout = function () {
|
||||
sankey.relayout = function() {
|
||||
computeLinkDepths();
|
||||
return sankey;
|
||||
};
|
||||
|
||||
sankey.link = function () {
|
||||
sankey.link = function() {
|
||||
let curvature = 0.5;
|
||||
|
||||
function link(d) {
|
||||
@@ -317,13 +318,10 @@ export default function() {
|
||||
const y0 = d.source.y + d.sy + d.dy / 2;
|
||||
const y1 = d.target.y + d.ty + d.dy / 2;
|
||||
|
||||
return `M${x0},${y0
|
||||
}C${x2},${y0
|
||||
} ${x3},${y1
|
||||
} ${x1},${y1}`;
|
||||
return `M${x0},${y0}C${x2},${y0} ${x3},${y1} ${x1},${y1}`;
|
||||
}
|
||||
|
||||
link.curvature = (_) => {
|
||||
link.curvature = _ => {
|
||||
if (!arguments.length) return curvature;
|
||||
curvature = +_;
|
||||
return link;
|
||||
@@ -332,6 +330,7 @@ export default function() {
|
||||
return link;
|
||||
};
|
||||
|
||||
|
||||
return sankey;
|
||||
};
|
||||
}
|
||||
|
||||
export default Sankey;
|
||||
@@ -1,4 +1,4 @@
|
||||
import d3 from 'd3';
|
||||
import * as d3 from 'd3';
|
||||
import _ from 'underscore';
|
||||
import angular from 'angular';
|
||||
|
||||
@@ -10,7 +10,6 @@ function colorMap(d) {
|
||||
return colors(d.name);
|
||||
}
|
||||
|
||||
|
||||
// Return array of ancestors of nodes, highest first, but excluding the root.
|
||||
function getAncestors(node) {
|
||||
const path = [];
|
||||
@@ -24,7 +23,7 @@ function getAncestors(node) {
|
||||
}
|
||||
|
||||
// The following is based on @chrisrzhou's example from: http://bl.ocks.org/chrisrzhou/d5bdd8546f64ca0e4366.
|
||||
export default function Sunburst(scope, element) {
|
||||
function Sunburst(scope, element) {
|
||||
this.element = element;
|
||||
this.watches = [];
|
||||
|
||||
@@ -60,27 +59,18 @@ export default function Sunburst(scope, element) {
|
||||
let totalSize = 0;
|
||||
|
||||
// create d3.layout.partition
|
||||
const partition = d3.layout.partition()
|
||||
const partition = d3.layout
|
||||
.partition()
|
||||
.size([2 * Math.PI, radius * radius])
|
||||
.value(d =>
|
||||
d.size
|
||||
);
|
||||
.value(d => d.size);
|
||||
|
||||
// create arcs for drawing D3 paths
|
||||
const arc = d3.svg.arc()
|
||||
.startAngle(d =>
|
||||
d.x
|
||||
)
|
||||
.endAngle(d =>
|
||||
d.x + d.dx
|
||||
)
|
||||
.innerRadius(d =>
|
||||
Math.sqrt(d.y)
|
||||
)
|
||||
.outerRadius(d =>
|
||||
Math.sqrt(d.y + d.dy)
|
||||
);
|
||||
|
||||
const arc = d3.svg
|
||||
.arc()
|
||||
.startAngle(d => d.x)
|
||||
.endAngle(d => d.x + d.dx)
|
||||
.innerRadius(d => Math.sqrt(d.y))
|
||||
.outerRadius(d => Math.sqrt(d.y + d.dy));
|
||||
|
||||
/**
|
||||
* Define and initialize D3 select references and div-containers
|
||||
@@ -88,15 +78,18 @@ export default function Sunburst(scope, element) {
|
||||
* e.g. vis, breadcrumbs, lastCrumb, summary, sunburst, legend
|
||||
*/
|
||||
// create main vis selection
|
||||
const vis = d3.select(element[0])
|
||||
.append('div').classed('vis-container', true)
|
||||
const vis = d3
|
||||
.select(element[0])
|
||||
.append('div')
|
||||
.classed('vis-container', true)
|
||||
.style('position', 'relative')
|
||||
.style('margin-top', '5px')
|
||||
.style('height', `${height + 2 * b.h}px`);
|
||||
|
||||
// create and position breadcrumbs container and svg
|
||||
const breadcrumbs = vis
|
||||
.append('div').classed('breadcrumbs-container', true)
|
||||
.append('div')
|
||||
.classed('breadcrumbs-container', true)
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', b.h)
|
||||
@@ -107,7 +100,8 @@ export default function Sunburst(scope, element) {
|
||||
|
||||
// create and position SVG
|
||||
const sunburst = vis
|
||||
.append('div').classed('sunburst-container', true)
|
||||
.append('div')
|
||||
.classed('sunburst-container', true)
|
||||
.style('z-index', '2')
|
||||
// .style("margin-left", marginLeft + "px")
|
||||
.style('left', `${marginLeft}px`)
|
||||
@@ -123,9 +117,10 @@ export default function Sunburst(scope, element) {
|
||||
|
||||
// create and position summary container
|
||||
const summary = vis
|
||||
.append('div').classed('summary-container', true)
|
||||
.append('div')
|
||||
.classed('summary-container', true)
|
||||
.style('position', 'absolute')
|
||||
.style('top', `${b.h + radius * 0.80}px`)
|
||||
.style('top', `${b.h + radius * 0.8}px`)
|
||||
.style('left', `${marginLeft + radius / 2}px`)
|
||||
.style('width', `${radius}px`)
|
||||
.style('height', `${radius}px`)
|
||||
@@ -143,7 +138,8 @@ export default function Sunburst(scope, element) {
|
||||
points.push(`${b.w},${b.h}`);
|
||||
points.push(`0,${b.h}`);
|
||||
|
||||
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
|
||||
if (i > 0) {
|
||||
// Leftmost breadcrumb; don't include 6th vertex.
|
||||
points.push(`${b.t},${b.h / 2}`);
|
||||
}
|
||||
return points.join(' ');
|
||||
@@ -152,34 +148,29 @@ export default function Sunburst(scope, element) {
|
||||
// Update the breadcrumb breadcrumbs to show the current sequence and percentage.
|
||||
function updateBreadcrumbs(ancestors, percentageString) {
|
||||
// Data join, where primary key = name + depth.
|
||||
const g = breadcrumbs.selectAll('g')
|
||||
.data(ancestors, d =>
|
||||
d.name + d.depth
|
||||
);
|
||||
const g = breadcrumbs.selectAll('g').data(ancestors, d => d.name + d.depth);
|
||||
|
||||
// Add breadcrumb and label for entering nodes.
|
||||
const breadcrumb = g.enter().append('g');
|
||||
|
||||
breadcrumb
|
||||
.append('polygon').classed('breadcrumbs-shape', true)
|
||||
.append('polygon')
|
||||
.classed('breadcrumbs-shape', true)
|
||||
.attr('points', breadcrumbPoints)
|
||||
.attr('fill', colorMap);
|
||||
|
||||
breadcrumb
|
||||
.append('text').classed('breadcrumbs-text', true)
|
||||
.append('text')
|
||||
.classed('breadcrumbs-text', true)
|
||||
.attr('x', (b.w + b.t) / 2)
|
||||
.attr('y', b.h / 2)
|
||||
.attr('dy', '0.35em')
|
||||
.attr('font-size', '10px')
|
||||
.attr('text-anchor', 'middle')
|
||||
.text(d =>
|
||||
d.name
|
||||
);
|
||||
.text(d => d.name);
|
||||
|
||||
// Set position for entering and updating nodes.
|
||||
g.attr('transform', (d, i) =>
|
||||
`translate(${i * (b.w + b.s)}, 0)`
|
||||
);
|
||||
g.attr('transform', (d, i) => `translate(${i * (b.w + b.s)}, 0)`);
|
||||
|
||||
// Remove exiting nodes.
|
||||
g.exit().remove();
|
||||
@@ -210,32 +201,27 @@ export default function Sunburst(scope, element) {
|
||||
updateBreadcrumbs(ancestors, percentageString);
|
||||
|
||||
// update sunburst (Fade all the segments and highlight only ancestors of current segment)
|
||||
sunburst.selectAll('path')
|
||||
.attr('opacity', 0.3);
|
||||
sunburst.selectAll('path')
|
||||
.filter(node =>
|
||||
(ancestors.indexOf(node) >= 0)
|
||||
)
|
||||
sunburst.selectAll('path').attr('opacity', 0.3);
|
||||
sunburst
|
||||
.selectAll('path')
|
||||
.filter(node => ancestors.indexOf(node) >= 0)
|
||||
.attr('opacity', 1);
|
||||
|
||||
// update summary
|
||||
summary.html(
|
||||
`Stage: ${d.depth}<br />` +
|
||||
`<span class='percentage' style='font-size: 2em;'>${percentageString}</span><br />${
|
||||
d.value} of ${totalSize}<br />`
|
||||
);
|
||||
summary.html(`Stage: ${d.depth}<br />` +
|
||||
`<span class='percentage' style='font-size: 2em;'>${percentageString}</span><br />${d.value} of ${totalSize}<br />`);
|
||||
|
||||
// display summary and breadcrumbs if hidden
|
||||
summary.style('visibility', '');
|
||||
breadcrumbs.style('visibility', '');
|
||||
}
|
||||
|
||||
|
||||
// helper function click to handle mouseleave events/animations
|
||||
function click() {
|
||||
// Deactivate all segments then retransition each segment to full opacity.
|
||||
sunburst.selectAll('path').on('mouseover', null);
|
||||
sunburst.selectAll('path')
|
||||
sunburst
|
||||
.selectAll('path')
|
||||
.transition()
|
||||
.duration(1000)
|
||||
.attr('opacity', 1)
|
||||
@@ -251,10 +237,8 @@ export default function Sunburst(scope, element) {
|
||||
// helper function to draw the sunburst and breadcrumbs
|
||||
function drawSunburst(json) {
|
||||
// Build only nodes of a threshold "visible" sizes to improve efficiency
|
||||
const nodes = partition.nodes(json)
|
||||
.filter(d =>
|
||||
(d.dx > 0.005) && d.name !== exitNode // 0.005 radians = 0.29 degrees
|
||||
);
|
||||
// 0.005 radians = 0.29 degrees
|
||||
const nodes = partition.nodes(json).filter(d => d.dx > 0.005 && d.name !== exitNode);
|
||||
|
||||
// this section is required to update the colors.domain() every time the data updates
|
||||
const uniqueNames = (function uniqueNames(a) {
|
||||
@@ -267,8 +251,11 @@ export default function Sunburst(scope, element) {
|
||||
colors.domain(uniqueNames); // update domain colors
|
||||
|
||||
// create path based on nodes
|
||||
const path = sunburst.data([json]).selectAll('path')
|
||||
.data(nodes).enter()
|
||||
const path = sunburst
|
||||
.data([json])
|
||||
.selectAll('path')
|
||||
.data(nodes)
|
||||
.enter()
|
||||
.append('path')
|
||||
.classed('nodePath', true)
|
||||
.attr('display', d => (d.depth ? null : 'none'))
|
||||
@@ -278,7 +265,6 @@ export default function Sunburst(scope, element) {
|
||||
.attr('stroke', 'white')
|
||||
.on('mouseover', mouseover);
|
||||
|
||||
|
||||
// // trigger mouse click over sunburst to reset visualization summary
|
||||
vis.on('click', click);
|
||||
|
||||
@@ -299,7 +285,12 @@ export default function Sunburst(scope, element) {
|
||||
function buildNodes(raw) {
|
||||
let values;
|
||||
|
||||
if (_.has(raw[0], 'sequence') && _.has(raw[0], 'stage') && _.has(raw[0], 'node') && _.has(raw[0], 'value')) {
|
||||
if (
|
||||
_.has(raw[0], 'sequence') &&
|
||||
_.has(raw[0], 'stage') &&
|
||||
_.has(raw[0], 'node') &&
|
||||
_.has(raw[0], 'value')
|
||||
) {
|
||||
const grouped = _.groupBy(raw, 'sequence');
|
||||
|
||||
values = _.map(grouped, (value) => {
|
||||
@@ -314,13 +305,11 @@ export default function Sunburst(scope, element) {
|
||||
const validKey = key => key !== 'value' && key.indexOf('$$') !== 0;
|
||||
const keys = _.sortBy(_.filter(_.keys(raw[0]), validKey), _.identity);
|
||||
|
||||
values = _.map(raw, (row, sequence) =>
|
||||
({
|
||||
size: row.value,
|
||||
sequence,
|
||||
nodes: _.compact(_.map(keys, key => row[key])),
|
||||
})
|
||||
);
|
||||
values = _.map(raw, (row, sequence) => ({
|
||||
size: row.value,
|
||||
sequence,
|
||||
nodes: _.compact(_.map(keys, key => row[key])),
|
||||
}));
|
||||
}
|
||||
|
||||
return values;
|
||||
@@ -346,7 +335,6 @@ export default function Sunburst(scope, element) {
|
||||
const nodeName = nodes[j];
|
||||
const isLeaf = j + 1 === nodes.length;
|
||||
|
||||
|
||||
if (!children) {
|
||||
currentNode.children = children = [];
|
||||
children.push({
|
||||
@@ -403,6 +391,10 @@ export default function Sunburst(scope, element) {
|
||||
}
|
||||
|
||||
Sunburst.prototype.remove = function remove() {
|
||||
this.watches.forEach((unregister) => { unregister(); });
|
||||
this.watches.forEach((unregister) => {
|
||||
unregister();
|
||||
});
|
||||
angular.element(this.element[0]).empty('.vis-container');
|
||||
};
|
||||
|
||||
export default Sunburst;
|
||||
@@ -1,10 +0,0 @@
|
||||
import registerStatusPage from './status';
|
||||
import registerOutdatedQueriesPage from './outdated-queries';
|
||||
import registerTasksPage from './tasks';
|
||||
|
||||
export default function (ngModule) {
|
||||
const routes = Object.assign({}, registerStatusPage(ngModule),
|
||||
registerOutdatedQueriesPage(ngModule),
|
||||
registerTasksPage(ngModule));
|
||||
return routes;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { Paginator } from '../../../utils';
|
||||
import { Paginator } from '@/lib/pagination';
|
||||
import template from './outdated-queries.html';
|
||||
|
||||
function OutdatedQueriesCtrl($scope, Events, $http, $timeout) {
|
||||
@@ -30,7 +30,7 @@ function OutdatedQueriesCtrl($scope, Events, $http, $timeout) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('outdatedQueriesPage', {
|
||||
template,
|
||||
controller: OutdatedQueriesCtrl,
|
||||
|
||||
@@ -25,7 +25,7 @@ function AdminStatusCtrl($scope, $http, $timeout, currentUser, Events) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('statusPage', {
|
||||
template,
|
||||
controller: AdminStatusCtrl,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import { Paginator } from '../../../utils';
|
||||
import { Paginator } from '@/lib/pagination';
|
||||
import template from './tasks.html';
|
||||
import registerCancelQueryButton from './cancel-query-button';
|
||||
|
||||
function TasksCtrl($scope, $location, $http, $timeout, Events) {
|
||||
Events.record('view', 'page', 'admin/tasks');
|
||||
@@ -46,14 +45,12 @@ function TasksCtrl($scope, $location, $http, $timeout, Events) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('tasksPage', {
|
||||
template,
|
||||
controller: TasksCtrl,
|
||||
});
|
||||
|
||||
registerCancelQueryButton(ngModule);
|
||||
|
||||
return {
|
||||
'/admin/queries/tasks': {
|
||||
template: '<tasks-page></tasks-page>',
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<td>{{row.started_at | toMilliseconds | dateTime }}</td>
|
||||
<td>{{row.updated_at | toMilliseconds | dateTime }}</td>
|
||||
<td ng-if="selectedTab === 'in_progress'">
|
||||
<cancel-query-button query-id="dataRow.query_id" task-id="dataRow.task_id"></cancel-query-button>
|
||||
<cancel-query-button query-id="row.query_id" task-id="row.task_id"></cancel-query-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { contains, without, compact } from 'underscore';
|
||||
import template from './alert-subscriptions.html';
|
||||
|
||||
function controller($scope, $q, $sce, currentUser, AlertSubscription, Destination, toastr) {
|
||||
'ngInject';
|
||||
|
||||
$scope.newSubscription = {};
|
||||
$scope.subscribers = [];
|
||||
$scope.destinations = [];
|
||||
$scope.currentUser = currentUser;
|
||||
|
||||
$q.all([Destination.query().$promise,
|
||||
AlertSubscription.query({ alertId: $scope.alertId }).$promise]).then((responses) => {
|
||||
const destinations = responses[0];
|
||||
const subscribers = responses[1];
|
||||
|
||||
const subscribedDestinations =
|
||||
compact(subscribers.map(s => s.destination && s.destination.id));
|
||||
|
||||
const subscribedUsers =
|
||||
compact(subscribers.map(s => !s.destination && s.user.id));
|
||||
|
||||
$scope.destinations = destinations.filter(d => !contains(subscribedDestinations, d.id));
|
||||
|
||||
if (!contains(subscribedUsers, currentUser.id)) {
|
||||
$scope.destinations.unshift({ user: { name: currentUser.name } });
|
||||
}
|
||||
|
||||
$scope.newSubscription.destination = $scope.destinations[0];
|
||||
$scope.subscribers = subscribers;
|
||||
});
|
||||
|
||||
$scope.destinationsDisplay = (d) => {
|
||||
if (!d) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let destination = d;
|
||||
if (d.destination) {
|
||||
destination = destination.destination;
|
||||
} else if (destination.user) {
|
||||
destination = {
|
||||
name: `${d.user.name} (Email)`,
|
||||
icon: 'fa-envelope',
|
||||
type: 'user',
|
||||
};
|
||||
}
|
||||
|
||||
return $sce.trustAsHtml(`<i class="fa ${destination.icon}"></i> ${destination.name}`);
|
||||
};
|
||||
|
||||
$scope.saveSubscriber = () => {
|
||||
const sub = new AlertSubscription({ alert_id: $scope.alertId });
|
||||
if ($scope.newSubscription.destination.id) {
|
||||
sub.destination_id = $scope.newSubscription.destination.id;
|
||||
}
|
||||
|
||||
sub.$save(() => {
|
||||
toastr.success('Subscribed.');
|
||||
$scope.subscribers.push(sub);
|
||||
$scope.destinations = without($scope.destinations, $scope.newSubscription.destination);
|
||||
if ($scope.destinations.length > 0) {
|
||||
$scope.newSubscription.destination = $scope.destinations[0];
|
||||
} else {
|
||||
$scope.newSubscription.destination = undefined;
|
||||
}
|
||||
}, () => {
|
||||
toastr.error('Failed saving subscription.');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.unsubscribe = (subscriber) => {
|
||||
const destination = subscriber.destination;
|
||||
const user = subscriber.user;
|
||||
|
||||
subscriber.$delete(() => {
|
||||
toastr.success('Unsubscribed');
|
||||
$scope.subscribers = without($scope.subscribers, subscriber);
|
||||
if (destination) {
|
||||
$scope.destinations.push(destination);
|
||||
} else if (user.id === currentUser.id) {
|
||||
$scope.destinations.push({ user: { name: currentUser.name } });
|
||||
}
|
||||
|
||||
if ($scope.destinations.length === 1) {
|
||||
$scope.newSubscription.destination = $scope.destinations[0];
|
||||
}
|
||||
}, () => {
|
||||
toastr.error('Failed unsubscribing.');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default () => ({
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
alertId: '=',
|
||||
},
|
||||
template,
|
||||
controller,
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { template as templateBuilder } from 'underscore';
|
||||
import template from './alert.html';
|
||||
import alertSubscriptions from './alert-subscriptions';
|
||||
|
||||
function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Events, Alert) {
|
||||
this.alertId = $routeParams.alertId;
|
||||
@@ -60,34 +59,38 @@ function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Ev
|
||||
if (this.alert.rearm === '' || this.alert.rearm === 0) {
|
||||
this.alert.rearm = null;
|
||||
}
|
||||
this.alert.$save((alert) => {
|
||||
toastr.success('Saved.');
|
||||
if (this.alertId === 'new') {
|
||||
$location.path(`/alerts/${alert.id}`).replace();
|
||||
}
|
||||
}, () => {
|
||||
toastr.error('Failed saving alert.');
|
||||
});
|
||||
this.alert.$save(
|
||||
(alert) => {
|
||||
toastr.success('Saved.');
|
||||
if (this.alertId === 'new') {
|
||||
$location.path(`/alerts/${alert.id}`).replace();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
toastr.error('Failed saving alert.');
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
this.delete = () => {
|
||||
this.alert.$delete(() => {
|
||||
$location.path('/alerts');
|
||||
toastr.success('Alert deleted.');
|
||||
}, () => {
|
||||
toastr.error('Failed deleting alert.');
|
||||
});
|
||||
this.alert.$delete(
|
||||
() => {
|
||||
$location.path('/alerts');
|
||||
toastr.success('Alert deleted.');
|
||||
},
|
||||
() => {
|
||||
toastr.error('Failed deleting alert.');
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('alertPage', {
|
||||
template,
|
||||
controller: AlertCtrl,
|
||||
});
|
||||
|
||||
ngModule.directive('alertSubscriptions', alertSubscriptions);
|
||||
|
||||
return {
|
||||
'/alerts/:alertId': {
|
||||
template: '<alert-page></alert-page>',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Paginator } from '../../utils';
|
||||
import { Paginator } from '@/lib/pagination';
|
||||
import template from './alerts-list.html';
|
||||
|
||||
const stateClass = {
|
||||
@@ -26,7 +26,7 @@ class AlertsListCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('alertsListPage', {
|
||||
template,
|
||||
controller: AlertsListCtrl,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<page-header title="Dashboards"></page-header>
|
||||
<div class="col-lg-3">
|
||||
<input type='text' class='form-control' placeholder="Search Dashboards..."
|
||||
ng-change="$ctrl.update()" ng-model="$ctrl.searchText"/>
|
||||
ng-change="$ctrl.update()" ng-model="$ctrl.searchText" autofocus/>
|
||||
<div class='list-group m-t-20 tags-list'>
|
||||
<a ng-repeat='tag in $ctrl.allTags' ng-class='{"active": $ctrl.tagIsSelected(tag)}'
|
||||
class='list-group-item' ng-click='$ctrl.toggleTag($event, tag)'>
|
||||
@@ -34,4 +34,4 @@
|
||||
<paginator paginator="$ctrl.paginator"></paginator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'underscore';
|
||||
|
||||
import { Paginator } from '../../utils';
|
||||
import { Paginator } from '@/lib/pagination';
|
||||
import template from './dashboard-list.html';
|
||||
import './dashboard-list.css';
|
||||
|
||||
@@ -75,7 +75,7 @@ function DashboardListCtrl(Dashboard, $location, clientConfig) {
|
||||
this.update();
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('pageDashboardList', {
|
||||
template,
|
||||
controller: DashboardListCtrl,
|
||||
|
||||
@@ -2,8 +2,10 @@ import * as _ from 'underscore';
|
||||
import template from './dashboard.html';
|
||||
import shareDashboardTemplate from './share-dashboard.html';
|
||||
|
||||
function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibModal,
|
||||
Title, AlertDialog, Dashboard, currentUser, clientConfig, Events) {
|
||||
function DashboardCtrl(
|
||||
$rootScope, $routeParams, $location, $timeout, $q, $uibModal,
|
||||
Title, AlertDialog, Dashboard, currentUser, clientConfig, Events,
|
||||
) {
|
||||
this.isFullscreen = false;
|
||||
this.refreshRate = null;
|
||||
this.showPermissionsControl = clientConfig.showPermissionsControl;
|
||||
@@ -42,8 +44,7 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
|
||||
globalParams[param.name].locals.push(param);
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}));
|
||||
this.globalParameters = _.values(globalParams);
|
||||
};
|
||||
|
||||
@@ -60,16 +61,15 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
|
||||
const promises = [];
|
||||
|
||||
this.dashboard.widgets.forEach(row =>
|
||||
row.forEach((widget) => {
|
||||
if (widget.visualization) {
|
||||
const maxAge = force ? 0 : undefined;
|
||||
const queryResult = widget.getQuery().getQueryResult(maxAge);
|
||||
if (!_.isUndefined(queryResult)) {
|
||||
promises.push(queryResult.toPromise());
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
row.forEach((widget) => {
|
||||
if (widget.visualization) {
|
||||
const maxAge = force ? 0 : undefined;
|
||||
const queryResult = widget.getQuery().getQueryResult(maxAge);
|
||||
if (!_.isUndefined(queryResult)) {
|
||||
promises.push(queryResult.toPromise());
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.extractGlobalParameters();
|
||||
|
||||
@@ -115,10 +115,10 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
|
||||
Events.record('view', 'dashboard', dashboard.id);
|
||||
renderDashboard(dashboard, force);
|
||||
}, () => {
|
||||
// error...
|
||||
// try again. we wrap loadDashboard with throttle so it doesn't happen too often.
|
||||
// we might want to consider exponential backoff and also move this as a general
|
||||
// solution in $http/$resource for all AJAX calls.
|
||||
// error...
|
||||
// try again. we wrap loadDashboard with throttle so it doesn't happen too often.
|
||||
// we might want to consider exponential backoff and also move this as a general
|
||||
// solution in $http/$resource for all AJAX calls.
|
||||
this.loadDashboard();
|
||||
});
|
||||
}, 1000);
|
||||
@@ -128,8 +128,7 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
|
||||
this.autoRefresh = () => {
|
||||
$timeout(() => {
|
||||
this.loadDashboard(true);
|
||||
}, this.refreshRate.rate * 1000
|
||||
).then(() => this.autoRefresh());
|
||||
}, this.refreshRate.rate * 1000).then(() => this.autoRefresh());
|
||||
};
|
||||
|
||||
this.archiveDashboard = () => {
|
||||
@@ -260,7 +259,7 @@ const ShareDashboardComponent = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('shareDashboard', ShareDashboardComponent);
|
||||
ngModule.component('dashboardPage', {
|
||||
template,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import dashboardPage from './dashboard';
|
||||
import dashboardList from './dashboard-list';
|
||||
import widgetComponent from './widget';
|
||||
import addWidgetDialog from './add-widget-dialog';
|
||||
import registerEditDashboardDialog from './edit-dashboard-dialog';
|
||||
import publicDashboardPage from './public-dashboard-page';
|
||||
|
||||
export default function (ngModule) {
|
||||
addWidgetDialog(ngModule);
|
||||
widgetComponent(ngModule);
|
||||
publicDashboardPage(ngModule);
|
||||
registerEditDashboardDialog(ngModule);
|
||||
return Object.assign({}, dashboardPage(ngModule), dashboardList(ngModule));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import logoUrl from '@/assets/images/redash_icon_small.png';
|
||||
import template from './public-dashboard-page.html';
|
||||
import logoUrl from '../../assets/images/redash_icon_small.png';
|
||||
|
||||
const PublicDashboardPage = {
|
||||
template,
|
||||
@@ -17,23 +17,18 @@ const PublicDashboardPage = {
|
||||
}
|
||||
this.public = true;
|
||||
this.dashboard.widgets = this.dashboard.widgets.map(row =>
|
||||
row.map(widget =>
|
||||
new Widget(widget)
|
||||
)
|
||||
);
|
||||
row.map(widget => new Widget(widget)));
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('publicDashboardPage', PublicDashboardPage);
|
||||
|
||||
function loadPublicDashboard($http, $route) {
|
||||
'ngInject';
|
||||
|
||||
const token = $route.current.params.token;
|
||||
return $http.get(`api/dashboards/public/${token}`).then(response =>
|
||||
response.data
|
||||
);
|
||||
return $http.get(`api/dashboards/public/${token}`).then(response => response.data);
|
||||
}
|
||||
|
||||
function session($http, $route, Auth) {
|
||||
@@ -52,4 +47,6 @@ export default function (ngModule) {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import registerList from './list';
|
||||
import registerShow from './show';
|
||||
|
||||
export default function (ngModule) {
|
||||
return Object.assign({}, registerList(ngModule), registerShow(ngModule));
|
||||
}
|
||||
@@ -6,7 +6,7 @@ function DataSourcesCtrl($scope, $location, currentUser, Events, DataSource) {
|
||||
$scope.dataSources = DataSource.query();
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.controller('DataSourcesCtrl', DataSourcesCtrl);
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,8 +3,10 @@ import template from './show.html';
|
||||
|
||||
const logger = debug('redash:http');
|
||||
|
||||
function DataSourceCtrl($scope, $routeParams, $http, $location, toastr,
|
||||
currentUser, Events, DataSource) {
|
||||
function DataSourceCtrl(
|
||||
$scope, $routeParams, $http, $location, toastr,
|
||||
currentUser, Events, DataSource,
|
||||
) {
|
||||
Events.record('view', 'page', 'admin/data_source');
|
||||
|
||||
$scope.dataSourceId = $routeParams.dataSourceId;
|
||||
@@ -52,11 +54,13 @@ function DataSourceCtrl($scope, $routeParams, $http, $location, toastr,
|
||||
|
||||
$scope.actions = [
|
||||
{ name: 'Delete', class: 'btn-danger', callback: deleteDataSource },
|
||||
{ name: 'Test Connection', class: 'btn-default', callback: testConnection, disableWhenDirty: true },
|
||||
{
|
||||
name: 'Test Connection', class: 'btn-default', callback: testConnection, disableWhenDirty: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.controller('DataSourceCtrl', DataSourceCtrl);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import registerList from './list';
|
||||
import registerShow from './show';
|
||||
|
||||
export default function (ngModule) {
|
||||
return Object.assign({}, registerList(ngModule), registerShow(ngModule));
|
||||
}
|
||||
@@ -6,7 +6,7 @@ function DestinationsCtrl($scope, $location, toastr, currentUser, Events, Destin
|
||||
$scope.destinations = Destination.query();
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.controller('DestinationsCtrl', DestinationsCtrl);
|
||||
|
||||
return {
|
||||
|
||||
@@ -4,8 +4,10 @@ import template from './show.html';
|
||||
|
||||
const logger = debug('redash:http');
|
||||
|
||||
function DestinationCtrl($scope, $routeParams, $http, $location, toastr,
|
||||
currentUser, Events, Destination) {
|
||||
function DestinationCtrl(
|
||||
$scope, $routeParams, $http, $location, toastr,
|
||||
currentUser, Events, Destination,
|
||||
) {
|
||||
Events.record('view', 'page', 'admin/destination');
|
||||
|
||||
$scope.destinationId = $routeParams.destinationId;
|
||||
@@ -35,7 +37,7 @@ function DestinationCtrl($scope, $routeParams, $http, $location, toastr,
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.controller('DestinationCtrl', DestinationCtrl);
|
||||
|
||||
return {
|
||||
|
||||
@@ -43,7 +43,7 @@ function GroupDataSourcesCtrl($scope, $routeParams, $http, Events, Group, DataSo
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.controller('GroupDataSourcesCtrl', GroupDataSourcesCtrl);
|
||||
|
||||
return {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user