mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Compare commits
153 Commits
param-feed
...
choropleth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
435787281a | ||
|
|
bc9dd814c9 | ||
|
|
c7b13459e8 | ||
|
|
813d97a62c | ||
|
|
ddb0ef15c1 | ||
|
|
9646156965 | ||
|
|
5c7cb1af3d | ||
|
|
2bd8771188 | ||
|
|
86f8f32ab4 | ||
|
|
fdccaabbe9 | ||
|
|
2f5920d5e4 | ||
|
|
e97510b2ee | ||
|
|
80cfa3c557 | ||
|
|
9b71b569e2 | ||
|
|
f2159472da | ||
|
|
c961c33e49 | ||
|
|
5afc94c562 | ||
|
|
cee1a07320 | ||
|
|
42b1eadeb2 | ||
|
|
7edac9ca89 | ||
|
|
69893f0304 | ||
|
|
b089f5f0ef | ||
|
|
7a34a76817 | ||
|
|
b331c4c922 | ||
|
|
6187448e6a | ||
|
|
2de3895986 | ||
|
|
3f280b1f6e | ||
|
|
3b29f0c0a7 | ||
|
|
4911764663 | ||
|
|
6260601213 | ||
|
|
8f7d1d8281 | ||
|
|
713fd2d0fb | ||
|
|
19c6d331b6 | ||
|
|
a36b10173c | ||
|
|
7d11fae9ea | ||
|
|
35e41385dc | ||
|
|
cdefa847c0 | ||
|
|
a90b8c7443 | ||
|
|
1ba3a23457 | ||
|
|
8a5e0ea3f4 | ||
|
|
cbc56264ea | ||
|
|
c92bb63f8b | ||
|
|
ff0dbd5f01 | ||
|
|
80bfd405fd | ||
|
|
945f53fea3 | ||
|
|
56b51be64a | ||
|
|
a682265e13 | ||
|
|
a891160b4d | ||
|
|
086798bbb7 | ||
|
|
ebcec85c0c | ||
|
|
2b5bfad054 | ||
|
|
479b277b91 | ||
|
|
94ac11c787 | ||
|
|
a7ef3ad72a | ||
|
|
afe8c95f4d | ||
|
|
fe06f7f63e | ||
|
|
5e01211852 | ||
|
|
375ffd3250 | ||
|
|
674f057c59 | ||
|
|
aa17681af2 | ||
|
|
13c3531956 | ||
|
|
350716c525 | ||
|
|
fe11b8cc35 | ||
|
|
465dbc03b7 | ||
|
|
76f0dcb085 | ||
|
|
99c276fc9a | ||
|
|
fc9e8fe2aa | ||
|
|
260bfca767 | ||
|
|
f85490cf50 | ||
|
|
29582e3212 | ||
|
|
329e85987c | ||
|
|
ff34dedf46 | ||
|
|
d0fb377ed6 | ||
|
|
30bc1e2ff6 | ||
|
|
fd46194580 | ||
|
|
f5900a1929 | ||
|
|
c2b39db03e | ||
|
|
f420e02cee | ||
|
|
0aa176e2e5 | ||
|
|
97d523e348 | ||
|
|
88d21e9461 | ||
|
|
40c1ef0f59 | ||
|
|
10ba2ddbaa | ||
|
|
e7eedd0556 | ||
|
|
c3299ff0ad | ||
|
|
6b2f23f357 | ||
|
|
0819f80e72 | ||
|
|
7223f60ddf | ||
|
|
38b6b47594 | ||
|
|
49dcb7f689 | ||
|
|
425e79fdd2 | ||
|
|
bc52b78889 | ||
|
|
944adb95ba | ||
|
|
8cb49158bf | ||
|
|
ca098172e9 | ||
|
|
a3beac0b78 | ||
|
|
56d3be2248 | ||
|
|
81b14a58ef | ||
|
|
2dff8b9a00 | ||
|
|
37a964c8d9 | ||
|
|
1b9b3032ca | ||
|
|
0385b6fb64 | ||
|
|
7c05a730dc | ||
|
|
263305214e | ||
|
|
3494e21cf4 | ||
|
|
15e8b88996 | ||
|
|
ba36b4e671 | ||
|
|
94bd03dc42 | ||
|
|
041d05d18b | ||
|
|
c14e7ab4ca | ||
|
|
4d6c30ef13 | ||
|
|
36ab8eae89 | ||
|
|
e82373ac1d | ||
|
|
d3feba69b2 | ||
|
|
80f3ec1c99 | ||
|
|
c612bba19c | ||
|
|
f5a40827aa | ||
|
|
5de291a98d | ||
|
|
c70a48db9c | ||
|
|
be56035bd6 | ||
|
|
7c97d8eafa | ||
|
|
0563ecf648 | ||
|
|
e72d7a8cca | ||
|
|
a7a933946b | ||
|
|
7cfd362a7a | ||
|
|
4d1b359713 | ||
|
|
818649bbec | ||
|
|
c6a2725f0a | ||
|
|
5cd6913e40 | ||
|
|
0aebb37317 | ||
|
|
aa06b32e17 | ||
|
|
b44fa51829 | ||
|
|
1a95904ffd | ||
|
|
ef56e4e920 | ||
|
|
d5a3f0de57 | ||
|
|
cf274d96c8 | ||
|
|
c00410768c | ||
|
|
dda5a9d58f | ||
|
|
a0a32be3dd | ||
|
|
e0e94d79ac | ||
|
|
f19d24287e | ||
|
|
80878abf7b | ||
|
|
6716bb390c | ||
|
|
a33d11de3a | ||
|
|
6f791a092b | ||
|
|
cce6546a62 | ||
|
|
5fd78fdb23 | ||
|
|
74dbb8acf3 | ||
|
|
36638be1dd | ||
|
|
82f488d231 | ||
|
|
7b3943052e | ||
|
|
96a95b7090 | ||
|
|
accf0f7ac5 |
@@ -2,7 +2,7 @@ version: '3'
|
||||
services:
|
||||
server:
|
||||
build: ../
|
||||
command: dev_server
|
||||
command: server
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
@@ -31,26 +31,12 @@ services:
|
||||
REDASH_LOG_LEVEL: "INFO"
|
||||
REDASH_REDIS_URL: "redis://redis:6379/0"
|
||||
REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
|
||||
QUEUES: "default periodic schemas"
|
||||
celery_worker:
|
||||
build: ../
|
||||
command: celery_worker
|
||||
depends_on:
|
||||
- server
|
||||
environment:
|
||||
PYTHONUNBUFFERED: 0
|
||||
REDASH_LOG_LEVEL: "INFO"
|
||||
REDASH_REDIS_URL: "redis://redis:6379/0"
|
||||
REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
|
||||
QUEUES: "queries,scheduled_queries"
|
||||
WORKERS_COUNT: 2
|
||||
cypress:
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: .circleci/Dockerfile.cypress
|
||||
depends_on:
|
||||
- server
|
||||
- celery_worker
|
||||
- worker
|
||||
- scheduler
|
||||
environment:
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
version: "2"
|
||||
checks:
|
||||
complex-logic:
|
||||
enabled: false
|
||||
file-lines:
|
||||
enabled: false
|
||||
method-complexity:
|
||||
enabled: false
|
||||
method-count:
|
||||
enabled: false
|
||||
method-lines:
|
||||
config:
|
||||
threshold: 100
|
||||
nested-control-flow:
|
||||
enabled: false
|
||||
identical-code:
|
||||
enabled: false
|
||||
similar-code:
|
||||
enabled: false
|
||||
plugins:
|
||||
pep8:
|
||||
enabled: true
|
||||
eslint:
|
||||
enabled: false
|
||||
exclude_patterns:
|
||||
- "tests/**/*.py"
|
||||
- "migrations/**/*.py"
|
||||
- "setup/**/*"
|
||||
- "bin/**/*"
|
||||
- "**/node_modules/"
|
||||
- "client/dist/"
|
||||
- "**/*.pyc"
|
||||
61
.restyled.yaml
Normal file
61
.restyled.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
enabled: true
|
||||
|
||||
auto: false
|
||||
|
||||
# Open Restyle PRs?
|
||||
pull_requests: true
|
||||
|
||||
# Leave comments on the original PR linking to the Restyle PR?
|
||||
comments: true
|
||||
|
||||
# Set commit statuses on the original PR?
|
||||
statuses:
|
||||
# Red status in the case of differences found
|
||||
differences: true
|
||||
# Green status in the case of no differences found
|
||||
no_differences: true
|
||||
# Red status if we encounter errors restyling
|
||||
error: true
|
||||
|
||||
# Request review on the Restyle PR?
|
||||
#
|
||||
# Possible values:
|
||||
#
|
||||
# author: From the author of the original PR
|
||||
# owner: From the owner of the repository
|
||||
# none: Don't
|
||||
#
|
||||
# One value will apply to both origin and forked PRs, but you can also specify
|
||||
# separate values.
|
||||
#
|
||||
# request_review:
|
||||
# origin: author
|
||||
# forked: owner
|
||||
#
|
||||
request_review: author
|
||||
|
||||
# Add labels to any created Restyle PRs
|
||||
#
|
||||
# These can be used to tell other automation to avoid our PRs.
|
||||
#
|
||||
labels: ["Skip CI"]
|
||||
|
||||
# Labels to ignore
|
||||
#
|
||||
# PRs with any of these labels will be ignored by Restyled.
|
||||
#
|
||||
# ignore_labels:
|
||||
# - restyled-ignore
|
||||
|
||||
# Restylers to run, and how
|
||||
restylers:
|
||||
- name: black
|
||||
include:
|
||||
- redash
|
||||
- tests
|
||||
- migrations/versions
|
||||
- name: prettier
|
||||
include:
|
||||
- client/app/**/*.js
|
||||
- client/app/**/*.jsx
|
||||
- client/cypress/**/*.js
|
||||
@@ -1,5 +1,9 @@
|
||||
# Change Log
|
||||
|
||||
## v8.0.0 - 2019-10-27
|
||||
|
||||
There were no changes in this release since `v8.0.0-beta.2`. This is just to mark a stable release.
|
||||
|
||||
## v8.0.0-beta.2 - 2019-09-16
|
||||
|
||||
This is an update to the previous beta release, which includes:
|
||||
|
||||
@@ -46,8 +46,8 @@ When creating a new bug report, please make sure to:
|
||||
|
||||
If you would like to suggest an enhancement or ask for a new feature:
|
||||
|
||||
- Please check [the roadmap](https://trello.com/b/b2LUHU7A/redash-roadmap) for existing Trello card for what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments.
|
||||
- If there is no existing card, open a thread in [the forum](https://discuss.redash.io/c/feature-requests) to start a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about *the problem you want to solve* rather only *your proposed solution*.
|
||||
- Please check [the forum](https://discuss.redash.io/c/feature-requests/5) for existing threads about what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments.
|
||||
- If there is no open thread, you're welcome to start one to have a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about *the problem you want to solve* rather only *your proposed solution*.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
@@ -55,9 +55,9 @@ If you would like to suggest an enhancement or ask for a new feature:
|
||||
- Include screenshots and animated GIFs in your pull request whenever possible.
|
||||
- Please add [documentation](#documentation) for new features or changes in functionality along with the code.
|
||||
- Please follow existing code style:
|
||||
- Python: we use PEP8 for Python.
|
||||
- Javascript: we use Airbnb's style guides for [JavaScript](https://github.com/airbnb/javascript#naming-conventions) and [React](https://github.com/airbnb/javascript/blob/master/react) (currently we don't follow Airbnb's convention for naming files, but we're gradually fixing this). To make it automatic and easy, we recommend using [Prettier](https://github.com/prettier/prettier).
|
||||
|
||||
- Python: we use [Black](https://github.com/psf/black) to auto format the code.
|
||||
- Javascript: we use [Prettier](https://github.com/prettier/prettier) to auto-format the code.
|
||||
|
||||
### Documentation
|
||||
|
||||
The project's documentation can be found at [https://redash.io/help/](https://redash.io/help/). The [documentation sources](https://github.com/getredash/website/tree/master/src/pages/kb) are hosted on GitHub. To contribute edits / new pages, you can use GitHub's interface. Click the "Edit on GitHub" link on the documentation page to quickly open the edit interface.
|
||||
@@ -66,9 +66,9 @@ The project's documentation can be found at [https://redash.io/help/](https://re
|
||||
|
||||
### Release Method
|
||||
|
||||
We publish a stable release every ~2 months, although the goal is to get to a stable release every month. You can see the change log on [GitHub releases page](https://github.com/getredash/redash/releases).
|
||||
We publish a stable release every ~3-4 months, although the goal is to get to a stable release every month.
|
||||
|
||||
Every build of the master branch updates the latest *RC release*. These releases are usually stable, but might contain regressions and therefore recommended for "advanced users" only.
|
||||
Every build of the master branch updates the *redash/redash:preview* Docker Image. These releases are usually stable, but might contain regressions and therefore recommended for "advanced users" only.
|
||||
|
||||
When we release a new stable release, we also update the *latest* Docker image tag, the EC2 AMIs and GCE images.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:10 as frontend-builder
|
||||
FROM node:12 as frontend-builder
|
||||
|
||||
WORKDIR /frontend
|
||||
COPY package.json package-lock.json /frontend/
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2013-2019, Arik Fraimovich.
|
||||
Copyright (c) 2013-2020, Arik Fraimovich.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
celery_worker() {
|
||||
WORKERS_COUNT=${WORKERS_COUNT:-2}
|
||||
QUEUES=${QUEUES:-queries,scheduled_queries}
|
||||
WORKER_EXTRA_OPTIONS=${WORKER_EXTRA_OPTIONS:-}
|
||||
|
||||
echo "Starting $WORKERS_COUNT workers for queues: $QUEUES..."
|
||||
exec /usr/local/bin/celery worker --app=redash.worker -c$WORKERS_COUNT -Q$QUEUES -linfo --max-tasks-per-child=10 -Ofair $WORKER_EXTRA_OPTIONS
|
||||
}
|
||||
|
||||
scheduler() {
|
||||
echo "Starting RQ scheduler..."
|
||||
|
||||
@@ -25,7 +16,10 @@ dev_scheduler() {
|
||||
worker() {
|
||||
echo "Starting RQ worker..."
|
||||
|
||||
exec /app/manage.py rq worker $QUEUES
|
||||
export WORKERS_COUNT=${WORKERS_COUNT:-2}
|
||||
export QUEUES=${QUEUES:-}
|
||||
|
||||
supervisord -c worker.conf
|
||||
}
|
||||
|
||||
dev_worker() {
|
||||
@@ -34,15 +28,6 @@ dev_worker() {
|
||||
exec watchmedo auto-restart --directory=./redash/ --pattern=*.py --recursive -- ./manage.py rq worker $QUEUES
|
||||
}
|
||||
|
||||
dev_celery_worker() {
|
||||
WORKERS_COUNT=${WORKERS_COUNT:-2}
|
||||
QUEUES=${QUEUES:-queries,scheduled_queries}
|
||||
|
||||
echo "Starting $WORKERS_COUNT workers for queues: $QUEUES..."
|
||||
|
||||
exec watchmedo auto-restart --directory=./redash/ --pattern=*.py --recursive -- /usr/local/bin/celery worker --app=redash.worker -c$WORKERS_COUNT -Q$QUEUES -linfo --max-tasks-per-child=10 -Ofair
|
||||
}
|
||||
|
||||
server() {
|
||||
# Recycle gunicorn workers every n-th request. See http://docs.gunicorn.org/en/stable/settings.html#max-requests for more details.
|
||||
MAX_REQUESTS=${MAX_REQUESTS:-1000}
|
||||
@@ -54,10 +39,6 @@ create_db() {
|
||||
exec /app/manage.py database create_tables
|
||||
}
|
||||
|
||||
celery_healthcheck() {
|
||||
exec /usr/local/bin/celery inspect ping --app=redash.worker -d celery@$HOSTNAME
|
||||
}
|
||||
|
||||
rq_healthcheck() {
|
||||
exec /app/manage.py rq healthcheck
|
||||
}
|
||||
@@ -69,13 +50,10 @@ help() {
|
||||
echo ""
|
||||
|
||||
echo "server -- start Redash server (with gunicorn)"
|
||||
echo "celery_worker -- start Celery worker"
|
||||
echo "dev_celery_worker -- start Celery worker process which picks up code changes and reloads"
|
||||
echo "worker -- start a single RQ worker"
|
||||
echo "dev_worker -- start a single RQ worker with code reloading"
|
||||
echo "scheduler -- start an rq-scheduler instance"
|
||||
echo "dev_scheduler -- start an rq-scheduler instance with code reloading"
|
||||
echo "celery_healthcheck -- runs a Celery healthcheck. Useful for Docker's HEALTHCHECK mechanism."
|
||||
echo "rq_healthcheck -- runs a RQ healthcheck that verifies that all local workers are active. Useful for Docker's HEALTHCHECK mechanism."
|
||||
echo ""
|
||||
echo "shell -- open shell"
|
||||
@@ -114,14 +92,6 @@ case "$1" in
|
||||
shift
|
||||
dev_scheduler
|
||||
;;
|
||||
celery_worker)
|
||||
shift
|
||||
celery_worker
|
||||
;;
|
||||
dev_celery_worker)
|
||||
shift
|
||||
dev_celery_worker
|
||||
;;
|
||||
dev_worker)
|
||||
shift
|
||||
dev_worker
|
||||
@@ -130,10 +100,6 @@ case "$1" in
|
||||
shift
|
||||
rq_healthcheck
|
||||
;;
|
||||
celery_healthcheck)
|
||||
shift
|
||||
celery_healthcheck
|
||||
;;
|
||||
dev_server)
|
||||
export FLASK_DEBUG=1
|
||||
exec /app/manage.py runserver --debugger --reload -h 0.0.0.0
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"angularjs-annotate",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-object-assign",
|
||||
["babel-plugin-transform-builtin-extend", {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
build/*.js
|
||||
dist
|
||||
config/*.js
|
||||
client/dist
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["airbnb", "plugin:compat/recommended"],
|
||||
extends: ["react-app", "plugin:compat/recommended", "prettier"],
|
||||
plugins: ["jest", "compat", "no-only-tests"],
|
||||
settings: {
|
||||
"import/resolver": "webpack"
|
||||
},
|
||||
parser: "babel-eslint",
|
||||
env: {
|
||||
browser: true,
|
||||
node: true
|
||||
@@ -13,54 +12,6 @@ module.exports = {
|
||||
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-use-before-define": ["error", "nofunc"],
|
||||
"prefer-destructuring": "off",
|
||||
"prefer-template": "off",
|
||||
"no-restricted-properties": "off",
|
||||
"no-restricted-globals": "off",
|
||||
"no-multi-assign": "off",
|
||||
"no-lonely-if": "off",
|
||||
"consistent-return": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-multiple-empty-lines": "warn",
|
||||
"no-only-tests/no-only-tests": "error",
|
||||
"operator-linebreak": "off",
|
||||
"react/destructuring-assignment": "off",
|
||||
"react/jsx-filename-extension": "off",
|
||||
"react/jsx-one-expression-per-line": "off",
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"react/jsx-wrap-multilines": "warn",
|
||||
"react/no-access-state-in-setstate": "warn",
|
||||
"react/prefer-stateless-function": "warn",
|
||||
"react/forbid-prop-types": "warn",
|
||||
"react/prop-types": "warn",
|
||||
"jsx-a11y/anchor-is-valid": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"jsx-a11y/label-has-associated-control": [
|
||||
"warn",
|
||||
{
|
||||
controlComponents: true
|
||||
}
|
||||
],
|
||||
"jsx-a11y/label-has-for": "off",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"max-len": [
|
||||
"error",
|
||||
120,
|
||||
2,
|
||||
{
|
||||
ignoreUrls: true,
|
||||
ignoreComments: false,
|
||||
ignoreRegExpLiterals: true,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true
|
||||
}
|
||||
],
|
||||
"no-else-return": ["error", { allowElseIf: true }],
|
||||
"object-curly-newline": ["error", { consistent: true }]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@ module.exports = {
|
||||
rules: {
|
||||
"jest/no-focused-tests": "off",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { configure } from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import MockDate from 'mockdate';
|
||||
import MockDate from "mockdate";
|
||||
|
||||
const date = new Date('2000-01-01T02:00:00.000');
|
||||
const date = new Date("2000-01-01T02:00:00.000");
|
||||
|
||||
MockDate.set(date);
|
||||
|
||||
BIN
client/app/assets/images/db-logos/cloudwatch.png
Normal file
BIN
client/app/assets/images/db-logos/cloudwatch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
client/app/assets/images/db-logos/cloudwatch_insights.png
Normal file
BIN
client/app/assets/images/db-logos/cloudwatch_insights.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
client/app/assets/images/db-logos/exasol.png
Normal file
BIN
client/app/assets/images/db-logos/exasol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
client/app/assets/images/db-logos/redshift_iam.png
Normal file
BIN
client/app/assets/images/db-logos/redshift_iam.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -86,6 +86,11 @@
|
||||
// Button overrides
|
||||
.@{btn-prefix-cls} {
|
||||
transition-duration: 150ms;
|
||||
|
||||
&.icon-button {
|
||||
width: 32px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix ant input number showing duplicate arrows
|
||||
@@ -374,4 +379,24 @@
|
||||
line-height: 20px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{menu-prefix-cls} {
|
||||
// invert stripe position with class .invert-stripe-position
|
||||
&-inline.invert-stripe-position {
|
||||
.@{menu-prefix-cls}-item {
|
||||
&::after {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// overrides for checkbox
|
||||
@checkbox-prefix-cls: ~'@{ant-prefix}-checkbox';
|
||||
|
||||
.@{checkbox-prefix-cls}-wrapper + span,
|
||||
.@{checkbox-prefix-cls} + span {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
.ace_editor {
|
||||
border: 1px solid #eee;
|
||||
border: 1px solid fade(@redash-gray, 15%);
|
||||
height: 100%;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.ace_autocomplete .ace_completion-highlight {
|
||||
text-shadow: none !important;
|
||||
background: #ffff005e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.ace-tm {
|
||||
.ace_gutter {
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
.ace_gutter-active-line {
|
||||
background-color: fade(@redash-gray, 20%) !important;
|
||||
}
|
||||
|
||||
.ace_marker-layer .ace_active-line {
|
||||
background: fade(@redash-gray, 9%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
a[ng-click] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Immediately apply ng-cloak, instead of waiting for angular.js to load: */
|
||||
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -1,283 +1,273 @@
|
||||
*, button, input, i, a {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
*,
|
||||
button,
|
||||
input,
|
||||
i,
|
||||
a {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
*,
|
||||
*:active,
|
||||
*:hover {
|
||||
outline: none !important;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
|
||||
outline: none !important;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: ~"hidden\0/";
|
||||
-ms-overflow-style: auto;
|
||||
overflow-x: ~"hidden\0/";
|
||||
-ms-overflow-style: auto;
|
||||
}
|
||||
|
||||
html, body {
|
||||
min-height: 100vh;
|
||||
html,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 0;
|
||||
background: #F6F8F9;
|
||||
font-family: @redash-font;
|
||||
position: relative;
|
||||
padding-top: 0;
|
||||
background: #f6f8f9;
|
||||
font-family: @redash-font;
|
||||
position: relative;
|
||||
|
||||
app-view {
|
||||
padding-bottom: 15px;
|
||||
#application-root {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
&.headless {
|
||||
#application-root {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&.headless {
|
||||
app-view {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.app-header-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.app-header-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app-view {
|
||||
min-height: 100vh;
|
||||
#application-root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
app-view, #app-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
#application-root,
|
||||
#app-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#content {
|
||||
position: relative;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 30px;
|
||||
position: relative;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 30px;
|
||||
|
||||
@media (min-width: (@screen-sm-min + 1)) {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
@media (min-width: (@screen-sm-min + 1)) {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
@media (min-width: (@screen-lg-min + 80px)) {
|
||||
margin-left: @sidebar-left-width;
|
||||
}
|
||||
@media (min-width: (@screen-lg-min + 80px)) {
|
||||
margin-left: @sidebar-left-width;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) {
|
||||
margin-left: @sidebar-left-mid-width;
|
||||
}
|
||||
@media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) {
|
||||
margin-left: @sidebar-left-mid-width;
|
||||
}
|
||||
|
||||
@media (max-width: (@screen-sm-min)) {
|
||||
margin-left: 0;
|
||||
}
|
||||
@media (max-width: (@screen-sm-min)) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
&.c-boxed {
|
||||
max-width: @boxed-width;
|
||||
}
|
||||
&.c-boxed {
|
||||
max-width: @boxed-width;
|
||||
}
|
||||
}
|
||||
|
||||
// Fixed width layout for specific pages
|
||||
@media (min-width: 768px) {
|
||||
settings-screen, home-page, page-dashboard-list, page-queries-list, page-alerts-list, alert-page, queries-search-results-page, .fixed-container {
|
||||
.container {
|
||||
width: 750px;
|
||||
}
|
||||
.settings-screen,
|
||||
.home-page,
|
||||
.page-dashboard-list,
|
||||
.page-queries-list,
|
||||
.page-alerts-list,
|
||||
.alert-page,
|
||||
.fixed-container {
|
||||
.container {
|
||||
width: 750px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
settings-screen, home-page, page-dashboard-list, page-queries-list, page-alerts-list, alert-page, queries-search-results-page, .fixed-container {
|
||||
.container {
|
||||
width: 970px;
|
||||
}
|
||||
.settings-screen,
|
||||
.home-page,
|
||||
.page-dashboard-list,
|
||||
.page-queries-list,
|
||||
.page-alerts-list,
|
||||
.alert-page,
|
||||
.fixed-container {
|
||||
.container {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
settings-screen, home-page, page-dashboard-list, page-queries-list, page-alerts-list, alert-page, queries-search-results-page, .fixed-container {
|
||||
.container {
|
||||
width: 1170px;
|
||||
}
|
||||
.settings-screen,
|
||||
.home-page,
|
||||
.page-dashboard-list,
|
||||
.page-queries-list,
|
||||
.page-alerts-list,
|
||||
.alert-page,
|
||||
.fixed-container {
|
||||
.container {
|
||||
width: 1170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbox {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.resize-vertical {
|
||||
resize: vertical !important;
|
||||
transition: height 0s !important;
|
||||
resize: vertical !important;
|
||||
transition: height 0s !important;
|
||||
}
|
||||
.resize-horizontal {
|
||||
resize: horizontal !important;
|
||||
transition: width 0s !important;
|
||||
resize: horizontal !important;
|
||||
transition: width 0s !important;
|
||||
}
|
||||
.resize-both,
|
||||
.resize-vertical.resize-horizontal {
|
||||
resize: both !important;
|
||||
transition: height 0s, width 0s !important;
|
||||
}
|
||||
|
||||
// Ace Editor
|
||||
.ace_editor {
|
||||
border: 1px solid fade(@redash-gray, 15%) !important;
|
||||
}
|
||||
|
||||
.ace-tm {
|
||||
.ace_gutter {
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
.ace_gutter-active-line {
|
||||
background-color: fade(@redash-gray, 20%) !important;
|
||||
}
|
||||
|
||||
.ace_marker-layer .ace_active-line {
|
||||
background: fade(@redash-gray, 9%) !important;
|
||||
}
|
||||
resize: both !important;
|
||||
transition: height 0s, width 0s !important;
|
||||
}
|
||||
|
||||
.bg-ace {
|
||||
background-color: fade(@redash-gray, 12%) !important;
|
||||
background-color: fade(@redash-gray, 12%) !important;
|
||||
}
|
||||
|
||||
// resizeable
|
||||
.rg-top span, .rg-bottom span {
|
||||
height: 3px;
|
||||
border-color: #b1c1ce; // TODO: variable
|
||||
.rg-top span,
|
||||
.rg-bottom span {
|
||||
height: 3px;
|
||||
border-color: #b1c1ce; // TODO: variable
|
||||
}
|
||||
|
||||
.rg-bottom {
|
||||
bottom: 15px;
|
||||
bottom: 15px;
|
||||
|
||||
span {
|
||||
margin: 1.5px 0 0 -10px;
|
||||
}
|
||||
span {
|
||||
margin: 1.5px 0 0 -10px;
|
||||
}
|
||||
}
|
||||
|
||||
// Plotly
|
||||
text.slicetext {
|
||||
text-shadow: 1px 1px 5px #333;
|
||||
text-shadow: 1px 1px 5px #333;
|
||||
}
|
||||
|
||||
// markdown
|
||||
.markdown strong {
|
||||
font-weight: bold;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
max-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus {
|
||||
background-color: fade(@redash-gray, 15%);
|
||||
color: #111;
|
||||
.dropdown-menu > li > a:hover,
|
||||
.dropdown-menu > li > a:focus {
|
||||
background-color: fade(@redash-gray, 15%);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.profile__image--sidebar {
|
||||
border-radius: 100%;
|
||||
margin-right: 3px;
|
||||
margin-top: -2px;
|
||||
border-radius: 100%;
|
||||
margin-right: 3px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.profile__image--settings {
|
||||
border-radius: 100%;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.profile__image_thumb {
|
||||
border-radius: 100%;
|
||||
margin-right: 3px;
|
||||
margin-top: -2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 100%;
|
||||
margin-right: 3px;
|
||||
margin-top: -2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
|
||||
// Error state
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
text-align: center;
|
||||
margin-top: 25vh;
|
||||
padding: 35px;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
text-align: center;
|
||||
margin-top: 25vh;
|
||||
padding: 35px;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
|
||||
.error-state__icon {
|
||||
.zmdi {
|
||||
font-size: 64px;
|
||||
color: @redash-gray;
|
||||
}
|
||||
.error-state__icon {
|
||||
.zmdi {
|
||||
font-size: 64px;
|
||||
color: @redash-gray;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
margin-top: 10vh;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
margin-top: 10vh;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-icon-danger {
|
||||
color: @red !important;
|
||||
color: @red !important;
|
||||
}
|
||||
|
||||
// page
|
||||
.page-header--new .btn-favourite, .page-header--new .btn-archive {
|
||||
.page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.favorites-control {
|
||||
font-size: 19px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
favorites-control {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
display: block;
|
||||
|
||||
favorites-control {
|
||||
float: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
width: 100%;
|
||||
margin-bottom: 5px !important;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
.page-header-wrapper,
|
||||
.page-header--new {
|
||||
h3 {
|
||||
margin: 0.2em 0;
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header-wrapper, .page-header--new {
|
||||
h3 {
|
||||
margin: 0.2em 0;
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.select-option-divider {
|
||||
margin: 10px 0 !important;
|
||||
}
|
||||
.select-option-divider {
|
||||
margin: 10px 0 !important;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
}
|
||||
|
||||
.edit-in-place span.editable {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -23,32 +24,3 @@
|
||||
.edit-in-place {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.edit-in-place {
|
||||
.rd-form-control {
|
||||
padding: 0px 6px;
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
&.active {
|
||||
textarea.rd-form-control {
|
||||
height: 29px;
|
||||
width: 40vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.edit-in-place {
|
||||
.rd-form-control {
|
||||
width: 50vw;
|
||||
}
|
||||
|
||||
&.active {
|
||||
textarea.rd-form-control {
|
||||
width: 50vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
label {
|
||||
font-weight: 500;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea.v-resizable {
|
||||
resize: vertical;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@@ -29,285 +29,266 @@ textarea.v-resizable {
|
||||
}
|
||||
}
|
||||
|
||||
/* light version of bootstrap's form-control */
|
||||
.rd-form-control {
|
||||
display: block;
|
||||
padding: 6px 12px;
|
||||
line-height: 1.428571429;
|
||||
color: #555555;
|
||||
vertical-align: middle;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Input Fields
|
||||
-----------------------------------------------------------*/
|
||||
.form-control {
|
||||
.transition(all);
|
||||
.transition-duration(300ms);
|
||||
resize: none;
|
||||
box-shadow: 0 0 0 40px rgba(0, 0, 0, 0) !important;
|
||||
border-radius: @redash-input-radius;
|
||||
.transition(all);
|
||||
.transition-duration(300ms);
|
||||
resize: none;
|
||||
box-shadow: 0 0 0 40px rgba(0, 0, 0, 0) !important;
|
||||
border-radius: @redash-input-radius;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
border-color: @blue;
|
||||
}
|
||||
&:hover {
|
||||
border-color: @blue;
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
border-color: @blue;
|
||||
}
|
||||
&:hover {
|
||||
border-color: @blue;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Custom Checkbox + Radio
|
||||
-----------------------------------------------------------*/
|
||||
.cra-validatation(@color) {
|
||||
input[type="checkbox"], input[type="radio"] {
|
||||
& + .input-helper {
|
||||
border-color: @color;
|
||||
}
|
||||
|
||||
&:checked + .input-helper:before {
|
||||
background: @color;
|
||||
}
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
& + .input-helper {
|
||||
border-color: @color;
|
||||
}
|
||||
|
||||
&:checked + .input-helper:before {
|
||||
background: @color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cr-alt {
|
||||
position: relative;
|
||||
padding-top: 0;
|
||||
margin: 0;
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
padding-top: 0;
|
||||
padding-left: 28px;
|
||||
}
|
||||
|
||||
&.has-success {
|
||||
.cra-validatation(@green);
|
||||
}
|
||||
|
||||
&.has-warning {
|
||||
.cra-validatation(@orange);
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
.cra-validatation(@red);
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
.opacity(0);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
margin: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: pointer;
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
padding-left: 28px;
|
||||
& + .input-helper {
|
||||
border: 1px solid @input-border;
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.has-success {
|
||||
.cra-validatation(@green);
|
||||
&:checked + .input-helper:before {
|
||||
content: "";
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background: #31acff;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
& + i {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&.has-warning {
|
||||
.cra-validatation(@orange);
|
||||
|
||||
&:checked + i:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
.cra-validatation(@red);
|
||||
|
||||
}
|
||||
|
||||
input[type="checkbox"], input[type="radio"] {
|
||||
.opacity(0);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
margin: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: pointer;
|
||||
|
||||
& + .input-helper {
|
||||
border: 1px solid @input-border;
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:checked + .input-helper:before {
|
||||
content: "";
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background: #31ACFF;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
& + i {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&:checked + i:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.opacity(0.7);
|
||||
}
|
||||
&.disabled {
|
||||
.opacity(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-inline,
|
||||
.radio-inline {
|
||||
padding-left: 27px;
|
||||
padding-left: 27px;
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Input Addon
|
||||
-----------------------------------------------------------*/
|
||||
.input-group {
|
||||
.input-group-addon {
|
||||
min-width: 40px;
|
||||
color: #333;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:not([class*="input-group-"]) {
|
||||
.input-group-addon {
|
||||
min-width: 40px;
|
||||
color: #333;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:not([class*="input-group-"]) {
|
||||
.input-group-addon {
|
||||
font-size: 15px;
|
||||
}
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Toggle Switch
|
||||
-----------------------------------------------------------*/
|
||||
.ts-color(@color){
|
||||
input {
|
||||
&:not(:disabled) {
|
||||
&:checked {
|
||||
& + .ts-helper {
|
||||
background: fade(@color, 50%);
|
||||
.ts-color(@color) {
|
||||
input {
|
||||
&:not(:disabled) {
|
||||
&:checked {
|
||||
& + .ts-helper {
|
||||
background: fade(@color, 50%);
|
||||
|
||||
&:before {
|
||||
background: @color;
|
||||
}
|
||||
&:before {
|
||||
background: @color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
&:before {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.28), 0 0 0 20px fade(@color, 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
&:before {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28), 0 0 0 20px fade(@color, 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
.user-select(none);
|
||||
|
||||
.ts-label {
|
||||
display: inline-block;
|
||||
margin: 0 20px 0 0;
|
||||
vertical-align: top;
|
||||
.user-select(none);
|
||||
-webkit-transition: color 0.56s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: color 0.56s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.ts-label {
|
||||
display: inline-block;
|
||||
margin: 0 20px 0 0;
|
||||
vertical-align: top;
|
||||
-webkit-transition: color 0.56s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: color 0.56s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
.ts-helper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.26);
|
||||
-webkit-transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #fafafa;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
|
||||
border-radius: 50%;
|
||||
webkit-transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
.ts-helper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0,0,0,0.26);
|
||||
-webkit-transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #fafafa;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.28);
|
||||
border-radius: 50%;
|
||||
webkit-transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28), 0 0 0 20px rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.disabled) {
|
||||
.ts-helper {
|
||||
&:active {
|
||||
&:before {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.28), 0 0 0 20px rgba(128,128,128,0.1);
|
||||
}
|
||||
}
|
||||
input {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 46px;
|
||||
margin: 0 0 0 -4px;
|
||||
height: 24px;
|
||||
.opacity(0);
|
||||
cursor: pointer;
|
||||
|
||||
&:checked {
|
||||
& + .ts-helper {
|
||||
&:before {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 46px;
|
||||
margin: 0 0 0 -4px;
|
||||
height: 24px;
|
||||
.opacity(0);
|
||||
cursor: pointer;
|
||||
&:not([data-ts-color]) {
|
||||
.ts-color(@teal);
|
||||
}
|
||||
|
||||
&:checked {
|
||||
& + .ts-helper {
|
||||
&:before {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.disabled {
|
||||
.opacity(0.6);
|
||||
}
|
||||
|
||||
&:not([data-ts-color]){
|
||||
.ts-color(@teal);
|
||||
}
|
||||
&[data-ts-color="red"] {
|
||||
.ts-color(@red);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.opacity(0.6);
|
||||
}
|
||||
&[data-ts-color="blue"] {
|
||||
.ts-color(@blue);
|
||||
}
|
||||
|
||||
&[data-ts-color="red"] {
|
||||
.ts-color(@red);
|
||||
}
|
||||
&[data-ts-color="amber"] {
|
||||
.ts-color(@amber);
|
||||
}
|
||||
|
||||
&[data-ts-color="blue"] {
|
||||
.ts-color(@blue);
|
||||
}
|
||||
&[data-ts-color="purple"] {
|
||||
.ts-color(@purple);
|
||||
}
|
||||
|
||||
&[data-ts-color="amber"] {
|
||||
.ts-color(@amber);
|
||||
}
|
||||
&[data-ts-color="pink"] {
|
||||
.ts-color(@pink);
|
||||
}
|
||||
|
||||
&[data-ts-color="purple"] {
|
||||
.ts-color(@purple);
|
||||
}
|
||||
&[data-ts-color="lime"] {
|
||||
.ts-color(@lime);
|
||||
}
|
||||
|
||||
&[data-ts-color="pink"] {
|
||||
.ts-color(@pink);
|
||||
}
|
||||
|
||||
&[data-ts-color="lime"] {
|
||||
.ts-color(@lime);
|
||||
}
|
||||
|
||||
&[data-ts-color="cyan"] {
|
||||
.ts-color(@cyan);
|
||||
}
|
||||
|
||||
&[data-ts-color="green"] {
|
||||
.ts-color(@green);
|
||||
}
|
||||
&[data-ts-color="cyan"] {
|
||||
.ts-color(@cyan);
|
||||
}
|
||||
|
||||
&[data-ts-color="green"] {
|
||||
.ts-color(@green);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/* angular-growl */
|
||||
.growl {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
float: right;
|
||||
width: 250px;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.growl-item.ng-enter,
|
||||
.growl-item.ng-leave {
|
||||
-webkit-transition: 0.5s linear all;
|
||||
-moz-transition: 0.5s linear all;
|
||||
-o-transition: 0.5s linear all;
|
||||
transition: 0.5s linear all;
|
||||
}
|
||||
|
||||
.growl-item.ng-enter,
|
||||
.growl-item.ng-leave.ng-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.growl-item.ng-leave,
|
||||
.growl-item.ng-enter.ng-enter-active {
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
@@ -17,31 +17,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
tags-list {
|
||||
a {
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-list__name {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 88%;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
.badge-light {
|
||||
background: fade(@redash-gray, 10%);
|
||||
color: fade(@redash-gray, 75%);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.max-character {
|
||||
.text-overflow();
|
||||
}
|
||||
|
||||
@@ -234,4 +234,9 @@
|
||||
.hide-in-percy, .pace {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// hide tooltips in Percy
|
||||
.ant-tooltip {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.overlay {
|
||||
background-color: #808080;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
z-index: 1000;
|
||||
opacity: 0.8;
|
||||
}
|
||||
@@ -31,6 +31,8 @@ div.table-name {
|
||||
overflow-x: hidden;
|
||||
border: none;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.collapse.in {
|
||||
background: transparent;
|
||||
@@ -72,11 +74,12 @@ div.table-name {
|
||||
|
||||
.schema-control {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0;
|
||||
|
||||
.form-control {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.ant-btn {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.parameter-label {
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
.tab-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
margin: 0 0 10px 0;
|
||||
overflow: auto;
|
||||
box-shadow: inset 0 -2px 0 0 #eee;
|
||||
|
||||
& > li {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
& > a {
|
||||
display: inline-block;
|
||||
color: #7a7a7a;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-sm-min) {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
@media (max-width: @screen-sm-min) {
|
||||
padding: 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
& > a {
|
||||
color: #000;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tab-nav-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.tn-justified {
|
||||
& > li {
|
||||
display: table-cell;
|
||||
width: 1%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.tn-icon {
|
||||
& > li {
|
||||
.zmdi {
|
||||
font-size: 22px;
|
||||
line-height: 100%;
|
||||
min-height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not([data-tab-color]) {
|
||||
& > li > a:after {
|
||||
background: @blue;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-tab-color="green"] {
|
||||
& > li > a:after {
|
||||
background: @green;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-tab-color="red"] {
|
||||
& > li > a:after {
|
||||
background: @red;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-tab-color="teal"] {
|
||||
& > li > a:after {
|
||||
background: @teal;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-tab-color="amber"] {
|
||||
& > li > a:after {
|
||||
background: @amber;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-tab-color="black"] {
|
||||
& > li > a:after {
|
||||
background: @black;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-tab-color="cyan"] {
|
||||
& > li > a:after {
|
||||
background: @cyan;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.rd-tab {
|
||||
.remove {
|
||||
cursor: pointer;
|
||||
color: #A09797;
|
||||
padding: 0 3px 1px 4px;
|
||||
font-size: 11px;
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #FF8080;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
margin-bottom: 0px;
|
||||
|
||||
> li.rd-tab-btn {
|
||||
float: right;
|
||||
padding-right: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
> li > a {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
#toast-container .toast {
|
||||
margin: 0 6px 6px 0;
|
||||
box-shadow: none;
|
||||
color: #ffffff;
|
||||
opacity: 0.75;
|
||||
border-radius: 2px;
|
||||
transition: opacity 0.35s ease-in-out;
|
||||
}
|
||||
#toast-container .toast:hover {
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background-color: #030303;
|
||||
}
|
||||
.toast-success {
|
||||
background-color: #3BD973;
|
||||
}
|
||||
.toast-error {
|
||||
background-color: #E92828;
|
||||
}
|
||||
.toast-info {
|
||||
background-color: #356AFF;
|
||||
}
|
||||
.toast-warning {
|
||||
background-color: #FB8D3D;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
.bootgrid-table {
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.bootgrid-footer .infoBar,
|
||||
.bootgrid-header .actionBar {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bootgrid-footer .search,
|
||||
.bootgrid-header .search {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.bootgrid-header {
|
||||
margin: 0;
|
||||
padding: 25px;
|
||||
|
||||
.search {
|
||||
border: 1px solid @input-border;
|
||||
|
||||
.form-control, .input-group-addon {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
padding-right: 0 !important;
|
||||
min-width: 26px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-xs-min) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xs-min) {
|
||||
width: 100%;
|
||||
padding-right: 90px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.actions {
|
||||
box-shadow: none;
|
||||
|
||||
.btn-group {
|
||||
.btn {
|
||||
height: 37px;
|
||||
background: #fff;
|
||||
border-radius: 0;
|
||||
border: 1px solid @input-border;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@media (min-width: @screen-sm-min) {
|
||||
left: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 5px 10px;
|
||||
|
||||
.input-helper {
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.zmdi {
|
||||
line-height: 100%;
|
||||
font-size: 18px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xs-min) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bootgrid-footer {
|
||||
border-top: 1px solid @table-border-color;
|
||||
margin-top: 0;
|
||||
|
||||
.col-sm-6 {
|
||||
padding: 25px;
|
||||
|
||||
@media (max-width: @screen-sm-min) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.infoBar {
|
||||
@media (max-width: @screen-sm-min) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.infos {
|
||||
border: 1px solid #EEE;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
padding: 7px 30px;
|
||||
font-size: 12px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select-cell .checkbox {
|
||||
margin: 0px 0 0 -19px;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
.bootstrap-datetimepicker-widget {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: auto !important;
|
||||
|
||||
&:after, &:before { display: none !important; }
|
||||
|
||||
table td {
|
||||
text-shadow: none;
|
||||
|
||||
span {
|
||||
margin: 0;
|
||||
|
||||
&:hover { background: transparent; }
|
||||
}
|
||||
}
|
||||
|
||||
.glyphicon { font-family: @font-icon; font-size: 18px; }
|
||||
.glyphicon-chevron-left:before { content: "\f2ff"; }
|
||||
.glyphicon-chevron-right:before { content: "\f301"; }
|
||||
.glyphicon-time:before { content: "\f337"; }
|
||||
.glyphicon-calendar:before { content: "\f32e"; }
|
||||
.glyphicon-chevron-up:before { content: "\f1e5"; }
|
||||
.glyphicon-chevron-down:before { content: "\f1e4"; }
|
||||
|
||||
[data-action="togglePicker"] span {
|
||||
font-size: 25px;
|
||||
color: #ccc;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
a[data-action] {
|
||||
color: @blue;
|
||||
}
|
||||
}
|
||||
|
||||
.timepicker-picker {
|
||||
.btn { box-shadow: none !important; }
|
||||
|
||||
table {
|
||||
tbody tr + tr:not(:last-child) {
|
||||
background: @blue;
|
||||
color: #fff;
|
||||
|
||||
td {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker {
|
||||
&.top {
|
||||
.transform-origin(0 100%) !important;
|
||||
}
|
||||
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
border-radius: 0;
|
||||
color: #fff;
|
||||
|
||||
.glyphicon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
&:hover .glyphicon {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
th {
|
||||
background: @blue;
|
||||
padding: 20px 0;
|
||||
|
||||
&:hover {
|
||||
background: @blue;
|
||||
}
|
||||
|
||||
&.picker-switch {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
th {
|
||||
&:first-child { padding-left: 20px; }
|
||||
&:last-child { padding-right: 20px; }
|
||||
|
||||
text-transform: uppercase;
|
||||
font-weight: normal;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&:not(:only-child) {
|
||||
background: darken(@blue, 3%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
&:last-child {
|
||||
td {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
&:first-child {
|
||||
padding-left: 13px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
|
||||
&.day {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
line-height: 20px;
|
||||
color: #333;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: -33px;
|
||||
display: inline-block;
|
||||
background: transparent;
|
||||
position: static;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
&.old, &.new {
|
||||
color: #CDCDCD;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.today):not(.active) {
|
||||
&:hover:before {
|
||||
background: #F0F0F0;
|
||||
}
|
||||
}
|
||||
|
||||
&.today {
|
||||
color: #333;
|
||||
|
||||
&:before {
|
||||
background-color: #E2E2E2;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
|
||||
&:before {
|
||||
background-color: @blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker-months .month,
|
||||
.datepicker-years .year,
|
||||
.timepicker-minutes .minute,
|
||||
.timepicker-hours .hour {
|
||||
border-radius: 50%;
|
||||
|
||||
&:not(.active) {
|
||||
&:hover {
|
||||
background: #F0F0F0;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: @blue;
|
||||
}
|
||||
}
|
||||
|
||||
.timepicker-minutes .minute,
|
||||
.timepicker-hours .hour {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
.bootstrap-select {
|
||||
|
||||
.bs-searchbox {
|
||||
padding: 0 18px;
|
||||
margin: 5px 0 10px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
top: 2px;
|
||||
width: 30px;
|
||||
height: 100%;
|
||||
content: "\f1c3";
|
||||
font-family: @font-icon;
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding-left: 25px;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-group {
|
||||
.dropdown-menu li a.opt {
|
||||
padding-left: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.check-mark {
|
||||
margin-top: -5px !important;
|
||||
font-size: 19px;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
right: 15px;
|
||||
|
||||
&:before {
|
||||
content: "\f26b";
|
||||
font-family: @font-icon;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
.check-mark {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.notify {
|
||||
bottom: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 100% !important;
|
||||
border: 0 !important;
|
||||
background: @red !important;
|
||||
color: #fff !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:not([class*=col-]):not([class*=form-control]):not(.input-group-btn) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background-color: #fff;
|
||||
border-radius: 0;
|
||||
border: 1px solid @input-border;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
.chosen-container {
|
||||
.chosen-drop {
|
||||
border-color: @input-border;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.chosen-results {
|
||||
margin: 10px 0 0 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 10px 17px;
|
||||
width: 100%;
|
||||
|
||||
&.highlighted {
|
||||
background: @dropdown-link-hover-bg;
|
||||
color: @dropdown-link-hover-color;
|
||||
}
|
||||
|
||||
&.result-selected {
|
||||
background: @lightblue;
|
||||
color: @white;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: "\f26b";
|
||||
font-family: @font-icon;
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 10px;
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
&.group-result {
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
color: #B2B2B2;
|
||||
font-weight: normal;
|
||||
padding: 16px 15px 6px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chosen-container-single {
|
||||
.chosen-single {
|
||||
border-radius: 0;
|
||||
height: 35px;
|
||||
padding: 7px 12px 6px;
|
||||
line-height: 1.42857143;
|
||||
border-color: @input-border;
|
||||
}
|
||||
|
||||
.chosen-search {
|
||||
padding: 5px 12px;
|
||||
|
||||
&:before {
|
||||
content: "\f1c3";
|
||||
font-family: @font-icon;
|
||||
position: absolute;
|
||||
left: 25px;
|
||||
top: 9px;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
border-color: @input-border;
|
||||
padding: 8px 10px 8px 35px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chosen-container-multi {
|
||||
.chosen-choices {
|
||||
padding: 0 4px;
|
||||
border-color: @input-border;
|
||||
|
||||
li {
|
||||
&.search-choice {
|
||||
border-radius: 0;
|
||||
margin: 4px 4px 0 0;
|
||||
background: @blue;
|
||||
border-color: @blue;
|
||||
color: #fff;
|
||||
padding: 5px 23px 5px 8px;
|
||||
|
||||
.search-choice-close {
|
||||
&:before {
|
||||
display: inline-block;
|
||||
font-family: @font-icon;
|
||||
content: "\f135";
|
||||
position: relative;
|
||||
top: 1px;
|
||||
color: #fff;
|
||||
z-index: 2;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.search-field {
|
||||
input[type=text] {
|
||||
padding: 0 8px;
|
||||
height: 31px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
.cp-container {
|
||||
position: relative;
|
||||
|
||||
& > .input-group {
|
||||
|
||||
input.cp-value {
|
||||
color: #000 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
padding: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
i.cp-value {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
.fileinput {
|
||||
position: relative;
|
||||
padding-right: 35px;
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
font-size: 12px;
|
||||
float: none;
|
||||
opacity: 1;
|
||||
font-weight: 500;
|
||||
border: 1px solid #ccc;
|
||||
width: 19px;
|
||||
text-align: center;
|
||||
height: 19px;
|
||||
line-height: 15px;
|
||||
border-radius: 50%;
|
||||
right: 0;
|
||||
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
padding: 0 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.fileinput-preview {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-top: -13px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
/** CALENDAR WIDGET **/
|
||||
#calendar-widget {
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 1px 1px rgba(0,0,0,.15);
|
||||
}
|
||||
|
||||
#fc-actions {
|
||||
position: absolute;
|
||||
bottom: 23px;
|
||||
right: 22px;
|
||||
|
||||
& > li > a {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
line-height: 30px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
& > li.open > a,
|
||||
& > li > a:hover {
|
||||
background: darken(@teal, 7%);
|
||||
}
|
||||
}
|
||||
|
||||
.fc {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20px;
|
||||
|
||||
td {
|
||||
border-color: @table-border-color !important;
|
||||
}
|
||||
|
||||
th {
|
||||
background: darken(@teal, 7%);
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
table tr {
|
||||
& > td:first-child {
|
||||
border-left-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-widget-header {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.fc-day-number {
|
||||
color: #CCC;
|
||||
}
|
||||
|
||||
.fc-event-container {
|
||||
padding: 0 2px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-toolbar {
|
||||
background: @teal;
|
||||
margin-bottom: 0;
|
||||
padding: 25px 7px 25px;
|
||||
position: relative;
|
||||
.user-select(none);
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
bottom: -30px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
background: darken(@teal, 7%);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 7px;
|
||||
font-size: 19px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.ui-button {
|
||||
border: 0;
|
||||
background: 0 0;
|
||||
padding: 0;
|
||||
outline: none !important;
|
||||
text-align: center;
|
||||
|
||||
& > span {
|
||||
position: relative;
|
||||
font-family: @font-icon;
|
||||
font-size: 20px;
|
||||
color: #FFF;
|
||||
line-height: 100%;
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
border-radius: 50%;
|
||||
padding-top: 6px;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
|
||||
&:before {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.ui-icon-circle-triangle-w:before {
|
||||
content: "\f2fa";
|
||||
}
|
||||
|
||||
&.ui-icon-circle-triangle-e:before {
|
||||
content: "\f2fb";
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: darken(@teal, 7%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fc-event {
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
|
||||
.fc-title {
|
||||
padding: 3px 5px 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fc-time {
|
||||
float: left;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 2px 6px;
|
||||
margin: 0 0 0 -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.fc-view, .fc-view > table {
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fc-content-skeleton {
|
||||
table {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
#calendar {
|
||||
.fc-day-number {
|
||||
@media screen and (min-width: @screen-sm-max) {
|
||||
font-size: 25px;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
padding-left: 10px !important;
|
||||
text-align: left !important;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Even Tag Color */
|
||||
.event-tag {
|
||||
margin-top: 5px;
|
||||
|
||||
& > span {
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 3px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.opacity(0.8);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
&:before {
|
||||
font-family: @font-icon;
|
||||
content: "\f26b";
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 3px;
|
||||
width: 100%;
|
||||
font-size: 17px;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Height Fix */
|
||||
.fc-day-grid-container {
|
||||
height: auto !important;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
.lg-outer .lg-item {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.lg-slide {
|
||||
&:after {
|
||||
content: "";
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
border-radius: 100%;
|
||||
border: 2px solid @white;
|
||||
-webkit-animation: ball-scale-ripple 1s 0s infinite cubic-bezier(0.21, 0.53, 0.56, 0.8);
|
||||
animation: ball-scale-ripple 1s 0s infinite cubic-bezier(0.21, 0.53, 0.56, 0.8);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -25px;
|
||||
top: 50%;
|
||||
margin-top: -25px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 5px;
|
||||
color: #D2D2D2;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6B6B6B;
|
||||
}
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes ball-scale-ripple {
|
||||
0% {
|
||||
-webkit-transform: scale(0.1);
|
||||
transform: scale(0.1);
|
||||
opacity: 1; }
|
||||
|
||||
70% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
opacity: 0.7; }
|
||||
|
||||
100% {
|
||||
opacity: 0.0; }
|
||||
}
|
||||
|
||||
@keyframes ball-scale-ripple {
|
||||
0% {
|
||||
-webkit-transform: scale(0.1);
|
||||
transform: scale(0.1);
|
||||
opacity: 1; }
|
||||
|
||||
70% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
opacity: 0.7; }
|
||||
|
||||
100% {
|
||||
opacity: 0.0; }
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
.mCSB_container,
|
||||
.mCustomScrollBox {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.mCSB_scrollTools {
|
||||
width: 12px;
|
||||
|
||||
.mCSB_draggerRail,
|
||||
.mCSB_dragger .mCSB_dragger_bar {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.mCSB_draggerRail {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mCS-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar {
|
||||
background: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.mCSB_inside > .mCSB_container {
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
.noUi-target {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.noUi-background {
|
||||
background: #d4d4d4;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.noUi-horizontal {
|
||||
height: 3px;
|
||||
|
||||
.noUi-handle {
|
||||
top: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.noUi-vertical {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.noUi-horizontal,
|
||||
.noUi-vertical {
|
||||
.noUi-handle {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
border: 0;
|
||||
border-radius: 100%;
|
||||
box-shadow: none;
|
||||
.transition(box-shadow);
|
||||
.transition-duration(200ms);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #ccc !important;
|
||||
}
|
||||
|
||||
.is-tooltip {
|
||||
position: absolute;
|
||||
bottom: 32px;
|
||||
height: 35px;
|
||||
border-radius: 2px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 33px;
|
||||
width: 50px;
|
||||
left: 50%;
|
||||
margin-left: -25px;
|
||||
padding: 0 10px;
|
||||
.transition(all);
|
||||
.transition-duration(200ms);
|
||||
.backface-visibility(hidden);
|
||||
.opacity(0);
|
||||
.scale(0);
|
||||
|
||||
&:after {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 15px 10px 0 10px;
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
left: 50%;
|
||||
margin-left: -9px;
|
||||
content: "";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.noUi-active {
|
||||
box-shadow: 0 0 0 13px rgba(0,0,0,0.1);
|
||||
|
||||
.is-tooltip {
|
||||
.scale(1);
|
||||
bottom: 40px;
|
||||
.opacity(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-slider,
|
||||
.input-slider-range,
|
||||
.input-slider-values {
|
||||
&:not([data-is-color]) {
|
||||
.noUi-handle,
|
||||
.noUi-connect, {
|
||||
background: @teal !important;
|
||||
}
|
||||
|
||||
.is-tooltip {
|
||||
background: @teal;
|
||||
|
||||
&:after {
|
||||
border-color: @teal transparent transparent transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-is-color=red] {
|
||||
.is-color-handle(@red);
|
||||
}
|
||||
|
||||
&[data-is-color=blue] {
|
||||
.is-color-handle(@blue);
|
||||
}
|
||||
|
||||
&[data-is-color=cyan] {
|
||||
.is-color-handle(@cyan);
|
||||
}
|
||||
|
||||
&[data-is-color=amber] {
|
||||
.is-color-handle(@amber);
|
||||
}
|
||||
|
||||
&[data-is-color=green] {
|
||||
.is-color-handle(@green);
|
||||
}
|
||||
}
|
||||
|
||||
.input-slider {
|
||||
.noUi-origin {
|
||||
background: #d4d4d4;
|
||||
}
|
||||
|
||||
&:not([data-is-color]) {
|
||||
.noUi-base {
|
||||
background: @teal !important;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-is-color=red] {
|
||||
.is-color-base(@red);
|
||||
}
|
||||
|
||||
&[data-is-color=blue] {
|
||||
.is-color-base(@blue);
|
||||
}
|
||||
|
||||
&[data-is-color=cyan] {
|
||||
.is-color-base(@cyan);
|
||||
}
|
||||
|
||||
&[data-is-color=amber] {
|
||||
.is-color-base(@amber);
|
||||
}
|
||||
|
||||
&[data-is-color=green] {
|
||||
.is-color-base(@green);
|
||||
}
|
||||
}
|
||||
|
||||
.is-color-handle(@color) {
|
||||
.noUi-handle,
|
||||
.noUi-connect {
|
||||
background: @color !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-color-base(@color) {
|
||||
.noUi-base {
|
||||
background: @color !important;
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
.note-editor,
|
||||
.note-popover {
|
||||
.note-toolbar,
|
||||
.popover-content {
|
||||
background: #fff;
|
||||
border-color: #e4e4e4;
|
||||
margin: 0;
|
||||
padding: 10px 0 15px;
|
||||
text-align: center;
|
||||
|
||||
& > .btn-group {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
box-shadow: none;
|
||||
|
||||
.btn {
|
||||
margin: 0 1px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
& > .active {
|
||||
background: @cyan;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
height: 40px;
|
||||
border-radius: 2px !important;
|
||||
box-shadow: none !important;
|
||||
background: #fff;
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.note-palette-title {
|
||||
margin: 0 !important;
|
||||
padding: 10px 0 !important;
|
||||
font-size: 13px !important;
|
||||
text-align: center !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.note-color-reset {
|
||||
padding: 0 0 10px !important;
|
||||
margin: 0 !important;
|
||||
background: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.note-color {
|
||||
.dropdown-menu {
|
||||
min-width: 335px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-statusbar {
|
||||
.note-resizebar {
|
||||
border-color: #E8E8E8;
|
||||
|
||||
.note-icon-bar {
|
||||
border-color: #BCBCBC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fa {
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
vertical-align: middle;
|
||||
|
||||
&:before {
|
||||
font-family: @font-icon;
|
||||
}
|
||||
|
||||
&.fa-magic:before {
|
||||
content: "\f16a";
|
||||
}
|
||||
|
||||
&.fa-bold:before {
|
||||
content: "\f23d";
|
||||
}
|
||||
|
||||
&.fa-italic:before {
|
||||
content: "\f245";
|
||||
}
|
||||
|
||||
&.fa-underline:before {
|
||||
content: "\f24f";
|
||||
}
|
||||
|
||||
&.fa-font:before {
|
||||
content: "\f242";
|
||||
}
|
||||
|
||||
&.fa-list-ul:before {
|
||||
content: "\f247";
|
||||
}
|
||||
|
||||
&.fa-list-ol:before {
|
||||
content: "\f248";
|
||||
}
|
||||
|
||||
&.fa-align-left:before {
|
||||
content: "\f23b";
|
||||
}
|
||||
|
||||
&.fa-align-right:before {
|
||||
content: "\f23c";
|
||||
}
|
||||
|
||||
&.fa-align-center:before {
|
||||
content: "\f239";
|
||||
}
|
||||
|
||||
&.fa-align-justify:before {
|
||||
content: "\f23a";
|
||||
}
|
||||
|
||||
&.fa-indent:before {
|
||||
content: "\f244";
|
||||
}
|
||||
|
||||
&.fa-outdent:before {
|
||||
content: "\f243";
|
||||
}
|
||||
|
||||
&.fa-text-height:before {
|
||||
content: "\f246";
|
||||
}
|
||||
|
||||
&.fa-table:before {
|
||||
content: "\f320";
|
||||
}
|
||||
|
||||
&.fa-link:before {
|
||||
content: "\f18e";
|
||||
}
|
||||
|
||||
&.fa-picture-o:before {
|
||||
content: "\f17f";
|
||||
}
|
||||
|
||||
&.fa-minus:before {
|
||||
content: "\f22f";
|
||||
}
|
||||
|
||||
&.fa-arrows-alt:before {
|
||||
content: "\f16d";
|
||||
}
|
||||
|
||||
&.fa-code:before {
|
||||
content: "\f13a";
|
||||
}
|
||||
|
||||
&.fa-question:before {
|
||||
content: "\f1f5";
|
||||
}
|
||||
|
||||
&.fa-eraser:before {
|
||||
content: "\f23f";
|
||||
}
|
||||
|
||||
&.fa-square:before {
|
||||
content: "\f279";
|
||||
}
|
||||
|
||||
&.fa-circle-o:before {
|
||||
content: "\f26c";
|
||||
}
|
||||
|
||||
&.fa-times:before {
|
||||
content: "\f136";
|
||||
}
|
||||
}
|
||||
|
||||
.note-air-popover {
|
||||
.arrow {
|
||||
left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-editor {
|
||||
border: 1px solid #e4e4e4;
|
||||
|
||||
.note-editable {
|
||||
padding: 20px 23px;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
.sweet-alert {
|
||||
border-radius: 2px;
|
||||
padding: 10px 30px;
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
margin: 20px 2px 0;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
.twitter-typeahead {
|
||||
width: 100%;
|
||||
|
||||
.tt-menu {
|
||||
min-width: 200px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tt-suggestion:hover,
|
||||
.tt-cursor {
|
||||
background-color: rgba(0,0,0,0.075);
|
||||
}
|
||||
|
||||
.tt-suggestion {
|
||||
padding: 8px 17px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tt-hint {
|
||||
color: #818181 !important;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/* ui-select adjustments for SuperFlat */
|
||||
.clearable button {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Same definition as .form-control */
|
||||
.ui-select-toggle.btn-default {
|
||||
height: 35px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.42857143;
|
||||
color: #9E9E9E;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 2px;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
|
||||
&:hover, &:active, &.active, &:focus, &.focus {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-default-focus {
|
||||
outline: none;
|
||||
outline-offset: 0;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
cohort-renderer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cornelius-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.cornelius-table {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
|
||||
tr, th, td {
|
||||
border-color: @table-border-color;
|
||||
}
|
||||
|
||||
td {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.cornelius-time, .cornelius-label, .cornelius-people {
|
||||
background-color: fade(@redash-gray, 3%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
.col-table .missing-value {
|
||||
color: #b94a48;
|
||||
}
|
||||
|
||||
.col-table .super-small-input {
|
||||
padding-left: 3px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.col-table .ui-select-toggle, .col-table .ui-select-search {
|
||||
padding: 2px;
|
||||
padding-left: 5px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
@@ -6,32 +6,9 @@
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.map-custom-control.leaflet-bar {
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
&.top-left {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.top-right {
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.bottom-left {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.bottom-right {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-popup-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -1,75 +1,57 @@
|
||||
/** LESS Plugins **/
|
||||
@import 'inc/less-plugins/for';
|
||||
@import "inc/less-plugins/for";
|
||||
|
||||
/** Load Main Bootstrap LESS files **/
|
||||
@import '~bootstrap/less/bootstrap';
|
||||
@import "~bootstrap/less/bootstrap";
|
||||
|
||||
/** Load Vendors Dependencies **/
|
||||
@import '~font-awesome/less/font-awesome';
|
||||
@import '~ui-select/dist/select.css';
|
||||
@import '~angular-resizable/src/angular-resizable.css';
|
||||
@import '~material-design-iconic-font/dist/css/material-design-iconic-font.css';
|
||||
@import '~pace-progress/themes/blue/pace-theme-minimal.css';
|
||||
@import "~font-awesome/less/font-awesome";
|
||||
@import "~material-design-iconic-font/dist/css/material-design-iconic-font.css";
|
||||
@import "~pace-progress/themes/blue/pace-theme-minimal.css";
|
||||
|
||||
@import 'inc/angular';
|
||||
@import 'inc/variables';
|
||||
@import 'inc/mixins';
|
||||
@import 'inc/font';
|
||||
@import 'inc/print';
|
||||
@import "inc/variables";
|
||||
@import "inc/mixins";
|
||||
@import "inc/font";
|
||||
@import "inc/print";
|
||||
|
||||
@import 'inc/bootstrap-overrides';
|
||||
@import 'inc/base';
|
||||
@import 'inc/generics';
|
||||
@import 'inc/form';
|
||||
@import 'inc/button';
|
||||
@import 'inc/list';
|
||||
@import 'inc/header';
|
||||
@import 'inc/tile';
|
||||
@import 'inc/label';
|
||||
@import 'inc/dropdown';
|
||||
@import 'inc/list-group';
|
||||
@import 'inc/misc';
|
||||
@import 'inc/progress-bar';
|
||||
@import 'inc/widgets';
|
||||
@import 'inc/table';
|
||||
@import 'inc/alert';
|
||||
@import 'inc/media';
|
||||
@import 'inc/modal';
|
||||
@import 'inc/tab';
|
||||
@import 'inc/panel';
|
||||
@import 'inc/tooltips';
|
||||
@import 'inc/popover';
|
||||
@import 'inc/breadcrumb';
|
||||
@import 'inc/jumbotron';
|
||||
@import 'inc/profile';
|
||||
@import 'inc/404';
|
||||
@import 'inc/ie-warning';
|
||||
@import 'inc/edit-in-place';
|
||||
@import 'inc/growl';
|
||||
@import 'inc/flex';
|
||||
@import 'inc/ace-editor';
|
||||
@import 'inc/overlay';
|
||||
@import 'inc/schema-browser';
|
||||
@import 'inc/toast';
|
||||
@import 'inc/visualizations/box';
|
||||
@import 'inc/visualizations/pivot-table';
|
||||
@import 'inc/visualizations/map';
|
||||
@import 'inc/visualizations/cohort';
|
||||
@import 'inc/visualizations/misc';
|
||||
|
||||
/** VENDOR OVERRIDES **/
|
||||
@import 'inc/vendor-overrides/bootstrap-select';
|
||||
@import 'inc/vendor-overrides/bootstrap-datetimepicker';
|
||||
@import 'inc/vendor-overrides/typeahead';
|
||||
@import 'inc/vendor-overrides/sweetalert';
|
||||
@import 'inc/vendor-overrides/ui-select';
|
||||
@import "inc/bootstrap-overrides";
|
||||
@import "inc/base";
|
||||
@import "inc/generics";
|
||||
@import "inc/form";
|
||||
@import "inc/button";
|
||||
@import "inc/list";
|
||||
@import "inc/header";
|
||||
@import "inc/tile";
|
||||
@import "inc/label";
|
||||
@import "inc/dropdown";
|
||||
@import "inc/list-group";
|
||||
@import "inc/misc";
|
||||
@import "inc/progress-bar";
|
||||
@import "inc/widgets";
|
||||
@import "inc/table";
|
||||
@import "inc/alert";
|
||||
@import "inc/media";
|
||||
@import "inc/modal";
|
||||
@import "inc/panel";
|
||||
@import "inc/tooltips";
|
||||
@import "inc/popover";
|
||||
@import "inc/breadcrumb";
|
||||
@import "inc/jumbotron";
|
||||
@import "inc/profile";
|
||||
@import "inc/404";
|
||||
@import "inc/ie-warning";
|
||||
@import "inc/edit-in-place";
|
||||
@import "inc/flex";
|
||||
@import "inc/ace-editor";
|
||||
@import "inc/schema-browser";
|
||||
@import "inc/visualizations/box";
|
||||
@import "inc/visualizations/pivot-table";
|
||||
@import "inc/visualizations/map";
|
||||
@import "inc/visualizations/misc";
|
||||
|
||||
/** REDASH STYLING **/
|
||||
@import 'redash/redash-table';
|
||||
@import 'redash/query';
|
||||
@import 'redash/tags-control';
|
||||
@import 'redash/css-logo';
|
||||
@import 'redash/loading-indicator';
|
||||
|
||||
|
||||
|
||||
@import "redash/redash-table";
|
||||
@import "redash/query";
|
||||
@import "redash/tags-control";
|
||||
@import "redash/css-logo";
|
||||
@import "redash/loading-indicator";
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// hide indicator when app-view has content
|
||||
app-view:not(:empty) ~ .loading-indicator {
|
||||
// hide indicator when application has content
|
||||
#application-root:not(:empty) ~ .loading-indicator {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
pointer-events: none;
|
||||
@@ -48,4 +48,4 @@ app-view:not(:empty) ~ .loading-indicator {
|
||||
* {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ body.fixed-layout {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
app-view {
|
||||
#application-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
@@ -17,24 +17,15 @@ body.fixed-layout {
|
||||
}
|
||||
}
|
||||
|
||||
.tab-nav .tab-new-vis {
|
||||
margin: 0 5px;
|
||||
|
||||
> a {
|
||||
color: @headings-color;
|
||||
margin-top: 8px;
|
||||
padding: 7px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-controller {
|
||||
padding: 10px 15px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button, div, span {
|
||||
button,
|
||||
div,
|
||||
span {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -44,7 +35,7 @@ body.fixed-layout {
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
content: "";
|
||||
height: 50px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
@@ -55,7 +46,7 @@ body.fixed-layout {
|
||||
}
|
||||
|
||||
.p-b-60 {
|
||||
padding-bottom: 60px !important;
|
||||
padding-bottom: 60px !important;
|
||||
}
|
||||
|
||||
.bottom-controller-container {
|
||||
@@ -65,91 +56,11 @@ body.fixed-layout {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.query-metadata__bottom {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.bottom-controller, .bottom-controller-container {
|
||||
.query-metadata__property {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
// Editor
|
||||
edit-in-place p, span.editable {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
edit-in-place p.editable:hover {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.editor__control {
|
||||
margin-top: 10px;
|
||||
|
||||
.dropdown-toggle {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.ace_editor.ace_autocomplete .ace_completion-highlight {
|
||||
text-shadow: none !important;
|
||||
background: #ffff005e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.query-metadata {
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
td {
|
||||
padding: 3px 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tr {
|
||||
td:first-of-type {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.query-metadata__property {
|
||||
width: 60px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
._query-metadata__time {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.editor__control {
|
||||
.form-control {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.schema-container {
|
||||
background: transparent;
|
||||
flex-grow: 1;
|
||||
@@ -159,7 +70,7 @@ edit-in-place p.editable:hover {
|
||||
|
||||
.editor__left {
|
||||
height: 100% !important;
|
||||
width: calc(~'25% - 10px');
|
||||
width: calc(~"25% - 10px");
|
||||
margin-right: 10px;
|
||||
|
||||
.form-control {
|
||||
@@ -191,7 +102,6 @@ edit-in-place p.editable:hover {
|
||||
}
|
||||
|
||||
.embed__vis {
|
||||
|
||||
}
|
||||
|
||||
.query__vis {
|
||||
@@ -246,6 +156,17 @@ edit-in-place p.editable:hover {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.tags-control a {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.tags-control a {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.label-tag {
|
||||
@@ -276,7 +197,22 @@ a.label-tag {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
|
||||
.tile, .tiled {
|
||||
.resizable-component.react-resizable {
|
||||
.react-resizable-handle-horizontal {
|
||||
border-right: 1px solid #efefef;
|
||||
}
|
||||
|
||||
.react-resizable-handle-vertical {
|
||||
border-bottom: 1px solid #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
.query-metadata.query-metadata-horizontal {
|
||||
border-bottom: 1px solid #efefef;
|
||||
}
|
||||
|
||||
.tile,
|
||||
.tiled {
|
||||
box-shadow: none;
|
||||
padding: 15px 0 !important;
|
||||
}
|
||||
@@ -291,22 +227,26 @@ a.label-tag {
|
||||
min-width: 10px;
|
||||
overflow-x: hidden;
|
||||
|
||||
.schema-container {
|
||||
}
|
||||
|
||||
.editor__left__data-source,
|
||||
.schema-control,
|
||||
.query-metadata--history,
|
||||
.editor {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.query-metadata {
|
||||
border-top: 1px solid #efefef;
|
||||
.editor__left__schema,
|
||||
.editor__left__data-source {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.query-metadata, .editor__left__schema, .editor__left__data-source {
|
||||
padding: 15px;
|
||||
.editor__left__data-source {
|
||||
.ant-select {
|
||||
.ant-select-selection-selected-value {
|
||||
img,
|
||||
span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor__left__schema {
|
||||
@@ -317,7 +257,7 @@ a.label-tag {
|
||||
padding-top: 0 !important;
|
||||
position: relative;
|
||||
|
||||
schema-browser {
|
||||
.schema-container {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 0;
|
||||
@@ -326,10 +266,6 @@ a.label-tag {
|
||||
}
|
||||
}
|
||||
}
|
||||
main {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.content {
|
||||
background: #fff;
|
||||
flex-grow: 1;
|
||||
@@ -340,22 +276,13 @@ a.label-tag {
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
|
||||
.editor {
|
||||
border-bottom: 1px solid #efefef;
|
||||
}
|
||||
|
||||
.pivot-table-visualization-container > table,
|
||||
.visualization-renderer > .visualization-renderer-wrapper {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.row {
|
||||
background: #fff;
|
||||
z-index: 9;
|
||||
min-height: 50px;
|
||||
|
||||
&.resizable {
|
||||
@@ -368,6 +295,10 @@ a.label-tag {
|
||||
justify-content: space-around;
|
||||
align-content: space-around;
|
||||
overflow: hidden;
|
||||
|
||||
min-height: 10px;
|
||||
max-height: 70vh;
|
||||
flex: 0 0 300px;
|
||||
}
|
||||
|
||||
.row {
|
||||
@@ -394,7 +325,10 @@ a.label-tag {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
.rg-right, .rg-left, .rg-top, .rg-bottom {
|
||||
.rg-right,
|
||||
.rg-left,
|
||||
.rg-top,
|
||||
.rg-bottom {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@@ -409,32 +343,34 @@ a.label-tag {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
.rg-right, .rg-left {
|
||||
.rg-right,
|
||||
.rg-left {
|
||||
span {
|
||||
border-width: 0 1px;
|
||||
top: 50%;
|
||||
margin: -10px 0 0 @spacing/4;
|
||||
margin: -10px 0 0 @spacing / 4;
|
||||
height: 20px;
|
||||
width: 3px;
|
||||
}
|
||||
}
|
||||
.rg-top, .rg-bottom {
|
||||
.rg-top,
|
||||
.rg-bottom {
|
||||
span {
|
||||
border-width: 1px 0;
|
||||
left: 50%;
|
||||
margin: @spacing/4 0 0 -10px;
|
||||
margin: @spacing / 4 0 0 -10px;
|
||||
width: 20px;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
.rg-top {
|
||||
.rg-top {
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-top: -@spacing/2;
|
||||
margin-top: -@spacing / 2;
|
||||
}
|
||||
.rg-right {
|
||||
.rg-right {
|
||||
cursor: col-resize;
|
||||
border-right: 1px solid #efefef;
|
||||
height: 100%;
|
||||
@@ -446,7 +382,7 @@ a.label-tag {
|
||||
background: fade(@redash-gray, 6%);
|
||||
}
|
||||
}
|
||||
.rg-bottom {
|
||||
.rg-bottom {
|
||||
cursor: row-resize;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
@@ -458,7 +394,7 @@ a.label-tag {
|
||||
background: fade(@redash-gray, 6%);
|
||||
}
|
||||
}
|
||||
.rg-left {
|
||||
.rg-left {
|
||||
cursor: col-resize;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
@@ -471,11 +407,6 @@ a.label-tag {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.query-fullscreen .query-metadata__mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
// Visualization editor
|
||||
.modal-xl .modal-content {
|
||||
border: none;
|
||||
@@ -512,32 +443,6 @@ nav .rg-bottom {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.query-metadata--description {
|
||||
max-height: 125px;
|
||||
overflow-y: auto;
|
||||
|
||||
.edit-in-place.active {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.edit-in-place .rd-form-control {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.query-metadata--refresh {
|
||||
height: 50px;
|
||||
border: none !important;
|
||||
|
||||
.query-metadata__property {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.query-tags {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
@@ -546,7 +451,6 @@ nav .rg-bottom {
|
||||
|
||||
.query-tags__mobile {
|
||||
display: none;
|
||||
margin: -5px 0 0 0;
|
||||
padding: 0 0 0 23px;
|
||||
}
|
||||
|
||||
@@ -561,7 +465,15 @@ nav .rg-bottom {
|
||||
}
|
||||
|
||||
.edit-visualization {
|
||||
margin-right: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (min-width: 880px) {
|
||||
.query-fullscreen {
|
||||
.query-metadata.query-metadata-horizontal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Smaller screens
|
||||
@@ -591,10 +503,6 @@ nav .rg-bottom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-nav .tab-new-vis {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.query-fullscreen {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -607,25 +515,6 @@ nav .rg-bottom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.query-metadata__mobile {
|
||||
border-bottom: 1px solid #efefef;
|
||||
min-height: 0 !important;
|
||||
flex-shrink: 0;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.profile__image_thumb {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
|
||||
.query-metadata__property {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
@@ -671,7 +560,8 @@ nav .rg-bottom {
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.editor__left__schema, .editor__left__data-source {
|
||||
.editor__left__schema,
|
||||
.editor__left__data-source {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -686,9 +576,5 @@ nav .rg-bottom {
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
favorites-control {
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,3 @@
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
// This is for using .inline-tags-control in Angular which renders
|
||||
// a little differently than React (e.g. in Alert.html)
|
||||
.inline-tags-control .tags-control {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import React, { forwardRef } from "react";
|
||||
import AceEditor from "react-ace";
|
||||
|
||||
import './AceEditorInput.less';
|
||||
import "./AceEditorInput.less";
|
||||
|
||||
function AceEditorInput(props, ref) {
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isEmpty, template } from 'lodash';
|
||||
import React, { useState, useMemo, useCallback, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { isEmpty, template } from "lodash";
|
||||
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Menu from "antd/lib/menu";
|
||||
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
|
||||
export default function FavoritesDropdown({ fetch, urlTemplate }) {
|
||||
const [items, setItems] = useState();
|
||||
@@ -15,19 +15,24 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
|
||||
const noItems = isEmpty(items);
|
||||
const urlCompiled = useMemo(() => template(urlTemplate), [urlTemplate]);
|
||||
|
||||
const fetchItems = useCallback(() => {
|
||||
setLoading(true);
|
||||
fetch().$promise
|
||||
.then(({ results }) => {
|
||||
setItems(results);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [fetch]);
|
||||
const fetchItems = useCallback(
|
||||
(showLoadingState = true) => {
|
||||
setLoading(showLoadingState);
|
||||
fetch()
|
||||
.then(({ results }) => {
|
||||
setItems(results);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[fetch]
|
||||
);
|
||||
|
||||
// fetch items on init
|
||||
useEffect(fetchItems, []);
|
||||
useEffect(() => {
|
||||
fetchItems(false);
|
||||
}, [fetchItems]);
|
||||
|
||||
// fetch items on click
|
||||
const onVisibleChange = visible => visible && fetchItems();
|
||||
@@ -57,7 +62,12 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown disabled={loading} trigger={['click']} placement="bottomLeft" onVisibleChange={onVisibleChange} overlay={menu}>
|
||||
<Dropdown
|
||||
disabled={loading}
|
||||
trigger={["click"]}
|
||||
placement="bottomLeft"
|
||||
onVisibleChange={onVisibleChange}
|
||||
overlay={menu}>
|
||||
{loading ? <Icon type="loading" spin /> : <Icon type="down" />}
|
||||
</Dropdown>
|
||||
);
|
||||
@@ -1,80 +1,85 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React, { useCallback, useRef } from "react";
|
||||
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import Input from 'antd/lib/input';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Button from "antd/lib/button";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Menu from "antd/lib/menu";
|
||||
import Input from "antd/lib/input";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
import FavoritesDropdown from './components/FavoritesDropdown';
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import CreateDashboardDialog from '@/components/dashboards/CreateDashboardDialog';
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
|
||||
import navigateTo from "@/components/ApplicationArea/navigateTo";
|
||||
|
||||
import { currentUser, Auth, clientConfig } from '@/services/auth';
|
||||
import { $location, $route } from '@/services/ng';
|
||||
import { Dashboard } from '@/services/dashboard';
|
||||
import { Query } from '@/services/query';
|
||||
import frontendVersion from '@/version.json';
|
||||
import logoUrl from '@/assets/images/redash_icon_small.png';
|
||||
import { currentUser, Auth, clientConfig } from "@/services/auth";
|
||||
import { Dashboard } from "@/services/dashboard";
|
||||
import { Query } from "@/services/query";
|
||||
import frontendVersion from "@/version.json";
|
||||
import logoUrl from "@/assets/images/redash_icon_small.png";
|
||||
|
||||
import './AppHeader.less';
|
||||
import FavoritesDropdown from "./FavoritesDropdown";
|
||||
import "./index.less";
|
||||
|
||||
function onSearch(q) {
|
||||
$location.path('/queries').search({ q });
|
||||
$route.reload();
|
||||
navigateTo(`queries?q=${encodeURIComponent(q)}`);
|
||||
}
|
||||
|
||||
function DesktopNavbar() {
|
||||
const showCreateDashboardDialog = useCallback(() => {
|
||||
CreateDashboardDialog.showModal().result.catch(() => {}); // ignore dismiss
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="app-header" data-platform="desktop">
|
||||
<div>
|
||||
<Menu mode="horizontal" selectable={false}>
|
||||
{currentUser.hasPermission('list_dashboards') && (
|
||||
{currentUser.hasPermission("list_dashboards") && (
|
||||
<Menu.Item key="dashboards" className="dropdown-menu-item">
|
||||
<Button href="dashboards">Dashboards</Button>
|
||||
<FavoritesDropdown fetch={Dashboard.favorites} urlTemplate="dashboard/${slug}" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('view_query') && (
|
||||
{currentUser.hasPermission("view_query") && (
|
||||
<Menu.Item key="queries" className="dropdown-menu-item">
|
||||
<Button href="queries">Queries</Button>
|
||||
<FavoritesDropdown fetch={Query.favorites} urlTemplate="queries/${id}" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('list_alerts') && (
|
||||
{currentUser.hasPermission("list_alerts") && (
|
||||
<Menu.Item key="alerts">
|
||||
<Button href="alerts">Alerts</Button>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={(
|
||||
<Menu>
|
||||
{currentUser.hasPermission('create_query') && (
|
||||
<Menu.Item key="new-query">
|
||||
<a href="queries/new">New Query</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('create_dashboard') && (
|
||||
<Menu.Item key="new-dashboard">
|
||||
<a onMouseUp={CreateDashboardDialog.showModal}>New Dashboard</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key="new-alert">
|
||||
<a href="alerts/new">New Alert</a>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
<Button type="primary" data-test="CreateButton">
|
||||
Create <Icon type="down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
{currentUser.canCreate() && (
|
||||
<Dropdown
|
||||
trigger={["click"]}
|
||||
overlay={
|
||||
<Menu>
|
||||
{currentUser.hasPermission("create_query") && (
|
||||
<Menu.Item key="new-query">
|
||||
<a href="queries/new">New Query</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission("create_dashboard") && (
|
||||
<Menu.Item key="new-dashboard">
|
||||
<a onMouseUp={showCreateDashboardDialog}>New Dashboard</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission("list_alerts") && (
|
||||
<Menu.Item key="new-alert">
|
||||
<a href="alerts/new">New Alert</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
}>
|
||||
<Button type="primary" data-test="CreateButton">
|
||||
Create <Icon type="down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-logo">
|
||||
<a href="./">
|
||||
@@ -105,65 +110,68 @@ function DesktopNavbar() {
|
||||
<Dropdown
|
||||
overlayStyle={{ minWidth: 200 }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
overlay={(
|
||||
trigger={["click"]}
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item key="profile">
|
||||
<a href="users/me">Edit Profile</a>
|
||||
</Menu.Item>
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
<Menu.Divider />
|
||||
)}
|
||||
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
|
||||
{currentUser.isAdmin && (
|
||||
<Menu.Item key="datasources">
|
||||
<a href="data_sources">Data Sources</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('list_users') && (
|
||||
{currentUser.hasPermission("list_users") && (
|
||||
<Menu.Item key="groups">
|
||||
<a href="groups">Groups</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('list_users') && (
|
||||
{currentUser.hasPermission("list_users") && (
|
||||
<Menu.Item key="users">
|
||||
<a href="users">Users</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key="snippets">
|
||||
<a href="query_snippets">Query Snippets</a>
|
||||
</Menu.Item>
|
||||
{currentUser.hasPermission('list_users') && (
|
||||
{currentUser.hasPermission("create_query") && (
|
||||
<Menu.Item key="snippets">
|
||||
<a href="query_snippets">Query Snippets</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.isAdmin && (
|
||||
<Menu.Item key="destinations">
|
||||
<a href="destinations">Alert Destinations</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
<Menu.Divider />
|
||||
)}
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
|
||||
{currentUser.hasPermission("super_admin") && (
|
||||
<Menu.Item key="status">
|
||||
<a href="admin/status">System Status</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="logout" onClick={() => Auth.logout()}>Log out</Menu.Item>
|
||||
<Menu.Item key="logout" onClick={() => Auth.logout()}>
|
||||
Log out
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="version" disabled>
|
||||
Version: {clientConfig.version}
|
||||
{frontendVersion !== clientConfig.version && ` (${frontendVersion.substring(0, 8)})`}
|
||||
{clientConfig.newVersionAvailable && currentUser.hasPermission('super_admin') && (
|
||||
{clientConfig.newVersionAvailable && currentUser.hasPermission("super_admin") && (
|
||||
<Tooltip title="Update Available" placement="rightTop">
|
||||
{' '}
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a href="https://version.redash.io/" className="update-available" target="_blank" rel="noopener">
|
||||
{" "}
|
||||
{/* eslint-disable react/jsx-no-target-blank */}
|
||||
<a
|
||||
href="https://version.redash.io/"
|
||||
className="update-available"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i className="fa fa-arrow-circle-down" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
}>
|
||||
<Button data-test="ProfileDropdown" className="profile-dropdown">
|
||||
<img src={currentUser.profile_image_url} alt={currentUser.name} />
|
||||
<span>{currentUser.name}</span>
|
||||
@@ -190,21 +198,21 @@ function MobileNavbar() {
|
||||
<div>
|
||||
<Dropdown
|
||||
overlayStyle={{ minWidth: 200 }}
|
||||
trigger={['click']}
|
||||
trigger={["click"]}
|
||||
getPopupContainer={() => ref.current} // so the overlay menu stays with the fixed header when page scrolls
|
||||
overlay={(
|
||||
overlay={
|
||||
<Menu mode="vertical" selectable={false}>
|
||||
{currentUser.hasPermission('list_dashboards') && (
|
||||
{currentUser.hasPermission("list_dashboards") && (
|
||||
<Menu.Item key="dashboards">
|
||||
<a href="dashboards">Dashboards</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('view_query') && (
|
||||
{currentUser.hasPermission("view_query") && (
|
||||
<Menu.Item key="queries">
|
||||
<a href="queries">Queries</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('list_alerts') && (
|
||||
{currentUser.hasPermission("list_alerts") && (
|
||||
<Menu.Item key="alerts">
|
||||
<a href="alerts">Alerts</a>
|
||||
</Menu.Item>
|
||||
@@ -218,30 +226,33 @@ function MobileNavbar() {
|
||||
<a href="data_sources">Settings</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
{currentUser.hasPermission("super_admin") && (
|
||||
<Menu.Item key="status">
|
||||
<a href="admin/status">System Status</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
<Menu.Divider />
|
||||
)}
|
||||
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
|
||||
<Menu.Item key="help">
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a href="https://redash.io/help" target="_blank" rel="noopener">Help</a>
|
||||
<a href="https://redash.io/help" target="_blank" rel="noopener">
|
||||
Help
|
||||
</a>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="logout" onClick={() => Auth.logout()}>
|
||||
Log out
|
||||
</Menu.Item>
|
||||
<Menu.Item key="logout" onClick={() => Auth.logout()}>Log out</Menu.Item>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
<Button><Icon type="menu" /></Button>
|
||||
}>
|
||||
<Button>
|
||||
<Icon type="menu" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppHeader() {
|
||||
export default function ApplicationHeader() {
|
||||
return (
|
||||
<nav className="app-header-wrapper">
|
||||
<DesktopNavbar />
|
||||
@@ -249,9 +260,3 @@ export function AppHeader() {
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('appHeader', react2angular(AppHeader));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
@@ -8,13 +8,13 @@ nav .app-header {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
background: white;
|
||||
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, .15);
|
||||
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15);
|
||||
|
||||
.darker {
|
||||
color: #333 !important;
|
||||
|
||||
&:hover {
|
||||
color: #2196F3 !important;
|
||||
color: #2196f3 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ nav .app-header {
|
||||
height: 50px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.ant-btn {
|
||||
font-weight: 500;
|
||||
|
||||
@@ -70,7 +70,7 @@ nav .app-header {
|
||||
top: 2px;
|
||||
|
||||
svg {
|
||||
transition: transform .2s cubic-bezier(.75,0,.25,1);
|
||||
transition: transform 0.2s cubic-bezier(0.75, 0, 0.25, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ nav .app-header {
|
||||
.menu-item-button {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
|
||||
.ant-menu-root {
|
||||
margin: 0 5px;
|
||||
}
|
||||
@@ -198,10 +198,10 @@ nav .app-header {
|
||||
|
||||
.ant-dropdown-menu-item .help-trigger {
|
||||
display: inline;
|
||||
color: #2196F3;
|
||||
color: #2196f3;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu.favorites-dropdown {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
59
client/app/components/ApplicationArea/ErrorMessage.jsx
Normal file
59
client/app/components/ApplicationArea/ErrorMessage.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { isObject, get } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
function getErrorMessageByStatus(status, defaultMessage) {
|
||||
switch (status) {
|
||||
case 404:
|
||||
return "It seems like the page you're looking for cannot be found.";
|
||||
case 401:
|
||||
case 403:
|
||||
return "It seems like you don’t have permission to see this page.";
|
||||
default:
|
||||
return defaultMessage;
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorMessage(error) {
|
||||
const message = "It seems like we encountered an error. Try refreshing this page or contact your administrator.";
|
||||
if (isObject(error)) {
|
||||
// HTTP errors
|
||||
if (error.isAxiosError && isObject(error.response)) {
|
||||
return getErrorMessageByStatus(error.response.status, get(error, "response.data.message", message));
|
||||
}
|
||||
// Router errors
|
||||
if (error.status) {
|
||||
return getErrorMessageByStatus(error.status, message);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
export default function ErrorMessage({ error }) {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
|
||||
return (
|
||||
<div className="fixed-container" data-test="ErrorMessage">
|
||||
<div className="container">
|
||||
<div className="col-md-8 col-md-push-2">
|
||||
<div className="error-state bg-white tiled">
|
||||
<div className="error-state__icon">
|
||||
<i className="zmdi zmdi-alert-circle-o" />
|
||||
</div>
|
||||
<div className="error-state__details">
|
||||
<h4>{getErrorMessage(error)}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
error: PropTypes.object.isRequired,
|
||||
};
|
||||
151
client/app/components/ApplicationArea/Router.jsx
Normal file
151
client/app/components/ApplicationArea/Router.jsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { isFunction, map, fromPairs, extend, startsWith, trimStart, trimEnd } from "lodash";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import UniversalRouter from "universal-router";
|
||||
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||
import location from "@/services/location";
|
||||
import url from "@/services/url";
|
||||
|
||||
import ErrorMessage from "./ErrorMessage";
|
||||
|
||||
function generateRouteKey() {
|
||||
return Math.random()
|
||||
.toString(32)
|
||||
.substr(2);
|
||||
}
|
||||
|
||||
export function stripBase(href) {
|
||||
// Resolve provided link and '' (root) relative to document's base.
|
||||
// If provided href is not related to current document (does not
|
||||
// start with resolved root) - return false. Otherwise
|
||||
// strip root and return relative url.
|
||||
|
||||
const baseHref = trimEnd(url.normalize(""), "/") + "/";
|
||||
href = url.normalize(href);
|
||||
|
||||
if (startsWith(href, baseHref)) {
|
||||
return "/" + trimStart(href.substr(baseHref.length), "/");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveRouteDependencies(route) {
|
||||
return Promise.all(
|
||||
map(route.resolve, (value, key) => {
|
||||
value = isFunction(value) ? value(route.routeParams, route, location) : value;
|
||||
return Promise.resolve(value).then(result => [key, result]);
|
||||
})
|
||||
).then(results => {
|
||||
route.routeParams = extend(route.routeParams, fromPairs(results));
|
||||
return route;
|
||||
});
|
||||
}
|
||||
|
||||
export default function Router({ routes, onRouteChange }) {
|
||||
const [currentRoute, setCurrentRoute] = useState(null);
|
||||
|
||||
const currentPathRef = useRef(null);
|
||||
const errorHandlerRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
let isAbandoned = false;
|
||||
|
||||
const router = new UniversalRouter(routes, {
|
||||
resolveRoute({ route }, routeParams) {
|
||||
if (isFunction(route.render)) {
|
||||
return { ...route, routeParams };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function resolve(action) {
|
||||
if (!isAbandoned) {
|
||||
if (errorHandlerRef.current) {
|
||||
errorHandlerRef.current.reset();
|
||||
}
|
||||
|
||||
const pathname = stripBase(location.path);
|
||||
|
||||
// This is a optimization for route resolver: if current route was already resolved
|
||||
// from this path - do nothing. It also prevents router from using outdated route in a case
|
||||
// when user navigated to another path while current one was still resolving.
|
||||
// Note: this lock uses only `path` fragment of URL to distinguish routes because currently
|
||||
// all pages depend only on this fragment and handle search/hash on their own. If router
|
||||
// should reload page on search/hash change - this fragment (and few checks below) should be updated
|
||||
if (pathname === currentPathRef.current) {
|
||||
return;
|
||||
}
|
||||
currentPathRef.current = pathname;
|
||||
|
||||
// Don't reload controller if URL was replaced
|
||||
if (action === "REPLACE") {
|
||||
return;
|
||||
}
|
||||
|
||||
router
|
||||
.resolve({ pathname })
|
||||
.then(route => {
|
||||
return isAbandoned || currentPathRef.current !== pathname ? null : resolveRouteDependencies(route);
|
||||
})
|
||||
.then(route => {
|
||||
if (route) {
|
||||
setCurrentRoute({ ...route, key: generateRouteKey() });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (!isAbandoned && currentPathRef.current === pathname) {
|
||||
setCurrentRoute({
|
||||
render: currentRoute => <ErrorMessage {...currentRoute.routeParams} />,
|
||||
routeParams: { error },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolve("PUSH");
|
||||
|
||||
const unlisten = location.listen((unused, action) => resolve(action));
|
||||
|
||||
return () => {
|
||||
isAbandoned = true;
|
||||
unlisten();
|
||||
};
|
||||
}, [routes]);
|
||||
|
||||
useEffect(() => {
|
||||
onRouteChange(currentRoute);
|
||||
}, [currentRoute, onRouteChange]);
|
||||
|
||||
if (!currentRoute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary ref={errorHandlerRef} renderError={error => <ErrorMessage error={error} />}>
|
||||
{currentRoute.render(currentRoute)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
Router.propTypes = {
|
||||
routes: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
path: PropTypes.string.isRequired,
|
||||
render: PropTypes.func, // (routeParams: PropTypes.object; currentRoute; location) => PropTypes.node
|
||||
// Additional props to be injected into route component.
|
||||
// Object keys are props names. Object values will become prop values:
|
||||
// - if value is a function - it will be called without arguments, and result will be used; otherwise value will be used;
|
||||
// - after previous step, if value is a promise - router will wait for it to resolve; resolved value then will be used;
|
||||
// otherwise value will be used directly.
|
||||
resolve: PropTypes.objectOf(PropTypes.any),
|
||||
})
|
||||
),
|
||||
onRouteChange: PropTypes.func,
|
||||
};
|
||||
|
||||
Router.defaultProps = {
|
||||
routes: [],
|
||||
onRouteChange: () => {},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { isString } from "lodash";
|
||||
import navigateTo from "./navigateTo";
|
||||
|
||||
export default function handleNavigationIntent(event) {
|
||||
let element = event.target;
|
||||
while (element) {
|
||||
if (element.tagName === "A") {
|
||||
break;
|
||||
}
|
||||
element = element.parentNode;
|
||||
}
|
||||
if (!element || !element.hasAttribute("href")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep some default behaviour
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = element.getAttribute("target");
|
||||
if (isString(target) && target.toLowerCase() === "_blank") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
navigateTo(element.href);
|
||||
}
|
||||
37
client/app/components/ApplicationArea/index.jsx
Normal file
37
client/app/components/ApplicationArea/index.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import routes from "@/pages";
|
||||
import Router from "./Router";
|
||||
import handleNavigationIntent from "./handleNavigationIntent";
|
||||
import ErrorMessage from "./ErrorMessage";
|
||||
|
||||
export default function ApplicationArea() {
|
||||
const [currentRoute, setCurrentRoute] = useState(null);
|
||||
const [unhandledError, setUnhandledError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentRoute && currentRoute.title) {
|
||||
document.title = currentRoute.title;
|
||||
}
|
||||
}, [currentRoute]);
|
||||
|
||||
useEffect(() => {
|
||||
function globalErrorHandler(event) {
|
||||
event.preventDefault();
|
||||
setUnhandledError(event.error);
|
||||
}
|
||||
|
||||
document.body.addEventListener("click", handleNavigationIntent, false);
|
||||
window.addEventListener("error", globalErrorHandler, false);
|
||||
|
||||
return () => {
|
||||
document.body.removeEventListener("click", handleNavigationIntent, false);
|
||||
window.removeEventListener("error", globalErrorHandler, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (unhandledError) {
|
||||
return <ErrorMessage error={unhandledError} />;
|
||||
}
|
||||
|
||||
return <Router routes={routes} onRouteChange={setCurrentRoute} />;
|
||||
}
|
||||
25
client/app/components/ApplicationArea/navigateTo.js
Normal file
25
client/app/components/ApplicationArea/navigateTo.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import location from "@/services/location";
|
||||
import url from "@/services/url";
|
||||
import { stripBase } from "./Router";
|
||||
|
||||
// When `replace` is set to `true` - it will just replace current URL
|
||||
// without reloading current page (router will skip this location change)
|
||||
export default function navigateTo(href, replace = false) {
|
||||
// Allow calling chain to roll up, and then navigate
|
||||
setTimeout(() => {
|
||||
const isExternal = stripBase(href) === false;
|
||||
if (isExternal) {
|
||||
window.location = href;
|
||||
return;
|
||||
}
|
||||
href = url.parse(href);
|
||||
location.update(
|
||||
{
|
||||
path: href.pathname,
|
||||
search: href.search,
|
||||
hash: href.hash,
|
||||
},
|
||||
replace
|
||||
);
|
||||
}, 10);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useEffect, useState, useContext } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import { Auth } from "@/services/auth";
|
||||
|
||||
// This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object
|
||||
// that contains:
|
||||
// - `currentRoute.routeParams`
|
||||
// - `pageTitle` field which is equal to `currentRoute.title`
|
||||
// - `onError` field which is a `handleError` method of nearest error boundary
|
||||
// - `apiKey` field
|
||||
|
||||
function ApiKeySessionWrapper({ apiKey, currentRoute, renderChildren }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const { handleError } = useContext(ErrorBoundaryContext);
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
Auth.setApiKey(apiKey);
|
||||
Auth.loadConfig()
|
||||
.then(() => {
|
||||
if (!isCancelled) {
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!isCancelled) {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [apiKey]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={currentRoute.key}>
|
||||
{renderChildren({ ...currentRoute.routeParams, pageTitle: currentRoute.title, onError: handleError, apiKey })}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
ApiKeySessionWrapper.propTypes = {
|
||||
apiKey: PropTypes.string.isRequired,
|
||||
renderChildren: PropTypes.func,
|
||||
};
|
||||
|
||||
ApiKeySessionWrapper.defaultProps = {
|
||||
renderChildren: () => null,
|
||||
};
|
||||
|
||||
export default function routeWithApiKeySession({ render, getApiKey, ...rest }) {
|
||||
return {
|
||||
...rest,
|
||||
render: currentRoute => (
|
||||
<ApiKeySessionWrapper apiKey={getApiKey(currentRoute)} currentRoute={currentRoute} renderChildren={render} />
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import ErrorBoundary, { ErrorBoundaryContext } from "@/components/ErrorBoundary";
|
||||
import { Auth } from "@/services/auth";
|
||||
import organizationStatus from "@/services/organizationStatus";
|
||||
import ApplicationHeader from "./ApplicationHeader";
|
||||
import ErrorMessage from "./ErrorMessage";
|
||||
|
||||
// This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object
|
||||
// that contains:
|
||||
// - `currentRoute.routeParams`
|
||||
// - `pageTitle` field which is equal to `currentRoute.title`
|
||||
// - `onError` field which is a `handleError` method of nearest error boundary
|
||||
|
||||
function UserSessionWrapper({ bodyClass, currentRoute, renderChildren }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(!!Auth.isAuthenticated());
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
Promise.all([Auth.requireSession(), organizationStatus.refresh()])
|
||||
.then(() => {
|
||||
if (!isCancelled) {
|
||||
setIsAuthenticated(!!Auth.isAuthenticated());
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!isCancelled) {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (bodyClass) {
|
||||
document.body.classList.toggle(bodyClass, true);
|
||||
return () => {
|
||||
document.body.classList.toggle(bodyClass, false);
|
||||
};
|
||||
}
|
||||
}, [bodyClass]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ApplicationHeader />
|
||||
<React.Fragment key={currentRoute.key}>
|
||||
<ErrorBoundary renderError={error => <ErrorMessage error={error} />}>
|
||||
<ErrorBoundaryContext.Consumer>
|
||||
{({ handleError }) =>
|
||||
renderChildren({ ...currentRoute.routeParams, pageTitle: currentRoute.title, onError: handleError })
|
||||
}
|
||||
</ErrorBoundaryContext.Consumer>
|
||||
</ErrorBoundary>
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
UserSessionWrapper.propTypes = {
|
||||
bodyClass: PropTypes.string,
|
||||
renderChildren: PropTypes.func,
|
||||
};
|
||||
|
||||
UserSessionWrapper.defaultProps = {
|
||||
bodyClass: null,
|
||||
renderChildren: () => null,
|
||||
};
|
||||
|
||||
export default function routeWithUserSession({ render, bodyClass, ...rest }) {
|
||||
return {
|
||||
...rest,
|
||||
render: currentRoute => (
|
||||
<UserSessionWrapper bodyClass={bodyClass} currentRoute={currentRoute} renderChildren={render} />
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import '@/redash-font/style.less';
|
||||
import recordEvent from '@/services/recordEvent';
|
||||
|
||||
export default function AutocompleteToggle({ state, disabled, onToggle }) {
|
||||
let tooltipMessage = 'Live Autocomplete Enabled';
|
||||
let icon = 'icon-flash';
|
||||
if (!state) {
|
||||
tooltipMessage = 'Live Autocomplete Disabled';
|
||||
icon = 'icon-flash-off';
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
tooltipMessage = 'Live Autocomplete Not Available (Use Ctrl+Space to Trigger)';
|
||||
icon = 'icon-flash-off';
|
||||
}
|
||||
|
||||
const toggle = (newState) => {
|
||||
recordEvent('toggle_autocomplete', 'screen', 'query_editor', { state: newState });
|
||||
onToggle(newState);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip placement="top" title={tooltipMessage}>
|
||||
<button
|
||||
type="button"
|
||||
className={'btn btn-default m-r-5' + (disabled ? ' disabled' : '')}
|
||||
onClick={() => toggle(!state)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<i className={'icon ' + icon} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
AutocompleteToggle.propTypes = {
|
||||
state: PropTypes.bool.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -1,16 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Card from 'antd/lib/card';
|
||||
import Button from 'antd/lib/button';
|
||||
import Typography from 'antd/lib/typography';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import DynamicComponent from '@/components/DynamicComponent';
|
||||
import OrgSettings from '@/services/organizationSettings';
|
||||
import React, { useState } from "react";
|
||||
import Card from "antd/lib/card";
|
||||
import Button from "antd/lib/button";
|
||||
import Typography from "antd/lib/typography";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
import OrgSettings from "@/services/organizationSettings";
|
||||
|
||||
const Text = Typography.Text;
|
||||
|
||||
export function BeaconConsent() {
|
||||
function BeaconConsent() {
|
||||
const [hide, setHide] = useState(false);
|
||||
|
||||
if (!clientConfig.showBeaconConsentMessage || hide) {
|
||||
@@ -22,11 +21,11 @@ export function BeaconConsent() {
|
||||
setHide(true);
|
||||
};
|
||||
|
||||
const confirmConsent = (confirm) => {
|
||||
let message = '🙏 Thank you.';
|
||||
const confirmConsent = confirm => {
|
||||
let message = "🙏 Thank you.";
|
||||
|
||||
if (!confirm) {
|
||||
message = 'Settings Saved.';
|
||||
message = "Settings Saved.";
|
||||
}
|
||||
|
||||
OrgSettings.save({ beacon_consent: confirm }, message)
|
||||
@@ -41,14 +40,13 @@ export function BeaconConsent() {
|
||||
<DynamicComponent name="BeaconConsent">
|
||||
<div className="m-t-10 tiled">
|
||||
<Card
|
||||
title={(
|
||||
title={
|
||||
<>
|
||||
Would you be ok with sharing anonymous usage data with the Redash team?{' '}
|
||||
Would you be ok with sharing anonymous usage data with the Redash team?{" "}
|
||||
<HelpTrigger type="USAGE_DATA_SHARING" />
|
||||
</>
|
||||
)}
|
||||
bordered={false}
|
||||
>
|
||||
}
|
||||
bordered={false}>
|
||||
<Text>Help Redash improve by automatically sending anonymous usage data:</Text>
|
||||
<div className="m-t-5">
|
||||
<ul>
|
||||
@@ -67,7 +65,8 @@ export function BeaconConsent() {
|
||||
</div>
|
||||
<div className="m-t-15">
|
||||
<Text type="secondary">
|
||||
You can change this setting anytime from the <a href="settings/organization">Organization Settings</a> page.
|
||||
You can change this setting anytime from the <a href="settings/organization">Organization Settings</a>{" "}
|
||||
page.
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -76,8 +75,4 @@ export function BeaconConsent() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('beaconConsent', react2angular(BeaconConsent));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default BeaconConsent;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export function BigMessage({ message, icon, children, className }) {
|
||||
function BigMessage({ message, icon, children, className }) {
|
||||
return (
|
||||
<div className={'p-15 text-center ' + className}>
|
||||
<div className={"p-15 text-center " + className}>
|
||||
<h3 className="m-t-0 m-b-0">
|
||||
<i className={'fa ' + icon} />
|
||||
<i className={"fa " + icon} />
|
||||
</h3>
|
||||
<br />
|
||||
{message}
|
||||
@@ -23,13 +22,9 @@ BigMessage.propTypes = {
|
||||
};
|
||||
|
||||
BigMessage.defaultProps = {
|
||||
message: '',
|
||||
message: "",
|
||||
children: null,
|
||||
className: 'tiled bg-white',
|
||||
className: "tiled bg-white",
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('bigMessage', react2angular(BigMessage));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default BigMessage;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'antd/lib/button';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import './CodeBlock.less';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import "./CodeBlock.less";
|
||||
|
||||
export default class CodeBlock extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -20,7 +20,7 @@ export default class CodeBlock extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.ref = React.createRef();
|
||||
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported('copy');
|
||||
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported("copy");
|
||||
this.resetCopyState = null;
|
||||
}
|
||||
|
||||
@@ -36,14 +36,14 @@ export default class CodeBlock extends React.Component {
|
||||
|
||||
// copy
|
||||
try {
|
||||
const success = document.execCommand('copy');
|
||||
const success = document.execCommand("copy");
|
||||
if (!success) {
|
||||
throw new Error();
|
||||
}
|
||||
this.setState({ copied: 'Copied!' });
|
||||
this.setState({ copied: "Copied!" });
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
copied: 'Copy failed',
|
||||
copied: "Copy failed",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,13 +58,8 @@ export default class CodeBlock extends React.Component {
|
||||
const { copyable, children, ...props } = this.props;
|
||||
|
||||
const copyButton = (
|
||||
<Tooltip title={this.state.copied || 'Copy'}>
|
||||
<Button
|
||||
icon="copy"
|
||||
type="dashed"
|
||||
size="small"
|
||||
onClick={this.copy}
|
||||
/>
|
||||
<Tooltip title={this.state.copied || "Copy"}>
|
||||
<Button icon="copy" type="dashed" size="small" onClick={this.copy} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import AntCollapse from 'antd/lib/collapse';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import AntCollapse from "antd/lib/collapse";
|
||||
|
||||
export default function Collapse({ collapsed, children, className, ...props }) {
|
||||
return (
|
||||
<AntCollapse {...props} activeKey={collapsed ? null : 'content'} className={cx(className, 'ant-collapse-headerless')}>
|
||||
<AntCollapse.Panel key="content" header="">{children}</AntCollapse.Panel>
|
||||
<AntCollapse
|
||||
{...props}
|
||||
activeKey={collapsed ? null : "content"}
|
||||
className={cx(className, "ant-collapse-headerless")}>
|
||||
<AntCollapse.Panel key="content" header="">
|
||||
{children}
|
||||
</AntCollapse.Panel>
|
||||
</AntCollapse>
|
||||
);
|
||||
}
|
||||
@@ -20,5 +25,5 @@ Collapse.propTypes = {
|
||||
Collapse.defaultProps = {
|
||||
collapsed: true,
|
||||
children: null,
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// ANGULAR_REMOVE_ME
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
import ColorPicker from '@/components/ColorPicker';
|
||||
|
||||
import './color-box.less';
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('colorBox', react2angular(ColorPicker.Swatch));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
@@ -1,12 +1,12 @@
|
||||
import { isNil, isArray, chunk, map, filter, toPairs } from 'lodash';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import TextInput from 'antd/lib/input';
|
||||
import Typography from 'antd/lib/typography';
|
||||
import Swatch from './Swatch';
|
||||
import { isNil, isArray, chunk, map, filter, toPairs } from "lodash";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import tinycolor from "tinycolor2";
|
||||
import TextInput from "antd/lib/input";
|
||||
import Typography from "antd/lib/typography";
|
||||
import Swatch from "./Swatch";
|
||||
|
||||
import './input.less';
|
||||
import "./input.less";
|
||||
|
||||
function preparePresets(presetColors, presetColumns) {
|
||||
presetColors = isArray(presetColors) ? map(presetColors, v => [null, v]) : toPairs(presetColors);
|
||||
@@ -16,14 +16,14 @@ function preparePresets(presetColors, presetColumns) {
|
||||
}
|
||||
value = tinycolor(value);
|
||||
if (value.isValid()) {
|
||||
return [title, '#' + value.toHex().toUpperCase()];
|
||||
return [title, "#" + value.toHex().toUpperCase()];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return chunk(filter(presetColors), presetColumns);
|
||||
}
|
||||
|
||||
function validateColor(value, callback, prefix = '#') {
|
||||
function validateColor(value, callback, prefix = "#") {
|
||||
if (isNil(value)) {
|
||||
callback(null);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ function validateColor(value, callback, prefix = '#') {
|
||||
}
|
||||
|
||||
export default function Input({ color, presetColors, presetColumns, onChange, onPressEnter }) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
|
||||
const presets = preparePresets(presetColors, presetColumns);
|
||||
@@ -46,7 +46,7 @@ export default function Input({ color, presetColors, presetColumns, onChange, on
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInputFocused) {
|
||||
validateColor(color, setInputValue, '');
|
||||
validateColor(color, setInputValue, "");
|
||||
}
|
||||
}, [color, isInputFocused]);
|
||||
|
||||
@@ -61,6 +61,7 @@ export default function Input({ color, presetColors, presetColumns, onChange, on
|
||||
))}
|
||||
<div className="color-picker-input">
|
||||
<TextInput
|
||||
data-test="ColorPicker.CustomColor"
|
||||
addonBefore={<Typography.Text type="secondary">#</Typography.Text>}
|
||||
value={inputValue}
|
||||
onChange={e => handleInputChange(e.target.value)}
|
||||
@@ -85,7 +86,7 @@ Input.propTypes = {
|
||||
};
|
||||
|
||||
Input.defaultProps = {
|
||||
color: '#FFFFFF',
|
||||
color: "#FFFFFF",
|
||||
presetColors: null,
|
||||
presetColumns: 8,
|
||||
onChange: () => {},
|
||||
|
||||
31
client/app/components/ColorPicker/Label.jsx
Normal file
31
client/app/components/ColorPicker/Label.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { useMemo } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
|
||||
import { validateColor, getColorName } from "./utils";
|
||||
import "./label.less";
|
||||
|
||||
export default function Label({ className, color, presetColors, ...props }) {
|
||||
const name = useMemo(() => getColorName(validateColor(color), presetColors), [color, presetColors]);
|
||||
|
||||
return (
|
||||
<span className={cx("color-label", className)} {...props}>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Label.propTypes = {
|
||||
className: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
presetColors: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
|
||||
PropTypes.objectOf(PropTypes.string), // color name => color value
|
||||
]),
|
||||
};
|
||||
|
||||
Label.defaultProps = {
|
||||
className: null,
|
||||
color: "#FFFFFF",
|
||||
presetColors: null,
|
||||
};
|
||||
@@ -1,22 +1,21 @@
|
||||
import { isString } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { isString } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
import './swatch.less';
|
||||
import "./swatch.less";
|
||||
|
||||
export default function Swatch({ className, color, title, size, ...props }) {
|
||||
const result = (
|
||||
<span
|
||||
className={`color-swatch ${className}`}
|
||||
style={{ backgroundColor: color, width: size }}
|
||||
{...props}
|
||||
/>
|
||||
<span className={cx("color-swatch", className)} style={{ backgroundColor: color, width: size }} {...props} />
|
||||
);
|
||||
|
||||
if (isString(title) && (title !== '')) {
|
||||
if (isString(title) && title !== "") {
|
||||
return (
|
||||
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>{result}</Tooltip>
|
||||
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
{result}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return result;
|
||||
@@ -30,8 +29,8 @@ Swatch.propTypes = {
|
||||
};
|
||||
|
||||
Swatch.defaultProps = {
|
||||
className: '',
|
||||
className: null,
|
||||
title: null,
|
||||
color: 'transparent',
|
||||
color: "transparent",
|
||||
size: 12,
|
||||
};
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import { toString } from 'lodash';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Card from 'antd/lib/card';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { toString } from "lodash";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import Popover from "antd/lib/popover";
|
||||
import Card from "antd/lib/card";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import Icon from "antd/lib/icon";
|
||||
import chooseTextColorForBackground from "@/lib/chooseTextColorForBackground";
|
||||
|
||||
import ColorInput from './Input';
|
||||
import Swatch from './Swatch';
|
||||
import ColorInput from "./Input";
|
||||
import Swatch from "./Swatch";
|
||||
import Label from "./Label";
|
||||
import { validateColor } from "./utils";
|
||||
|
||||
import './index.less';
|
||||
|
||||
function validateColor(value, fallback = null) {
|
||||
value = tinycolor(value);
|
||||
return value.isValid() ? '#' + value.toHex().toUpperCase() : fallback;
|
||||
}
|
||||
import "./index.less";
|
||||
|
||||
export default function ColorPicker({
|
||||
color, placement, presetColors, presetColumns, triggerSize, interactive, children, onChange,
|
||||
color,
|
||||
placement,
|
||||
presetColors,
|
||||
presetColumns,
|
||||
interactive,
|
||||
children,
|
||||
onChange,
|
||||
triggerProps,
|
||||
addonBefore,
|
||||
addonAfter,
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentColor, setCurrentColor] = useState('');
|
||||
const validatedColor = useMemo(() => validateColor(color), [color]);
|
||||
const [currentColor, setCurrentColor] = useState("");
|
||||
|
||||
function handleApply() {
|
||||
setVisible(false);
|
||||
@@ -36,16 +44,16 @@ export default function ColorPicker({
|
||||
|
||||
const actions = [];
|
||||
if (!interactive) {
|
||||
actions.push((
|
||||
actions.push(
|
||||
<Tooltip key="cancel" title="Cancel">
|
||||
<Icon type="close" onClick={handleCancel} />
|
||||
</Tooltip>
|
||||
));
|
||||
actions.push((
|
||||
);
|
||||
actions.push(
|
||||
<Tooltip key="apply" title="Apply">
|
||||
<Icon type="check" onClick={handleApply} />
|
||||
</Tooltip>
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
function handleInputChange(newColor) {
|
||||
@@ -57,72 +65,97 @@ export default function ColorPicker({
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setCurrentColor(validateColor(color));
|
||||
setCurrentColor(validatedColor);
|
||||
}
|
||||
}, [color, visible]);
|
||||
}, [validatedColor, visible]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
|
||||
overlayStyle={{ '--color-picker-selected-color': currentColor }}
|
||||
content={(
|
||||
<Card
|
||||
className="color-picker-panel"
|
||||
bordered={false}
|
||||
title={toString(currentColor).toUpperCase()}
|
||||
headStyle={{
|
||||
backgroundColor: currentColor,
|
||||
color: tinycolor(currentColor).isLight() ? '#000000' : '#ffffff',
|
||||
}}
|
||||
actions={actions}
|
||||
>
|
||||
<ColorInput
|
||||
color={currentColor}
|
||||
presetColors={presetColors}
|
||||
presetColumns={presetColumns}
|
||||
onChange={handleInputChange}
|
||||
onPressEnter={handleApply}
|
||||
<React.Fragment>
|
||||
{addonBefore}
|
||||
<Popover
|
||||
arrowPointAtCenter
|
||||
overlayClassName={`color-picker ${interactive ? "color-picker-interactive" : "color-picker-with-actions"}`}
|
||||
overlayStyle={{ "--color-picker-selected-color": currentColor }}
|
||||
content={
|
||||
<Card
|
||||
data-test="ColorPicker"
|
||||
className="color-picker-panel"
|
||||
bordered={false}
|
||||
title={toString(currentColor).toUpperCase()}
|
||||
headStyle={{
|
||||
backgroundColor: currentColor,
|
||||
color: chooseTextColorForBackground(currentColor),
|
||||
}}
|
||||
actions={actions}>
|
||||
<ColorInput
|
||||
color={currentColor}
|
||||
presetColors={presetColors}
|
||||
presetColumns={presetColumns}
|
||||
onChange={handleInputChange}
|
||||
onPressEnter={handleApply}
|
||||
/>
|
||||
</Card>
|
||||
}
|
||||
trigger="click"
|
||||
placement={placement}
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}>
|
||||
{children || (
|
||||
<Swatch
|
||||
color={validatedColor}
|
||||
size={30}
|
||||
{...triggerProps}
|
||||
className={cx("color-picker-trigger", triggerProps.className)}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
trigger="click"
|
||||
placement={placement}
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
>
|
||||
{children || (<Swatch className="color-picker-trigger" color={validateColor(color)} size={triggerSize} />)}
|
||||
</Popover>
|
||||
)}
|
||||
</Popover>
|
||||
{addonAfter}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
ColorPicker.propTypes = {
|
||||
color: PropTypes.string,
|
||||
placement: PropTypes.oneOf([
|
||||
'top', 'left', 'right', 'bottom',
|
||||
'topLeft', 'topRight', 'bottomLeft', 'bottomRight',
|
||||
'leftTop', 'leftBottom', 'rightTop', 'rightBottom',
|
||||
"top",
|
||||
"left",
|
||||
"right",
|
||||
"bottom",
|
||||
"topLeft",
|
||||
"topRight",
|
||||
"bottomLeft",
|
||||
"bottomRight",
|
||||
"leftTop",
|
||||
"leftBottom",
|
||||
"rightTop",
|
||||
"rightBottom",
|
||||
]),
|
||||
presetColors: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
|
||||
PropTypes.objectOf(PropTypes.string), // color name => color value
|
||||
]),
|
||||
presetColumns: PropTypes.number,
|
||||
triggerSize: PropTypes.number,
|
||||
interactive: PropTypes.bool,
|
||||
triggerProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
children: PropTypes.node,
|
||||
addonBefore: PropTypes.node,
|
||||
addonAfter: PropTypes.node,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
ColorPicker.defaultProps = {
|
||||
color: '#FFFFFF',
|
||||
placement: 'top',
|
||||
color: "#FFFFFF",
|
||||
placement: "top",
|
||||
presetColors: null,
|
||||
presetColumns: 8,
|
||||
triggerSize: 30,
|
||||
interactive: false,
|
||||
triggerProps: {},
|
||||
children: null,
|
||||
addonBefore: null,
|
||||
addonAfter: null,
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
ColorPicker.Input = ColorInput;
|
||||
ColorPicker.Swatch = Swatch;
|
||||
ColorPicker.Label = Label;
|
||||
|
||||
7
client/app/components/ColorPicker/label.less
Normal file
7
client/app/components/ColorPicker/label.less
Normal file
@@ -0,0 +1,7 @@
|
||||
.color-label {
|
||||
vertical-align: middle;
|
||||
|
||||
.color-swatch + & {
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
14
client/app/components/ColorPicker/utils.js
Normal file
14
client/app/components/ColorPicker/utils.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { isArray, findKey } from "lodash";
|
||||
import tinycolor from "tinycolor2";
|
||||
|
||||
export function validateColor(value, fallback = null) {
|
||||
value = tinycolor(value);
|
||||
return value.isValid() ? "#" + value.toHex().toUpperCase() : fallback;
|
||||
}
|
||||
|
||||
export function getColorName(color, presetColors) {
|
||||
if (isArray(presetColors)) {
|
||||
return color;
|
||||
}
|
||||
return findKey(presetColors, v => validateColor(v) === color) || color;
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isEmpty, toUpper, includes } from 'lodash';
|
||||
import Button from 'antd/lib/button';
|
||||
import List from 'antd/lib/list';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Input from 'antd/lib/input';
|
||||
import Steps from 'antd/lib/steps';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { PreviewCard } from '@/components/PreviewCard';
|
||||
import EmptyState from '@/components/items-list/components/EmptyState';
|
||||
import DynamicForm from '@/components/dynamic-form/DynamicForm';
|
||||
import helper from '@/components/dynamic-form/dynamicFormHelper';
|
||||
import HelpTrigger, { TYPES as HELP_TRIGGER_TYPES } from '@/components/HelpTrigger';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { isEmpty, toUpper, includes } from "lodash";
|
||||
import Button from "antd/lib/button";
|
||||
import List from "antd/lib/list";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Input from "antd/lib/input";
|
||||
import Steps from "antd/lib/steps";
|
||||
import { getErrorMessage } from "@/components/ApplicationArea/ErrorMessage";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import { PreviewCard } from "@/components/PreviewCard";
|
||||
import EmptyState from "@/components/items-list/components/EmptyState";
|
||||
import DynamicForm from "@/components/dynamic-form/DynamicForm";
|
||||
import helper from "@/components/dynamic-form/dynamicFormHelper";
|
||||
import HelpTrigger, { TYPES as HELP_TRIGGER_TYPES } from "@/components/HelpTrigger";
|
||||
|
||||
const { Step } = Steps;
|
||||
const { Search } = Input;
|
||||
@@ -38,19 +39,19 @@ class CreateSourceDialog extends React.Component {
|
||||
};
|
||||
|
||||
state = {
|
||||
searchText: '',
|
||||
searchText: "",
|
||||
selectedType: null,
|
||||
savingSource: false,
|
||||
currentStep: StepEnum.SELECT_TYPE,
|
||||
};
|
||||
|
||||
selectType = (selectedType) => {
|
||||
selectType = selectedType => {
|
||||
this.setState({ selectedType, currentStep: StepEnum.CONFIGURE_IT });
|
||||
};
|
||||
|
||||
resetType = () => {
|
||||
if (this.state.currentStep === StepEnum.CONFIGURE_IT) {
|
||||
this.setState({ searchText: '', selectedType: null, currentStep: StepEnum.SELECT_TYPE });
|
||||
this.setState({ searchText: "", selectedType: null, currentStep: StepEnum.SELECT_TYPE });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,21 +59,25 @@ class CreateSourceDialog extends React.Component {
|
||||
const { selectedType, savingSource } = this.state;
|
||||
if (!savingSource) {
|
||||
this.setState({ savingSource: true, currentStep: StepEnum.DONE });
|
||||
this.props.onCreate(selectedType, values).then((data) => {
|
||||
successCallback('Saved.');
|
||||
this.props.dialog.close({ success: true, data });
|
||||
}).catch((error) => {
|
||||
this.setState({ savingSource: false, currentStep: StepEnum.CONFIGURE_IT });
|
||||
errorCallback(error.message);
|
||||
});
|
||||
this.props
|
||||
.onCreate(selectedType, values)
|
||||
.then(data => {
|
||||
successCallback("Saved.");
|
||||
this.props.dialog.close({ success: true, data });
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({ savingSource: false, currentStep: StepEnum.CONFIGURE_IT });
|
||||
errorCallback(getErrorMessage(error.message));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderTypeSelector() {
|
||||
const { types } = this.props;
|
||||
const { searchText } = this.state;
|
||||
const filteredTypes = types.filter(type => isEmpty(searchText) ||
|
||||
includes(type.name.toLowerCase(), searchText.toLowerCase()));
|
||||
const filteredTypes = types.filter(
|
||||
type => isEmpty(searchText) || includes(type.name.toLowerCase(), searchText.toLowerCase())
|
||||
);
|
||||
return (
|
||||
<div className="m-t-10">
|
||||
<Search
|
||||
@@ -81,13 +86,11 @@ class CreateSourceDialog extends React.Component {
|
||||
autoFocus
|
||||
data-test="SearchSource"
|
||||
/>
|
||||
<div className="scrollbox p-5 m-t-10" style={{ minHeight: '30vh', maxHeight: '40vh' }}>
|
||||
{isEmpty(filteredTypes) ? (<EmptyState className="" />) : (
|
||||
<List
|
||||
size="small"
|
||||
dataSource={filteredTypes}
|
||||
renderItem={item => this.renderItem(item)}
|
||||
/>
|
||||
<div className="scrollbox p-5 m-t-10" style={{ minHeight: "30vh", maxHeight: "40vh" }}>
|
||||
{isEmpty(filteredTypes) ? (
|
||||
<EmptyState className="" />
|
||||
) : (
|
||||
<List size="small" dataSource={filteredTypes} renderItem={item => this.renderItem(item)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,12 +105,7 @@ class CreateSourceDialog extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<img
|
||||
className="p-5"
|
||||
src={`${imageFolder}/${selectedType.type}.png`}
|
||||
alt={selectedType.name}
|
||||
width="48"
|
||||
/>
|
||||
<img className="p-5" src={`${imageFolder}/${selectedType.type}.png`} alt={selectedType.name} width="48" />
|
||||
<h4 className="m-0">{selectedType.name}</h4>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
@@ -117,13 +115,7 @@ class CreateSourceDialog extends React.Component {
|
||||
</HelpTrigger>
|
||||
)}
|
||||
</div>
|
||||
<DynamicForm
|
||||
id="sourceForm"
|
||||
fields={fields}
|
||||
onSubmit={this.createSource}
|
||||
feedbackIcons
|
||||
hideSubmitButton
|
||||
/>
|
||||
<DynamicForm id="sourceForm" fields={fields} onSubmit={this.createSource} feedbackIcons hideSubmitButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -131,10 +123,7 @@ class CreateSourceDialog extends React.Component {
|
||||
renderItem(item) {
|
||||
const { imageFolder } = this.props;
|
||||
return (
|
||||
<List.Item
|
||||
className="p-l-10 p-r-10 clickable"
|
||||
onClick={() => this.selectType(item)}
|
||||
>
|
||||
<List.Item className="p-l-10 p-r-10 clickable" onClick={() => this.selectType(item)}>
|
||||
<PreviewCard title={item.name} imageUrl={`${imageFolder}/${item.type}.png`} roundedImage={false}>
|
||||
<i className="fa fa-angle-double-right" />
|
||||
</PreviewCard>
|
||||
@@ -149,34 +138,38 @@ class CreateSourceDialog extends React.Component {
|
||||
<Modal
|
||||
{...dialog.props}
|
||||
title={`Create a New ${sourceType}`}
|
||||
footer={(currentStep === StepEnum.SELECT_TYPE) ? [
|
||||
(<Button key="cancel" onClick={() => dialog.dismiss()}>Cancel</Button>),
|
||||
(<Button key="submit" type="primary" disabled>Create</Button>),
|
||||
] : [
|
||||
(<Button key="previous" onClick={this.resetType}>Previous</Button>),
|
||||
(
|
||||
<Button
|
||||
key="submit"
|
||||
htmlType="submit"
|
||||
form="sourceForm"
|
||||
type="primary"
|
||||
loading={savingSource}
|
||||
data-test="CreateSourceButton"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
),
|
||||
]}
|
||||
>
|
||||
footer={
|
||||
currentStep === StepEnum.SELECT_TYPE
|
||||
? [
|
||||
<Button key="cancel" onClick={() => dialog.dismiss()}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" disabled>
|
||||
Create
|
||||
</Button>,
|
||||
]
|
||||
: [
|
||||
<Button key="previous" onClick={this.resetType}>
|
||||
Previous
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
htmlType="submit"
|
||||
form="sourceForm"
|
||||
type="primary"
|
||||
loading={savingSource}
|
||||
data-test="CreateSourceButton">
|
||||
Create
|
||||
</Button>,
|
||||
]
|
||||
}>
|
||||
<div data-test="CreateSourceDialog">
|
||||
<Steps className="hidden-xs m-b-10" size="small" current={currentStep} progressDot>
|
||||
{currentStep === StepEnum.CONFIGURE_IT ? (
|
||||
<Step
|
||||
title={<a>Type Selection</a>}
|
||||
className="clickable"
|
||||
onClick={this.resetType}
|
||||
/>
|
||||
) : (<Step title="Type Selection" />)}
|
||||
<Step title={<a>Type Selection</a>} className="clickable" onClick={this.resetType} />
|
||||
) : (
|
||||
<Step title="Type Selection" />
|
||||
)}
|
||||
<Step title="Configuration" />
|
||||
<Step title="Done" />
|
||||
</Steps>
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DatePicker from 'antd/lib/date-picker';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import DatePicker from "antd/lib/date-picker";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
|
||||
const DateInput = React.forwardRef(({
|
||||
defaultValue,
|
||||
value,
|
||||
onSelect,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const format = clientConfig.dateFormat || 'YYYY-MM-DD';
|
||||
const DateInput = React.forwardRef(({ defaultValue, value, onSelect, className, ...props }, ref) => {
|
||||
const format = clientConfig.dateFormat || "YYYY-MM-DD";
|
||||
const additionalAttributes = {};
|
||||
if (defaultValue && defaultValue.isValid()) {
|
||||
additionalAttributes.defaultValue = defaultValue;
|
||||
@@ -43,7 +37,7 @@ DateInput.defaultProps = {
|
||||
defaultValue: null,
|
||||
value: undefined,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default DateInput;
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { isArray } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DatePicker from 'antd/lib/date-picker';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import { isArray } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import DatePicker from "antd/lib/date-picker";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const DateRangeInput = React.forwardRef(({
|
||||
defaultValue,
|
||||
value,
|
||||
onSelect,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const format = clientConfig.dateFormat || 'YYYY-MM-DD';
|
||||
const DateRangeInput = React.forwardRef(({ defaultValue, value, onSelect, className, ...props }, ref) => {
|
||||
const format = clientConfig.dateFormat || "YYYY-MM-DD";
|
||||
const additionalAttributes = {};
|
||||
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
|
||||
additionalAttributes.defaultValue = defaultValue;
|
||||
@@ -45,7 +39,7 @@ DateRangeInput.defaultProps = {
|
||||
defaultValue: null,
|
||||
value: undefined,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default DateRangeInput;
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DatePicker from 'antd/lib/date-picker';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import DatePicker from "antd/lib/date-picker";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
|
||||
const DateTimeInput = React.forwardRef(({
|
||||
defaultValue,
|
||||
value,
|
||||
withSeconds,
|
||||
onSelect,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const format = (clientConfig.dateFormat || 'YYYY-MM-DD') +
|
||||
(withSeconds ? ' HH:mm:ss' : ' HH:mm');
|
||||
const DateTimeInput = React.forwardRef(({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
|
||||
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
|
||||
const additionalAttributes = {};
|
||||
if (defaultValue && defaultValue.isValid()) {
|
||||
additionalAttributes.defaultValue = defaultValue;
|
||||
@@ -48,7 +40,7 @@ DateTimeInput.defaultProps = {
|
||||
value: undefined,
|
||||
withSeconds: false,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default DateTimeInput;
|
||||
|
||||
@@ -1,41 +1,35 @@
|
||||
import { isArray } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DatePicker from 'antd/lib/date-picker';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import { isArray } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import DatePicker from "antd/lib/date-picker";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const DateTimeRangeInput = React.forwardRef(({
|
||||
defaultValue,
|
||||
value,
|
||||
withSeconds,
|
||||
onSelect,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const format = (clientConfig.dateFormat || 'YYYY-MM-DD') +
|
||||
(withSeconds ? ' HH:mm:ss' : ' HH:mm');
|
||||
const additionalAttributes = {};
|
||||
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
|
||||
additionalAttributes.defaultValue = defaultValue;
|
||||
const DateTimeRangeInput = React.forwardRef(
|
||||
({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
|
||||
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
|
||||
const additionalAttributes = {};
|
||||
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
|
||||
additionalAttributes.defaultValue = defaultValue;
|
||||
}
|
||||
if (value === null || (isArray(value) && value[0].isValid() && value[1].isValid())) {
|
||||
additionalAttributes.value = value;
|
||||
}
|
||||
return (
|
||||
<RangePicker
|
||||
ref={ref}
|
||||
className={className}
|
||||
showTime
|
||||
{...additionalAttributes}
|
||||
format={format}
|
||||
onChange={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (value === null || (isArray(value) && value[0].isValid() && value[1].isValid())) {
|
||||
additionalAttributes.value = value;
|
||||
}
|
||||
return (
|
||||
<RangePicker
|
||||
ref={ref}
|
||||
className={className}
|
||||
showTime
|
||||
{...additionalAttributes}
|
||||
format={format}
|
||||
onChange={onSelect}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
);
|
||||
|
||||
DateTimeRangeInput.propTypes = {
|
||||
defaultValue: PropTypes.arrayOf(Moment),
|
||||
@@ -50,7 +44,7 @@ DateTimeRangeInput.defaultProps = {
|
||||
value: undefined,
|
||||
withSeconds: false,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default DateTimeRangeInput;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isFunction } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { isFunction } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
/**
|
||||
Wrapper for dialogs based on Ant's <Modal> component.
|
||||
@@ -140,7 +140,7 @@ function openDialog(DialogComponent, props) {
|
||||
reject: () => {},
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
|
||||
function render() {
|
||||
@@ -176,7 +176,7 @@ function openDialog(DialogComponent, props) {
|
||||
const result = {
|
||||
close: closeDialog,
|
||||
dismiss: dismissDialog,
|
||||
update: (newProps) => {
|
||||
update: newProps => {
|
||||
props = { ...props, ...newProps };
|
||||
render();
|
||||
},
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { isFunction, isString } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isFunction, isString } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const componentsRegistry = new Map();
|
||||
const activeInstances = new Set();
|
||||
|
||||
export function registerComponent(name, component) {
|
||||
if (isString(name) && name !== '') {
|
||||
if (isString(name) && name !== "") {
|
||||
componentsRegistry.set(name, isFunction(component) ? component : null);
|
||||
// Refresh active DynamicComponent instances which use this component
|
||||
activeInstances.forEach((dynamicComponent) => {
|
||||
activeInstances.forEach(dynamicComponent => {
|
||||
if (dynamicComponent.props.name === name) {
|
||||
dynamicComponent.forceUpdate();
|
||||
}
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { trim } from 'lodash';
|
||||
import { trim } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import Input from "antd/lib/input";
|
||||
|
||||
export class EditInPlace extends React.Component {
|
||||
export default class EditInPlace extends React.Component {
|
||||
static propTypes = {
|
||||
ignoreBlanks: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
editor: PropTypes.string.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onDone: PropTypes.func.isRequired,
|
||||
multiline: PropTypes.bool,
|
||||
editorProps: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
ignoreBlanks: false,
|
||||
isEditable: true,
|
||||
placeholder: '',
|
||||
value: '',
|
||||
placeholder: "",
|
||||
value: "",
|
||||
multiline: false,
|
||||
editorProps: {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -26,12 +30,12 @@ export class EditInPlace extends React.Component {
|
||||
editing: false,
|
||||
};
|
||||
this.inputRef = React.createRef();
|
||||
const self = this;
|
||||
this.componentDidUpdate = (prevProps, prevState) => {
|
||||
if (self.state.editing && !prevState.editing) {
|
||||
self.inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(_, prevState) {
|
||||
if (this.state.editing && !prevState.editing) {
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
startEditing = () => {
|
||||
@@ -40,19 +44,19 @@ export class EditInPlace extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
stopEditing = () => {
|
||||
const newValue = trim(this.inputRef.current.value);
|
||||
const ignorableBlank = this.props.ignoreBlanks && newValue === '';
|
||||
stopEditing = currentValue => {
|
||||
const newValue = trim(currentValue);
|
||||
const ignorableBlank = this.props.ignoreBlanks && newValue === "";
|
||||
if (!ignorableBlank && newValue !== this.props.value) {
|
||||
this.props.onDone(newValue);
|
||||
}
|
||||
this.setState({ editing: false });
|
||||
};
|
||||
|
||||
keyDown = (event) => {
|
||||
handleKeyDown = event => {
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
this.stopEditing();
|
||||
this.stopEditing(event.target.value);
|
||||
} else if (event.keyCode === 27) {
|
||||
this.setState({ editing: false });
|
||||
}
|
||||
@@ -63,31 +67,30 @@ export class EditInPlace extends React.Component {
|
||||
role="presentation"
|
||||
onFocus={this.startEditing}
|
||||
onClick={this.startEditing}
|
||||
className={this.props.isEditable ? 'editable' : ''}
|
||||
>
|
||||
className={this.props.isEditable ? "editable" : ""}>
|
||||
{this.props.value || this.props.placeholder}
|
||||
</span>
|
||||
);
|
||||
|
||||
renderEdit = () => React.createElement(this.props.editor, {
|
||||
ref: this.inputRef,
|
||||
className: 'rd-form-control',
|
||||
defaultValue: this.props.value,
|
||||
onBlur: this.stopEditing,
|
||||
onKeyDown: this.keyDown,
|
||||
});
|
||||
renderEdit = () => {
|
||||
const { multiline, value, editorProps } = this.props;
|
||||
const InputComponent = multiline ? Input.TextArea : Input;
|
||||
return (
|
||||
<InputComponent
|
||||
ref={this.inputRef}
|
||||
defaultValue={value}
|
||||
onBlur={e => this.stopEditing(e.target.value)}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
{...editorProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className={'edit-in-place' + (this.state.editing ? ' active' : '')}>
|
||||
<span className={cx("edit-in-place", { active: this.state.editing }, this.props.className)}>
|
||||
{this.state.editing ? this.renderEdit() : this.renderNormal()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('editInPlace', react2angular(EditInPlace));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
|
||||
import { includes, words, capitalize, clone, isNull } from 'lodash';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Form from 'antd/lib/form';
|
||||
import Button from 'antd/lib/button';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import Divider from 'antd/lib/divider';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { QuerySelector } from '@/components/QuerySelector';
|
||||
import { Query } from '@/services/query';
|
||||
import { includes, words, capitalize, clone, isNull } from "lodash";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Checkbox from "antd/lib/checkbox";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Form from "antd/lib/form";
|
||||
import Button from "antd/lib/button";
|
||||
import Select from "antd/lib/select";
|
||||
import Input from "antd/lib/input";
|
||||
import Divider from "antd/lib/divider";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import QuerySelector from "@/components/QuerySelector";
|
||||
import { Query } from "@/services/query";
|
||||
|
||||
const { Option } = Select;
|
||||
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
|
||||
|
||||
function getDefaultTitle(text) {
|
||||
return capitalize(words(text).join(' ')); // humanize
|
||||
return capitalize(words(text).join(" ")); // humanize
|
||||
}
|
||||
|
||||
function isTypeDateRange(type) {
|
||||
@@ -26,30 +25,26 @@ function isTypeDateRange(type) {
|
||||
|
||||
function joinExampleList(multiValuesOptions) {
|
||||
const { prefix, suffix } = multiValuesOptions;
|
||||
return ['value1', 'value2', 'value3']
|
||||
.map(value => `${prefix}${value}${suffix}`)
|
||||
.join(',');
|
||||
return ["value1", "value2", "value3"].map(value => `${prefix}${value}${suffix}`).join(",");
|
||||
}
|
||||
|
||||
function NameInput({ name, type, onChange, existingNames, setValidation }) {
|
||||
let helpText = '';
|
||||
let validateStatus = '';
|
||||
let helpText = "";
|
||||
let validateStatus = "";
|
||||
|
||||
if (!name) {
|
||||
helpText = 'Choose a keyword for this parameter';
|
||||
helpText = "Choose a keyword for this parameter";
|
||||
setValidation(false);
|
||||
} else if (includes(existingNames, name)) {
|
||||
helpText = 'Parameter with this name already exists';
|
||||
helpText = "Parameter with this name already exists";
|
||||
setValidation(false);
|
||||
validateStatus = 'error';
|
||||
validateStatus = "error";
|
||||
} else {
|
||||
if (isTypeDateRange(type)) {
|
||||
helpText = (
|
||||
<React.Fragment>
|
||||
Appears in query as {' '}
|
||||
<code style={{ display: 'inline-block', color: 'inherit' }}>
|
||||
{`{{${name}.start}} {{${name}.end}}`}
|
||||
</code>
|
||||
Appears in query as{" "}
|
||||
<code style={{ display: "inline-block", color: "inherit" }}>{`{{${name}.start}} {{${name}.end}}`}</code>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -57,13 +52,7 @@ function NameInput({ name, type, onChange, existingNames, setValidation }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
required
|
||||
label="Keyword"
|
||||
help={helpText}
|
||||
validateStatus={validateStatus}
|
||||
{...formItemProps}
|
||||
>
|
||||
<Form.Item required label="Keyword" help={helpText} validateStatus={validateStatus} {...formItemProps}>
|
||||
<Input onChange={e => onChange(e.target.value)} autoFocus />
|
||||
</Form.Item>
|
||||
);
|
||||
@@ -86,13 +75,11 @@ function EditParameterSettingsDialog(props) {
|
||||
|
||||
// fetch query by id
|
||||
useEffect(() => {
|
||||
const { queryId } = props.parameter;
|
||||
const queryId = props.parameter.queryId;
|
||||
if (queryId) {
|
||||
Query.get({ id: queryId }, (query) => {
|
||||
setInitialQuery(query);
|
||||
});
|
||||
Query.get({ id: queryId }).then(setInitialQuery);
|
||||
}
|
||||
}, []);
|
||||
}, [props.parameter.queryId]);
|
||||
|
||||
function isFulfilled() {
|
||||
// name
|
||||
@@ -101,12 +88,12 @@ function EditParameterSettingsDialog(props) {
|
||||
}
|
||||
|
||||
// title
|
||||
if (param.title === '') {
|
||||
if (param.title === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// query
|
||||
if (param.type === 'query' && !param.queryId) {
|
||||
if (param.type === "query" && !param.queryId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -129,16 +116,22 @@ function EditParameterSettingsDialog(props) {
|
||||
return (
|
||||
<Modal
|
||||
{...props.dialog.props}
|
||||
title={isNew ? 'Add Parameter' : param.name}
|
||||
title={isNew ? "Add Parameter" : param.name}
|
||||
width={600}
|
||||
footer={[(
|
||||
<Button key="cancel" onClick={props.dialog.dismiss}>Cancel</Button>
|
||||
), (
|
||||
<Button key="submit" htmlType="submit" disabled={!isFulfilled()} type="primary" form="paramForm" data-test="SaveParameterSettings">
|
||||
{isNew ? 'Add Parameter' : 'OK'}
|
||||
</Button>
|
||||
)]}
|
||||
>
|
||||
footer={[
|
||||
<Button key="cancel" onClick={props.dialog.dismiss}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
htmlType="submit"
|
||||
disabled={!isFulfilled()}
|
||||
type="primary"
|
||||
form="paramForm"
|
||||
data-test="SaveParameterSettings">
|
||||
{isNew ? "Add Parameter" : "OK"}
|
||||
</Button>,
|
||||
]}>
|
||||
<Form layout="horizontal" onSubmit={onConfirm} id="paramForm">
|
||||
{isNew && (
|
||||
<NameInput
|
||||
@@ -158,25 +151,35 @@ function EditParameterSettingsDialog(props) {
|
||||
</Form.Item>
|
||||
<Form.Item label="Type" {...formItemProps}>
|
||||
<Select value={param.type} onChange={type => setParam({ ...param, type })} data-test="ParameterTypeSelect">
|
||||
<Option value="text" data-test="TextParameterTypeOption">Text</Option>
|
||||
<Option value="number" data-test="NumberParameterTypeOption">Number</Option>
|
||||
<Option value="text" data-test="TextParameterTypeOption">
|
||||
Text
|
||||
</Option>
|
||||
<Option value="number" data-test="NumberParameterTypeOption">
|
||||
Number
|
||||
</Option>
|
||||
<Option value="enum">Dropdown List</Option>
|
||||
<Option value="query">Query Based Dropdown List</Option>
|
||||
<Option disabled key="dv1">
|
||||
<Divider className="select-option-divider" />
|
||||
</Option>
|
||||
<Option value="date" data-test="DateParameterTypeOption">Date</Option>
|
||||
<Option value="datetime-local" data-test="DateTimeParameterTypeOption">Date and Time</Option>
|
||||
<Option value="date" data-test="DateParameterTypeOption">
|
||||
Date
|
||||
</Option>
|
||||
<Option value="datetime-local" data-test="DateTimeParameterTypeOption">
|
||||
Date and Time
|
||||
</Option>
|
||||
<Option value="datetime-with-seconds">Date and Time (with seconds)</Option>
|
||||
<Option disabled key="dv2">
|
||||
<Divider className="select-option-divider" />
|
||||
</Option>
|
||||
<Option value="date-range" data-test="DateRangeParameterTypeOption">Date Range</Option>
|
||||
<Option value="date-range" data-test="DateRangeParameterTypeOption">
|
||||
Date Range
|
||||
</Option>
|
||||
<Option value="datetime-range">Date and Time Range</Option>
|
||||
<Option value="datetime-range-with-seconds">Date and Time Range (with seconds)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{param.type === 'enum' && (
|
||||
{param.type === "enum" && (
|
||||
<Form.Item label="Values" help="Dropdown list values (newline delimited)" {...formItemProps}>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
@@ -185,7 +188,7 @@ function EditParameterSettingsDialog(props) {
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{param.type === 'query' && (
|
||||
{param.type === "query" && (
|
||||
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
|
||||
<QuerySelector
|
||||
selectedQuery={initialQuery}
|
||||
@@ -194,45 +197,54 @@ function EditParameterSettingsDialog(props) {
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{(param.type === 'enum' || param.type === 'query') && (
|
||||
{(param.type === "enum" || param.type === "query") && (
|
||||
<Form.Item className="m-b-0" label=" " colon={false} {...formItemProps}>
|
||||
<Checkbox
|
||||
defaultChecked={!!param.multiValuesOptions}
|
||||
onChange={e => setParam({ ...param,
|
||||
multiValuesOptions: e.target.checked ? {
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
separator: ',',
|
||||
} : null })}
|
||||
data-test="AllowMultipleValuesCheckbox"
|
||||
>
|
||||
Allow multiple values
|
||||
onChange={e =>
|
||||
setParam({
|
||||
...param,
|
||||
multiValuesOptions: e.target.checked
|
||||
? {
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
separator: ",",
|
||||
}
|
||||
: null,
|
||||
})
|
||||
}
|
||||
data-test="AllowMultipleValuesCheckbox">
|
||||
Allow multiple values
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
)}
|
||||
{(param.type === 'enum' || param.type === 'query') && param.multiValuesOptions && (
|
||||
{(param.type === "enum" || param.type === "query") && param.multiValuesOptions && (
|
||||
<Form.Item
|
||||
label="Quotation"
|
||||
help={(
|
||||
help={
|
||||
<React.Fragment>
|
||||
Placed in query as: <code>{joinExampleList(param.multiValuesOptions)}</code>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{...formItemProps}
|
||||
>
|
||||
}
|
||||
{...formItemProps}>
|
||||
<Select
|
||||
value={param.multiValuesOptions.prefix}
|
||||
onChange={quoteOption => setParam({ ...param,
|
||||
multiValuesOptions: {
|
||||
...param.multiValuesOptions,
|
||||
prefix: quoteOption,
|
||||
suffix: quoteOption,
|
||||
} })}
|
||||
data-test="QuotationSelect"
|
||||
>
|
||||
onChange={quoteOption =>
|
||||
setParam({
|
||||
...param,
|
||||
multiValuesOptions: {
|
||||
...param.multiValuesOptions,
|
||||
prefix: quoteOption,
|
||||
suffix: quoteOption,
|
||||
},
|
||||
})
|
||||
}
|
||||
data-test="QuotationSelect">
|
||||
<Option value="">None (default)</Option>
|
||||
<Option value="'">Single Quotation Mark</Option>
|
||||
<Option value={'"'} data-test="DoubleQuotationMarkOption">Double Quotation Mark</Option>
|
||||
<Option value={'"'} data-test="DoubleQuotationMarkOption">
|
||||
Double Quotation Mark
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Menu from "antd/lib/menu";
|
||||
import Button from "antd/lib/button";
|
||||
import Icon from "antd/lib/icon";
|
||||
|
||||
import QueryResultsLink from './QueryResultsLink';
|
||||
import QueryResultsLink from "./QueryResultsLink";
|
||||
|
||||
|
||||
export function QueryControlDropdown(props) {
|
||||
export default function QueryControlDropdown(props) {
|
||||
const menu = (
|
||||
<Menu>
|
||||
{!props.query.isNew() && (!props.query.is_draft || !props.query.is_archived) && (
|
||||
@@ -28,15 +26,26 @@ export function QueryControlDropdown(props) {
|
||||
)}
|
||||
<Menu.Item>
|
||||
<QueryResultsLink
|
||||
fileType="csv"
|
||||
disabled={props.queryExecuting || !props.queryResult.getData || !props.queryResult.getData()}
|
||||
query={props.query}
|
||||
queryResult={props.queryResult}
|
||||
embed={props.embed}
|
||||
apiKey={props.apiKey}
|
||||
>
|
||||
apiKey={props.apiKey}>
|
||||
<Icon type="file" /> Download as CSV File
|
||||
</QueryResultsLink>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<QueryResultsLink
|
||||
fileType="tsv"
|
||||
disabled={props.queryExecuting || !props.queryResult.getData || !props.queryResult.getData()}
|
||||
query={props.query}
|
||||
queryResult={props.queryResult}
|
||||
embed={props.embed}
|
||||
apiKey={props.apiKey}>
|
||||
<Icon type="file" /> Download as TSV File
|
||||
</QueryResultsLink>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<QueryResultsLink
|
||||
fileType="xlsx"
|
||||
@@ -44,8 +53,7 @@ export function QueryControlDropdown(props) {
|
||||
query={props.query}
|
||||
queryResult={props.queryResult}
|
||||
embed={props.embed}
|
||||
apiKey={props.apiKey}
|
||||
>
|
||||
apiKey={props.apiKey}>
|
||||
<Icon type="file-excel" /> Download as Excel File
|
||||
</QueryResultsLink>
|
||||
</Menu.Item>
|
||||
@@ -53,11 +61,7 @@ export function QueryControlDropdown(props) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={menu}
|
||||
overlayClassName="query-control-dropdown-overlay"
|
||||
>
|
||||
<Dropdown trigger={["click"]} overlay={menu} overlayClassName="query-control-dropdown-overlay">
|
||||
<Button data-test="QueryControlDropdownButton">
|
||||
<Icon type="ellipsis" rotate={90} />
|
||||
</Button>
|
||||
@@ -72,22 +76,13 @@ QueryControlDropdown.propTypes = {
|
||||
showEmbedDialog: PropTypes.func.isRequired,
|
||||
embed: PropTypes.bool,
|
||||
apiKey: PropTypes.string,
|
||||
selectedTab: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
selectedTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
openAddToDashboardForm: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
QueryControlDropdown.defaultProps = {
|
||||
queryResult: {},
|
||||
embed: false,
|
||||
apiKey: '',
|
||||
selectedTab: '',
|
||||
apiKey: "",
|
||||
selectedTab: "",
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('queryControlDropdown', react2angular(QueryControlDropdown));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default function QueryResultsLink(props) {
|
||||
let href = '';
|
||||
let href = "";
|
||||
|
||||
const { query, queryResult, fileType } = props;
|
||||
const resultId = queryResult.getId && queryResult.getId();
|
||||
@@ -11,9 +10,7 @@ export default function QueryResultsLink(props) {
|
||||
|
||||
if (resultId && resultData && query.name) {
|
||||
if (query.id) {
|
||||
href = `api/queries/${query.id}/results/${resultId}.${fileType}${
|
||||
props.embed ? `?api_key=${props.apiKey}` : ''
|
||||
}`;
|
||||
href = `api/queries/${query.id}/results/${resultId}.${fileType}${props.embed ? `?api_key=${props.apiKey}` : ""}`;
|
||||
} else {
|
||||
href = `api/query_results/${resultId}.${fileType}`;
|
||||
}
|
||||
@@ -33,15 +30,12 @@ QueryResultsLink.propTypes = {
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
embed: PropTypes.bool,
|
||||
apiKey: PropTypes.string,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
|
||||
};
|
||||
|
||||
QueryResultsLink.defaultProps = {
|
||||
queryResult: {},
|
||||
fileType: 'csv',
|
||||
fileType: "csv",
|
||||
embed: false,
|
||||
apiKey: '',
|
||||
apiKey: "",
|
||||
};
|
||||
|
||||
@@ -1,39 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import Icon from "antd/lib/icon";
|
||||
|
||||
|
||||
export function EditVisualizationButton(props) {
|
||||
export default function EditVisualizationButton(props) {
|
||||
return (
|
||||
<Button
|
||||
data-test="EditVisualization"
|
||||
className="edit-visualization"
|
||||
onClick={() => props.openVisualizationEditor(props.selectedTab)}
|
||||
>
|
||||
onClick={() => props.openVisualizationEditor(props.selectedTab)}>
|
||||
<Icon type="form" />
|
||||
<span className="hidden-xs hidden-s hidden-m">
|
||||
Edit Visualization
|
||||
</span>
|
||||
<span className="hidden-xs hidden-s hidden-m">Edit Visualization</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
EditVisualizationButton.propTypes = {
|
||||
openVisualizationEditor: PropTypes.func.isRequired,
|
||||
selectedTab: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
selectedTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
EditVisualizationButton.defaultProps = {
|
||||
selectedTab: '',
|
||||
selectedTab: "",
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('editVisualizationButton', react2angular(EditVisualizationButton));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { clientConfig, currentUser } from '@/services/auth';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Alert from 'antd/lib/alert';
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import { clientConfig, currentUser } from "@/services/auth";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import Alert from "antd/lib/alert";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
|
||||
export default function EmailSettingsWarning({ featureName, className, mode, adminOnly }) {
|
||||
if (!clientConfig.mailSettingsMissing) {
|
||||
@@ -17,33 +17,31 @@ export default function EmailSettingsWarning({ featureName, className, mode, adm
|
||||
|
||||
const message = (
|
||||
<span>
|
||||
Your mail server isn't configured correctly, and is needed for {featureName} to work.{' '}
|
||||
Your mail server isn't configured correctly, and is needed for {featureName} to work.{" "}
|
||||
<HelpTrigger type="MAIL_CONFIG" className="f-inherit" />
|
||||
</span>
|
||||
);
|
||||
|
||||
if (mode === 'icon') {
|
||||
if (mode === "icon") {
|
||||
return (
|
||||
<Tooltip title={message}>
|
||||
<i className={cx('fa fa-exclamation-triangle', className)} />
|
||||
<i className={cx("fa fa-exclamation-triangle", className)} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert message={message} type="error" className={className} />
|
||||
);
|
||||
return <Alert message={message} type="error" className={className} />;
|
||||
}
|
||||
|
||||
EmailSettingsWarning.propTypes = {
|
||||
featureName: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
mode: PropTypes.oneOf(['alert', 'icon']),
|
||||
mode: PropTypes.oneOf(["alert", "icon"]),
|
||||
adminOnly: PropTypes.bool,
|
||||
};
|
||||
|
||||
EmailSettingsWarning.defaultProps = {
|
||||
className: null,
|
||||
mode: 'alert',
|
||||
mode: "alert",
|
||||
adminOnly: false,
|
||||
};
|
||||
|
||||
74
client/app/components/ErrorBoundary.jsx
Normal file
74
client/app/components/ErrorBoundary.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { isFunction } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import debug from "debug";
|
||||
import Alert from "antd/lib/alert";
|
||||
|
||||
const logger = debug("redash:errors");
|
||||
|
||||
export const ErrorBoundaryContext = React.createContext({
|
||||
handleError: error => {
|
||||
// Allow calling chain to roll up, and then throw the error in global context
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
reset: () => {},
|
||||
});
|
||||
|
||||
export function ErrorMessage({ children }) {
|
||||
return <Alert message={children} type="error" showIcon />;
|
||||
}
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
ErrorMessage.defaultProps = {
|
||||
children: "Something went wrong.",
|
||||
};
|
||||
|
||||
export default class ErrorBoundary extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
renderError: PropTypes.func, // error => ReactNode
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
children: null,
|
||||
renderError: null,
|
||||
};
|
||||
|
||||
state = { error: null };
|
||||
|
||||
handleError = error => {
|
||||
this.setState(this.constructor.getDerivedStateFromError(error));
|
||||
this.componentDidCatch(error, null);
|
||||
};
|
||||
|
||||
reset = () => {
|
||||
this.setState({ error: null });
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
logger(error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { renderError, children } = this.props;
|
||||
const { error } = this.state;
|
||||
|
||||
if (error) {
|
||||
if (isFunction(renderError)) {
|
||||
return renderError(error);
|
||||
}
|
||||
return <ErrorMessage />;
|
||||
}
|
||||
|
||||
return <ErrorBoundaryContext.Provider value={this}>{children}</ErrorBoundaryContext.Provider>;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { $rootScope } from '@/services/ng';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export class FavoritesControl extends React.Component {
|
||||
export default class FavoritesControl extends React.Component {
|
||||
static propTypes = {
|
||||
item: PropTypes.shape({
|
||||
is_favorite: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
onChange: PropTypes.func,
|
||||
// Force component update when `item` changes.
|
||||
// Remove this when `react2angular` will finally go to hell
|
||||
forceUpdate: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
forceUpdate: '',
|
||||
};
|
||||
|
||||
toggleItem(event, item, callback) {
|
||||
const action = item.is_favorite ? item.$unfavorite.bind(item) : item.$favorite.bind(item);
|
||||
const action = item.is_favorite ? item.unfavorite.bind(item) : item.favorite.bind(item);
|
||||
const savedIsFavorite = item.is_favorite;
|
||||
|
||||
action().then(() => {
|
||||
item.is_favorite = !savedIsFavorite;
|
||||
this.forceUpdate();
|
||||
$rootScope.$broadcast('reloadFavorites');
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { item, onChange } = this.props;
|
||||
const icon = item.is_favorite ? 'fa fa-star' : 'fa fa-star-o';
|
||||
const title = item.is_favorite ? 'Remove from favorites' : 'Add to favorites';
|
||||
const icon = item.is_favorite ? "fa fa-star" : "fa fa-star-o";
|
||||
const title = item.is_favorite ? "Remove from favorites" : "Add to favorites";
|
||||
return (
|
||||
<a
|
||||
title={title}
|
||||
className="btn-favourite"
|
||||
onClick={event => this.toggleItem(event, item, onChange)}
|
||||
>
|
||||
className="favorites-control btn-favourite"
|
||||
onClick={event => this.toggleItem(event, item, onChange)}>
|
||||
<i className={icon} aria-hidden="true" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('favoritesControlImpl', react2angular(FavoritesControl));
|
||||
ngModule.component('favoritesControl', {
|
||||
template: `
|
||||
<favorites-control-impl
|
||||
ng-if="$ctrl.item"
|
||||
item="$ctrl.item"
|
||||
on-change="$ctrl.onChange"
|
||||
force-update="$ctrl.forceUpdateTag"
|
||||
></favorites-control-impl>
|
||||
`,
|
||||
bindings: {
|
||||
item: '=',
|
||||
},
|
||||
controller($scope) {
|
||||
// See comment for FavoritesControl.propTypes.forceUpdate
|
||||
this.forceUpdateTag = 'force' + Date.now();
|
||||
$scope.$on('reloadFavorites', () => {
|
||||
this.forceUpdateTag = 'force' + Date.now();
|
||||
});
|
||||
|
||||
this.onChange = () => {
|
||||
$scope.$applyAsync();
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { isArray, indexOf, get, map, includes, every, some, toNumber } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Select from 'antd/lib/select';
|
||||
import { formatColumnValue } from '@/filters';
|
||||
import { isArray, indexOf, get, map, includes, every, some, toNumber } from "lodash";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Select from "antd/lib/select";
|
||||
import { formatColumnValue } from "@/lib/utils";
|
||||
|
||||
const ALL_VALUES = '###Redash::Filters::SelectAll###';
|
||||
const NONE_VALUES = '###Redash::Filters::Clear###';
|
||||
const ALL_VALUES = "###Redash::Filters::SelectAll###";
|
||||
const NONE_VALUES = "###Redash::Filters::Clear###";
|
||||
|
||||
export const FilterType = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
friendlyName: PropTypes.string.isRequired,
|
||||
multiple: PropTypes.bool,
|
||||
current: PropTypes.oneOfType([
|
||||
PropTypes.any,
|
||||
PropTypes.arrayOf(PropTypes.any),
|
||||
]),
|
||||
current: PropTypes.oneOfType([PropTypes.any, PropTypes.arrayOf(PropTypes.any)]),
|
||||
values: PropTypes.arrayOf(PropTypes.any).isRequired,
|
||||
});
|
||||
|
||||
@@ -49,29 +45,28 @@ export function filterData(rows, filters = []) {
|
||||
|
||||
let result = rows;
|
||||
|
||||
if (isArray(filters) && (filters.length > 0)) {
|
||||
if (isArray(filters) && filters.length > 0) {
|
||||
// "every" field's value should match "some" of corresponding filter's values
|
||||
result = result.filter(row => every(
|
||||
filters,
|
||||
(filter) => {
|
||||
result = result.filter(row =>
|
||||
every(filters, filter => {
|
||||
const rowValue = row[filter.name];
|
||||
const filterValues = isArray(filter.current) ? filter.current : [filter.current];
|
||||
return some(filterValues, (filterValue) => {
|
||||
return some(filterValues, filterValue => {
|
||||
if (moment.isMoment(rowValue)) {
|
||||
return rowValue.isSame(filterValue);
|
||||
}
|
||||
// We compare with either the value or the String representation of the value,
|
||||
// because Select2 casts true/false to "true"/"false".
|
||||
return (filterValue === rowValue) || (String(rowValue) === filterValue);
|
||||
return filterValue === rowValue || String(rowValue) === filterValue;
|
||||
});
|
||||
},
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function Filters({ filters, onChange }) {
|
||||
function Filters({ filters, onChange }) {
|
||||
if (filters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -82,36 +77,45 @@ export function Filters({ filters, onChange }) {
|
||||
<div className="filters-wrapper">
|
||||
<div className="container bg-white">
|
||||
<div className="row">
|
||||
{map(filters, (filter) => {
|
||||
{map(filters, filter => {
|
||||
const options = map(filter.values, (value, index) => (
|
||||
<Select.Option key={index}>{formatColumnValue(value, get(filter, 'column.type'))}</Select.Option>
|
||||
<Select.Option key={index}>{formatColumnValue(value, get(filter, "column.type"))}</Select.Option>
|
||||
));
|
||||
|
||||
return (
|
||||
<div key={filter.name} className="col-sm-6 p-l-0 filter-container">
|
||||
<label>{filter.friendlyName}</label>
|
||||
{(options.length === 0) && (
|
||||
<Select className="w-100" disabled value="No values" />
|
||||
)}
|
||||
{(options.length > 0) && (
|
||||
{options.length === 0 && <Select className="w-100" disabled value="No values" />}
|
||||
{options.length > 0 && (
|
||||
<Select
|
||||
labelInValue
|
||||
className="w-100"
|
||||
mode={filter.multiple ? 'multiple' : 'default'}
|
||||
value={isArray(filter.current) ?
|
||||
map(filter.current,
|
||||
value => ({ key: `${indexOf(filter.values, value)}`, label: formatColumnValue(value) })) :
|
||||
({ key: `${indexOf(filter.values, filter.current)}`, label: formatColumnValue(filter.current) })}
|
||||
mode={filter.multiple ? "multiple" : "default"}
|
||||
value={
|
||||
isArray(filter.current)
|
||||
? map(filter.current, value => ({
|
||||
key: `${indexOf(filter.values, value)}`,
|
||||
label: formatColumnValue(value),
|
||||
}))
|
||||
: { key: `${indexOf(filter.values, filter.current)}`, label: formatColumnValue(filter.current) }
|
||||
}
|
||||
allowClear={filter.multiple}
|
||||
optionFilterProp="children"
|
||||
showSearch
|
||||
onChange={values => onChange(filter, values)}
|
||||
>
|
||||
onChange={values => onChange(filter, values)}>
|
||||
{!filter.multiple && options}
|
||||
{filter.multiple && [
|
||||
<Select.Option key={NONE_VALUES}><i className="fa fa-square-o m-r-5" />Clear</Select.Option>,
|
||||
<Select.Option key={ALL_VALUES}><i className="fa fa-check-square-o m-r-5" />Select All</Select.Option>,
|
||||
<Select.OptGroup key="Values" title="Values">{options}</Select.OptGroup>,
|
||||
<Select.Option key={NONE_VALUES}>
|
||||
<i className="fa fa-square-o m-r-5" />
|
||||
Clear
|
||||
</Select.Option>,
|
||||
<Select.Option key={ALL_VALUES}>
|
||||
<i className="fa fa-check-square-o m-r-5" />
|
||||
Select All
|
||||
</Select.Option>,
|
||||
<Select.OptGroup key="Values" title="Values">
|
||||
{options}
|
||||
</Select.OptGroup>,
|
||||
]}
|
||||
</Select>
|
||||
)}
|
||||
@@ -133,8 +137,4 @@ Filters.defaultProps = {
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('filters', react2angular(Filters));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default Filters;
|
||||
|
||||
@@ -1,111 +1,63 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Drawer from 'antd/lib/drawer';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { BigMessage } from '@/components/BigMessage';
|
||||
import DynamicComponent from '@/components/DynamicComponent';
|
||||
import { startsWith } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import Drawer from "antd/lib/drawer";
|
||||
import Icon from "antd/lib/icon";
|
||||
import BigMessage from "@/components/BigMessage";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
|
||||
import './HelpTrigger.less';
|
||||
import "./HelpTrigger.less";
|
||||
|
||||
const DOMAIN = 'https://redash.io';
|
||||
const HELP_PATH = '/help';
|
||||
const DOMAIN = "https://redash.io";
|
||||
const HELP_PATH = "/help";
|
||||
const IFRAME_TIMEOUT = 20000;
|
||||
const IFRAME_URL_UPDATE_MESSAGE = 'iframe_url';
|
||||
const IFRAME_URL_UPDATE_MESSAGE = "iframe_url";
|
||||
|
||||
export const TYPES = {
|
||||
HOME: [
|
||||
'',
|
||||
'Help',
|
||||
],
|
||||
VALUE_SOURCE_OPTIONS: [
|
||||
'/user-guide/querying/query-parameters#Value-Source-Options',
|
||||
'Guide: Value Source Options',
|
||||
],
|
||||
SHARE_DASHBOARD: [
|
||||
'/user-guide/dashboards/sharing-dashboards',
|
||||
'Guide: Sharing and Embedding Dashboards',
|
||||
],
|
||||
AUTHENTICATION_OPTIONS: [
|
||||
'/user-guide/users/authentication-options',
|
||||
'Guide: Authentication Options',
|
||||
],
|
||||
USAGE_DATA_SHARING: [
|
||||
'/open-source/admin-guide/usage-data',
|
||||
'Help: Anonymous Usage Data Sharing',
|
||||
],
|
||||
DS_ATHENA: [
|
||||
'/data-sources/amazon-athena-setup',
|
||||
'Guide: Help Setting up Amazon Athena',
|
||||
],
|
||||
DS_BIGQUERY: [
|
||||
'/data-sources/bigquery-setup',
|
||||
'Guide: Help Setting up BigQuery',
|
||||
],
|
||||
DS_URL: [
|
||||
'/data-sources/querying-urls',
|
||||
'Guide: Help Setting up URL',
|
||||
],
|
||||
DS_MONGODB: [
|
||||
'/data-sources/mongodb-setup',
|
||||
'Guide: Help Setting up MongoDB',
|
||||
],
|
||||
DS_GOOGLE_SPREADSHEETS: [
|
||||
'/data-sources/querying-a-google-spreadsheet',
|
||||
'Guide: Help Setting up Google Spreadsheets',
|
||||
],
|
||||
DS_GOOGLE_ANALYTICS: [
|
||||
'/data-sources/google-analytics-setup',
|
||||
'Guide: Help Setting up Google Analytics',
|
||||
],
|
||||
DS_AXIBASETSD: [
|
||||
'/data-sources/axibase-time-series-database',
|
||||
'Guide: Help Setting up Axibase Time Series',
|
||||
],
|
||||
DS_RESULTS: [
|
||||
'/user-guide/querying/query-results-data-source',
|
||||
'Guide: Help Setting up Query Results',
|
||||
],
|
||||
ALERT_SETUP: [
|
||||
'/user-guide/alerts/setting-up-an-alert',
|
||||
'Guide: Setting Up a New Alert',
|
||||
],
|
||||
MAIL_CONFIG: [
|
||||
'/open-source/setup/#Mail-Configuration',
|
||||
'Guide: Mail Configuration',
|
||||
],
|
||||
ALERT_NOTIF_TEMPLATE_GUIDE: [
|
||||
'/user-guide/alerts/custom-alert-notifications',
|
||||
'Guide: Custom Alerts Notifications',
|
||||
],
|
||||
FAVORITES: [
|
||||
'/user-guide/querying/favorites-tagging/#Favorites',
|
||||
'Guide: Favorites',
|
||||
HOME: ["", "Help"],
|
||||
VALUE_SOURCE_OPTIONS: ["/user-guide/querying/query-parameters#Value-Source-Options", "Guide: Value Source Options"],
|
||||
SHARE_DASHBOARD: ["/user-guide/dashboards/sharing-dashboards", "Guide: Sharing and Embedding Dashboards"],
|
||||
AUTHENTICATION_OPTIONS: ["/user-guide/users/authentication-options", "Guide: Authentication Options"],
|
||||
USAGE_DATA_SHARING: ["/open-source/admin-guide/usage-data", "Help: Anonymous Usage Data Sharing"],
|
||||
DS_ATHENA: ["/data-sources/amazon-athena-setup", "Guide: Help Setting up Amazon Athena"],
|
||||
DS_BIGQUERY: ["/data-sources/bigquery-setup", "Guide: Help Setting up BigQuery"],
|
||||
DS_URL: ["/data-sources/querying-urls", "Guide: Help Setting up URL"],
|
||||
DS_MONGODB: ["/data-sources/mongodb-setup", "Guide: Help Setting up MongoDB"],
|
||||
DS_GOOGLE_SPREADSHEETS: ["/data-sources/querying-a-google-spreadsheet", "Guide: Help Setting up Google Spreadsheets"],
|
||||
DS_GOOGLE_ANALYTICS: ["/data-sources/google-analytics-setup", "Guide: Help Setting up Google Analytics"],
|
||||
DS_AXIBASETSD: ["/data-sources/axibase-time-series-database", "Guide: Help Setting up Axibase Time Series"],
|
||||
DS_RESULTS: ["/user-guide/querying/query-results-data-source", "Guide: Help Setting up Query Results"],
|
||||
ALERT_SETUP: ["/user-guide/alerts/setting-up-an-alert", "Guide: Setting Up a New Alert"],
|
||||
MAIL_CONFIG: ["/open-source/setup/#Mail-Configuration", "Guide: Mail Configuration"],
|
||||
ALERT_NOTIF_TEMPLATE_GUIDE: ["/user-guide/alerts/custom-alert-notifications", "Guide: Custom Alerts Notifications"],
|
||||
FAVORITES: ["/user-guide/querying/favorites-tagging/#Favorites", "Guide: Favorites"],
|
||||
MANAGE_PERMISSIONS: [
|
||||
"/user-guide/querying/writing-queries#Managing-Query-Permissions",
|
||||
"Guide: Managing Query Permissions",
|
||||
],
|
||||
NUMBER_FORMAT_SPECS: ["/user-guide/visualizations/formatting-numbers", "Formatting Numbers"],
|
||||
};
|
||||
|
||||
export default class HelpTrigger extends React.Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.oneOf(Object.keys(TYPES)).isRequired,
|
||||
className: PropTypes.string,
|
||||
showTooltip: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
className: null,
|
||||
showTooltip: true,
|
||||
children: <i className="fa fa-question-circle" />,
|
||||
};
|
||||
|
||||
iframeRef = null;
|
||||
iframeRef = React.createRef();
|
||||
|
||||
iframeLoadingTimeout = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.iframeRef = React.createRef();
|
||||
}
|
||||
|
||||
state = {
|
||||
visible: false,
|
||||
loading: false,
|
||||
@@ -114,15 +66,15 @@ export default class HelpTrigger extends React.Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.onPostMessageReceived, DOMAIN);
|
||||
window.addEventListener("message", this.onPostMessageReceived, false);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.onPostMessageReceived);
|
||||
window.removeEventListener("message", this.onPostMessageReceived);
|
||||
clearTimeout(this.iframeLoadingTimeout);
|
||||
}
|
||||
|
||||
loadIframe = (url) => {
|
||||
loadIframe = url => {
|
||||
clearTimeout(this.iframeLoadingTimeout);
|
||||
this.setState({ loading: true, error: false });
|
||||
|
||||
@@ -137,14 +89,18 @@ export default class HelpTrigger extends React.Component {
|
||||
clearTimeout(this.iframeLoadingTimeout);
|
||||
};
|
||||
|
||||
onPostMessageReceived = (event) => {
|
||||
onPostMessageReceived = event => {
|
||||
if (!startsWith(event.origin, DOMAIN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, message: currentUrl } = event.data || {};
|
||||
if (type !== IFRAME_URL_UPDATE_MESSAGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ currentUrl });
|
||||
}
|
||||
};
|
||||
|
||||
openDrawer = () => {
|
||||
this.setState({ visible: true });
|
||||
@@ -155,7 +111,7 @@ export default class HelpTrigger extends React.Component {
|
||||
setTimeout(() => this.loadIframe(url), 300);
|
||||
};
|
||||
|
||||
closeDrawer = (event) => {
|
||||
closeDrawer = event => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -165,12 +121,12 @@ export default class HelpTrigger extends React.Component {
|
||||
|
||||
render() {
|
||||
const [, tooltip] = TYPES[this.props.type];
|
||||
const className = cx('help-trigger', this.props.className);
|
||||
const className = cx("help-trigger", this.props.className);
|
||||
const url = this.state.currentUrl;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Tooltip title={tooltip}>
|
||||
<Tooltip title={this.props.showTooltip ? tooltip : null}>
|
||||
<a onClick={this.openDrawer} className={className}>
|
||||
{this.props.children}
|
||||
</a>
|
||||
@@ -182,8 +138,7 @@ export default class HelpTrigger extends React.Component {
|
||||
visible={this.state.visible}
|
||||
className="help-drawer"
|
||||
destroyOnClose
|
||||
width={400}
|
||||
>
|
||||
width={400}>
|
||||
<div className="drawer-wrapper">
|
||||
<div className="drawer-menu">
|
||||
{url && (
|
||||
@@ -220,20 +175,19 @@ export default class HelpTrigger extends React.Component {
|
||||
{/* error message */}
|
||||
{this.state.error && (
|
||||
<BigMessage icon="fa-exclamation-circle" className="help-message">
|
||||
Something went wrong.<br />
|
||||
Something went wrong.
|
||||
<br />
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a href={this.state.error} target="_blank" rel="noopener">Click here</a>{' '}
|
||||
<a href={this.state.error} target="_blank" rel="noopener">
|
||||
Click here
|
||||
</a>{" "}
|
||||
to open the page in a new window.
|
||||
</BigMessage>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* extra content */}
|
||||
<DynamicComponent
|
||||
name="HelpDrawerExtraContent"
|
||||
onLeave={this.closeDrawer}
|
||||
openPageUrl={this.loadIframe}
|
||||
/>
|
||||
<DynamicComponent name="HelpDrawerExtraContent" onLeave={this.closeDrawer} openPageUrl={this.loadIframe} />
|
||||
</Drawer>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { $sanitize } from '@/services/ng';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { sanitize } from "dompurify";
|
||||
|
||||
export default function HtmlContent({ children, ...props }) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
dangerouslySetInnerHTML={{ __html: $sanitize(children) }} // eslint-disable-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: sanitize(children) }} // eslint-disable-line react/no-danger
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -16,5 +16,5 @@ HtmlContent.propTypes = {
|
||||
};
|
||||
|
||||
HtmlContent.defaultProps = {
|
||||
children: '',
|
||||
children: "",
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import Input from 'antd/lib/input';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import React from "react";
|
||||
import Input from "antd/lib/input";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
export default class InputWithCopy extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { copied: null };
|
||||
this.ref = React.createRef();
|
||||
this.copyFeatureSupported = document.queryCommandSupported('copy');
|
||||
this.copyFeatureSupported = document.queryCommandSupported("copy");
|
||||
this.resetCopyState = null;
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ export default class InputWithCopy extends React.Component {
|
||||
|
||||
// copy
|
||||
try {
|
||||
const success = document.execCommand('copy');
|
||||
const success = document.execCommand("copy");
|
||||
if (!success) {
|
||||
throw new Error();
|
||||
}
|
||||
this.setState({ copied: 'Copied!' });
|
||||
this.setState({ copied: "Copied!" });
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
copied: 'Copy failed',
|
||||
copied: "Copy failed",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,17 +41,11 @@ export default class InputWithCopy extends React.Component {
|
||||
|
||||
render() {
|
||||
const copyButton = (
|
||||
<Tooltip title={this.state.copied || 'Copy'}>
|
||||
<Icon
|
||||
type="copy"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={this.copy}
|
||||
/>
|
||||
<Tooltip title={this.state.copied || "Copy"}>
|
||||
<Icon type="copy" style={{ cursor: "pointer" }} onClick={this.copy} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<Input {...this.props} ref={this.ref} addonAfter={this.copyFeatureSupported && copyButton} />
|
||||
);
|
||||
return <Input {...this.props} ref={this.ref} addonAfter={this.copyFeatureSupported && copyButton} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { BigMessage } from '@/components/BigMessage';
|
||||
import { TagsControl } from '@/components/tags-control/TagsControl';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import BigMessage from "@/components/BigMessage";
|
||||
import { TagsControl } from "@/components/tags-control/TagsControl";
|
||||
|
||||
export function NoTaggedObjectsFound({ objectType, tags }) {
|
||||
export default function NoTaggedObjectsFound({ objectType, tags }) {
|
||||
return (
|
||||
<BigMessage icon="fa-tags">
|
||||
No {objectType} found tagged with <TagsControl className="inline-tags-control" tags={Array.from(tags)} />.
|
||||
No {objectType} found tagged with
|
||||
<TagsControl className="inline-tags-control" tags={Array.from(tags)} />.
|
||||
</BigMessage>
|
||||
);
|
||||
}
|
||||
|
||||
NoTaggedObjectsFound.propTypes = {
|
||||
objectType: PropTypes.string.isRequired,
|
||||
tags: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.objectOf(Set),
|
||||
]).isRequired,
|
||||
tags: PropTypes.oneOfType([PropTypes.array, PropTypes.objectOf(Set)]).isRequired,
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('noTaggedObjectsFound', react2angular(NoTaggedObjectsFound));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export function PageHeader({ title }) {
|
||||
export default function PageHeader({ title }) {
|
||||
return (
|
||||
<div className="page-header-wrapper row p-l-15 p-r-15 m-b-10 m-l-0 m-r-0">
|
||||
<div className="col-sm-9 p-l-0 p-r-0">
|
||||
<h3>{ title }</h3>
|
||||
<h3>{title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -15,9 +14,3 @@ export function PageHeader({ title }) {
|
||||
PageHeader.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('pageHeader', react2angular(PageHeader));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Pagination from 'antd/lib/pagination';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Pagination from "antd/lib/pagination";
|
||||
|
||||
export function Paginator({
|
||||
page,
|
||||
itemsPerPage,
|
||||
totalCount,
|
||||
onChange,
|
||||
}) {
|
||||
export default function Paginator({ page, itemsPerPage, totalCount, onChange }) {
|
||||
if (totalCount <= itemsPerPage) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="paginator-container">
|
||||
<Pagination
|
||||
defaultCurrent={page}
|
||||
defaultPageSize={itemsPerPage}
|
||||
total={totalCount}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Pagination defaultCurrent={page} defaultPageSize={itemsPerPage} total={totalCount} onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,27 +23,3 @@ Paginator.propTypes = {
|
||||
Paginator.defaultProps = {
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('paginatorImpl', react2angular(Paginator));
|
||||
ngModule.component('paginator', {
|
||||
template: `
|
||||
<paginator-impl
|
||||
page="$ctrl.paginator.page"
|
||||
items-per-page="$ctrl.paginator.itemsPerPage"
|
||||
total-count="$ctrl.paginator.totalCount"
|
||||
on-change="$ctrl.onPageChanged"
|
||||
></paginator-impl>`,
|
||||
bindings: {
|
||||
paginator: '<',
|
||||
},
|
||||
controller($scope) {
|
||||
this.onPageChanged = (page) => {
|
||||
this.paginator.setPage(page);
|
||||
$scope.$applyAsync();
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'antd/lib/button';
|
||||
import Badge from 'antd/lib/badge';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { KeyboardShortcuts } from '@/services/keyboard-shortcuts';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import Badge from "antd/lib/badge";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import KeyboardShortcuts from "@/services/KeyboardShortcuts";
|
||||
|
||||
function ParameterApplyButton({ paramCount, onClick }) {
|
||||
// show spinner when count is empty so the fade out is consistent
|
||||
const icon = !paramCount ? 'spinner fa-pulse' : 'check';
|
||||
const icon = !paramCount ? "spinner fa-pulse" : "check";
|
||||
|
||||
return (
|
||||
<div className="parameter-apply-button" data-show={!!paramCount} data-test="ParameterApplyButton">
|
||||
<Badge count={paramCount}>
|
||||
<Tooltip title={`${KeyboardShortcuts.modKey} + Enter`}>
|
||||
<Tooltip title={paramCount ? `${KeyboardShortcuts.modKey} + Enter` : null}>
|
||||
<span>
|
||||
<Button onClick={onClick}>
|
||||
<i className={`fa fa-${icon}`} /> Apply Changes
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
|
||||
import { isString, extend, each, has, map, includes, findIndex, find,
|
||||
fromPairs, clone, isEmpty } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Select from 'antd/lib/select';
|
||||
import Table from 'antd/lib/table';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Tag from 'antd/lib/tag';
|
||||
import Input from 'antd/lib/input';
|
||||
import Radio from 'antd/lib/radio';
|
||||
import Form from 'antd/lib/form';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import ParameterValueInput from '@/components/ParameterValueInput';
|
||||
import { ParameterMappingType } from '@/services/widget';
|
||||
import { Parameter } from '@/services/parameters';
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import { isString, extend, each, has, map, includes, findIndex, find, fromPairs, clone, isEmpty } from "lodash";
|
||||
import React, { Fragment } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import Select from "antd/lib/select";
|
||||
import Table from "antd/lib/table";
|
||||
import Popover from "antd/lib/popover";
|
||||
import Button from "antd/lib/button";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Tag from "antd/lib/tag";
|
||||
import Input from "antd/lib/input";
|
||||
import Radio from "antd/lib/radio";
|
||||
import Form from "antd/lib/form";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import ParameterValueInput from "@/components/ParameterValueInput";
|
||||
import { ParameterMappingType } from "@/services/widget";
|
||||
import { Parameter, cloneParameter } from "@/services/parameters";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
|
||||
import './ParameterMappingInput.less';
|
||||
import "./ParameterMappingInput.less";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export const MappingType = {
|
||||
DashboardAddNew: 'dashboard-add-new',
|
||||
DashboardMapToExisting: 'dashboard-map-to-existing',
|
||||
WidgetLevel: 'widget-level',
|
||||
StaticValue: 'static-value',
|
||||
DashboardAddNew: "dashboard-add-new",
|
||||
DashboardMapToExisting: "dashboard-map-to-existing",
|
||||
WidgetLevel: "widget-level",
|
||||
StaticValue: "static-value",
|
||||
};
|
||||
|
||||
export function parameterMappingsToEditableMappings(mappings, parameters, existingParameterNames = []) {
|
||||
return map(mappings, (mapping) => {
|
||||
return map(mappings, mapping => {
|
||||
const result = extend({}, mapping);
|
||||
const alreadyExists = includes(existingParameterNames, mapping.mapTo);
|
||||
result.param = find(parameters, p => p.name === mapping.name);
|
||||
@@ -43,7 +42,7 @@ export function parameterMappingsToEditableMappings(mappings, parameters, existi
|
||||
break;
|
||||
case ParameterMappingType.StaticValue:
|
||||
result.type = MappingType.StaticValue;
|
||||
result.param = result.param.clone();
|
||||
result.param = cloneParameter(result.param);
|
||||
result.param.setValue(result.value);
|
||||
break;
|
||||
case ParameterMappingType.WidgetLevel:
|
||||
@@ -57,49 +56,52 @@ export function parameterMappingsToEditableMappings(mappings, parameters, existi
|
||||
}
|
||||
|
||||
export function editableMappingsToParameterMappings(mappings) {
|
||||
return fromPairs(map( // convert to map
|
||||
mappings,
|
||||
(mapping) => {
|
||||
const result = extend({}, mapping);
|
||||
switch (mapping.type) {
|
||||
case MappingType.DashboardAddNew:
|
||||
result.type = ParameterMappingType.DashboardLevel;
|
||||
result.value = null;
|
||||
break;
|
||||
case MappingType.DashboardMapToExisting:
|
||||
result.type = ParameterMappingType.DashboardLevel;
|
||||
result.value = null;
|
||||
break;
|
||||
case MappingType.StaticValue:
|
||||
result.type = ParameterMappingType.StaticValue;
|
||||
result.param = mapping.param.clone();
|
||||
result.param.setValue(result.value);
|
||||
result.value = result.param.value;
|
||||
break;
|
||||
case MappingType.WidgetLevel:
|
||||
result.type = ParameterMappingType.WidgetLevel;
|
||||
result.value = null;
|
||||
break;
|
||||
// no default
|
||||
return fromPairs(
|
||||
map(
|
||||
// convert to map
|
||||
mappings,
|
||||
mapping => {
|
||||
const result = extend({}, mapping);
|
||||
switch (mapping.type) {
|
||||
case MappingType.DashboardAddNew:
|
||||
result.type = ParameterMappingType.DashboardLevel;
|
||||
result.value = null;
|
||||
break;
|
||||
case MappingType.DashboardMapToExisting:
|
||||
result.type = ParameterMappingType.DashboardLevel;
|
||||
result.value = null;
|
||||
break;
|
||||
case MappingType.StaticValue:
|
||||
result.type = ParameterMappingType.StaticValue;
|
||||
result.param = cloneParameter(mapping.param);
|
||||
result.param.setValue(result.value);
|
||||
result.value = result.param.value;
|
||||
break;
|
||||
case MappingType.WidgetLevel:
|
||||
result.type = ParameterMappingType.WidgetLevel;
|
||||
result.value = null;
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
delete result.param;
|
||||
return [result.name, result];
|
||||
}
|
||||
delete result.param;
|
||||
return [result.name, result];
|
||||
},
|
||||
));
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function synchronizeWidgetTitles(sourceMappings, widgets) {
|
||||
const affectedWidgets = [];
|
||||
|
||||
each(sourceMappings, (sourceMapping) => {
|
||||
each(sourceMappings, sourceMapping => {
|
||||
if (sourceMapping.type === ParameterMappingType.DashboardLevel) {
|
||||
each(widgets, (widget) => {
|
||||
each(widgets, widget => {
|
||||
const widgetMappings = widget.options.parameterMappings;
|
||||
each(widgetMappings, (widgetMapping) => {
|
||||
each(widgetMappings, widgetMapping => {
|
||||
// check if mapped to the same dashboard-level parameter
|
||||
if (
|
||||
(widgetMapping.type === ParameterMappingType.DashboardLevel) &&
|
||||
(widgetMapping.mapTo === sourceMapping.mapTo)
|
||||
widgetMapping.type === ParameterMappingType.DashboardLevel &&
|
||||
widgetMapping.mapTo === sourceMapping.mapTo
|
||||
) {
|
||||
// dirty check - update only when needed
|
||||
if (widgetMapping.title !== sourceMapping.title) {
|
||||
@@ -133,33 +135,32 @@ export class ParameterMappingInput extends React.Component {
|
||||
formItemProps = {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 16 },
|
||||
className: 'form-item',
|
||||
className: "form-item",
|
||||
};
|
||||
|
||||
updateSourceType = (type) => {
|
||||
let { mapping: { mapTo } } = this.props;
|
||||
updateSourceType = type => {
|
||||
let {
|
||||
mapping: { mapTo },
|
||||
} = this.props;
|
||||
const { existingParamNames } = this.props;
|
||||
|
||||
// if mapped name doesn't already exists
|
||||
// default to first select option
|
||||
if (
|
||||
type === MappingType.DashboardMapToExisting &&
|
||||
!includes(existingParamNames, mapTo)
|
||||
) {
|
||||
if (type === MappingType.DashboardMapToExisting && !includes(existingParamNames, mapTo)) {
|
||||
mapTo = existingParamNames[0];
|
||||
}
|
||||
|
||||
this.updateParamMapping({ type, mapTo });
|
||||
};
|
||||
|
||||
updateParamMapping = (update) => {
|
||||
updateParamMapping = update => {
|
||||
const { onChange, mapping } = this.props;
|
||||
const newMapping = extend({}, mapping, update);
|
||||
if (newMapping.value !== mapping.value) {
|
||||
newMapping.param = newMapping.param.clone();
|
||||
newMapping.param = cloneParameter(newMapping.param);
|
||||
newMapping.param.setValue(newMapping.value);
|
||||
}
|
||||
if (has(update, 'type')) {
|
||||
if (has(update, "type")) {
|
||||
if (update.type === MappingType.StaticValue) {
|
||||
newMapping.value = newMapping.param.value;
|
||||
} else {
|
||||
@@ -172,24 +173,17 @@ export class ParameterMappingInput extends React.Component {
|
||||
renderMappingTypeSelector() {
|
||||
const noExisting = isEmpty(this.props.existingParamNames);
|
||||
return (
|
||||
<Radio.Group
|
||||
value={this.props.mapping.type}
|
||||
onChange={e => this.updateSourceType(e.target.value)}
|
||||
>
|
||||
<Radio.Group value={this.props.mapping.type} onChange={e => this.updateSourceType(e.target.value)}>
|
||||
<Radio className="radio" value={MappingType.DashboardAddNew} data-test="NewDashboardParameterOption">
|
||||
New dashboard parameter
|
||||
</Radio>
|
||||
<Radio
|
||||
className="radio"
|
||||
value={MappingType.DashboardMapToExisting}
|
||||
disabled={noExisting}
|
||||
>
|
||||
Existing dashboard parameter{' '}
|
||||
<Radio className="radio" value={MappingType.DashboardMapToExisting} disabled={noExisting}>
|
||||
Existing dashboard parameter{" "}
|
||||
{noExisting ? (
|
||||
<Tooltip title="There are no dashboard parameters corresponding to this data type">
|
||||
<Icon type="question-circle" theme="filled" />
|
||||
</Tooltip>
|
||||
) : null }
|
||||
) : null}
|
||||
</Radio>
|
||||
<Radio className="radio" value={MappingType.WidgetLevel} data-test="WidgetParameterOption">
|
||||
Widget parameter
|
||||
@@ -202,13 +196,10 @@ export class ParameterMappingInput extends React.Component {
|
||||
}
|
||||
|
||||
renderDashboardAddNew() {
|
||||
const { mapping: { mapTo } } = this.props;
|
||||
return (
|
||||
<Input
|
||||
value={mapTo}
|
||||
onChange={e => this.updateParamMapping({ mapTo: e.target.value })}
|
||||
/>
|
||||
);
|
||||
const {
|
||||
mapping: { mapTo },
|
||||
} = this.props;
|
||||
return <Input value={mapTo} onChange={e => this.updateParamMapping({ mapTo: e.target.value })} />;
|
||||
}
|
||||
|
||||
renderDashboardMapToExisting() {
|
||||
@@ -218,10 +209,11 @@ export class ParameterMappingInput extends React.Component {
|
||||
<Select
|
||||
value={mapping.mapTo}
|
||||
onChange={mapTo => this.updateParamMapping({ mapTo })}
|
||||
dropdownMatchSelectWidth={false}
|
||||
>
|
||||
dropdownMatchSelectWidth={false}>
|
||||
{map(existingParamNames, name => (
|
||||
<Option value={name} key={name}>{ name }</Option>
|
||||
<Option value={name} key={name}>
|
||||
{name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
@@ -245,24 +237,13 @@ export class ParameterMappingInput extends React.Component {
|
||||
const { mapping } = this.props;
|
||||
switch (mapping.type) {
|
||||
case MappingType.DashboardAddNew:
|
||||
return [
|
||||
'Key',
|
||||
'Enter a new parameter keyword',
|
||||
this.renderDashboardAddNew(),
|
||||
];
|
||||
return ["Key", "Enter a new parameter keyword", this.renderDashboardAddNew()];
|
||||
case MappingType.DashboardMapToExisting:
|
||||
return [
|
||||
'Key',
|
||||
'Select from a list of existing parameters',
|
||||
this.renderDashboardMapToExisting(),
|
||||
];
|
||||
return ["Key", "Select from a list of existing parameters", this.renderDashboardMapToExisting()];
|
||||
case MappingType.StaticValue:
|
||||
return [
|
||||
'Value',
|
||||
null,
|
||||
this.renderStaticValue(),
|
||||
];
|
||||
default: return [];
|
||||
return ["Value", null, this.renderStaticValue()];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,10 +257,10 @@ export class ParameterMappingInput extends React.Component {
|
||||
{this.renderMappingTypeSelector()}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
style={{ height: 60, visibility: input ? 'visible' : 'hidden' }}
|
||||
style={{ height: 60, visibility: input ? "visible" : "hidden" }}
|
||||
label={label}
|
||||
{...this.formItemProps}
|
||||
validateStatus={inputError ? 'error' : ''}
|
||||
validateStatus={inputError ? "error" : ""}
|
||||
help={inputError || help} // empty space so line doesn't collapse
|
||||
>
|
||||
{input}
|
||||
@@ -305,18 +286,19 @@ class MappingEditor extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
onVisibleChange = (visible) => {
|
||||
if (visible) this.show(); else this.hide();
|
||||
onVisibleChange = visible => {
|
||||
if (visible) this.show();
|
||||
else this.hide();
|
||||
};
|
||||
|
||||
onChange = (mapping) => {
|
||||
onChange = mapping => {
|
||||
let inputError = null;
|
||||
|
||||
if (mapping.type === MappingType.DashboardAddNew) {
|
||||
if (isEmpty(mapping.mapTo)) {
|
||||
inputError = 'Keyword must have a value';
|
||||
inputError = "Keyword must have a value";
|
||||
} else if (includes(this.props.existingParamNames, mapping.mapTo)) {
|
||||
inputError = 'A parameter with this name already exists';
|
||||
inputError = "A parameter with this name already exists";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +337,9 @@ class MappingEditor extends React.Component {
|
||||
/>
|
||||
<footer>
|
||||
<Button onClick={this.hide}>Cancel</Button>
|
||||
<Button onClick={this.save} disabled={!!inputError} type="primary">OK</Button>
|
||||
<Button onClick={this.save} disabled={!!inputError} type="primary">
|
||||
OK
|
||||
</Button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
@@ -369,8 +353,7 @@ class MappingEditor extends React.Component {
|
||||
trigger="click"
|
||||
content={this.renderContent()}
|
||||
visible={visible}
|
||||
onVisibleChange={this.onVisibleChange}
|
||||
>
|
||||
onVisibleChange={this.onVisibleChange}>
|
||||
<Button size="small" type="dashed" data-test={`EditParamMappingButon-${mapping.param.name}`}>
|
||||
<Icon type="edit" />
|
||||
</Button>
|
||||
@@ -392,24 +375,24 @@ class TitleEditor extends React.Component {
|
||||
|
||||
state = {
|
||||
showPopup: false,
|
||||
title: '', // will be set on editing
|
||||
title: "", // will be set on editing
|
||||
};
|
||||
|
||||
onPopupVisibleChange = (showPopup) => {
|
||||
onPopupVisibleChange = showPopup => {
|
||||
this.setState({
|
||||
showPopup,
|
||||
title: showPopup ? this.getMappingTitle() : '',
|
||||
title: showPopup ? this.getMappingTitle() : "",
|
||||
});
|
||||
};
|
||||
|
||||
onEditingTitleChange = (event) => {
|
||||
onEditingTitleChange = event => {
|
||||
this.setState({ title: event.target.value });
|
||||
};
|
||||
|
||||
getMappingTitle() {
|
||||
let { mapping } = this.props;
|
||||
|
||||
if (isString(mapping.title) && (mapping.title !== '')) {
|
||||
if (isString(mapping.title) && mapping.title !== "") {
|
||||
return mapping.title;
|
||||
}
|
||||
|
||||
@@ -435,7 +418,9 @@ class TitleEditor extends React.Component {
|
||||
};
|
||||
|
||||
renderPopover() {
|
||||
const { param: { title: paramTitle } } = this.props.mapping;
|
||||
const {
|
||||
param: { title: paramTitle },
|
||||
} = this.props.mapping;
|
||||
|
||||
return (
|
||||
<div className="parameter-mapping-title-editor">
|
||||
@@ -473,8 +458,7 @@ class TitleEditor extends React.Component {
|
||||
trigger="click"
|
||||
content={this.renderPopover()}
|
||||
visible={this.state.showPopup}
|
||||
onVisibleChange={this.onPopupVisibleChange}
|
||||
>
|
||||
onVisibleChange={this.onPopupVisibleChange}>
|
||||
<Button size="small" type="dashed">
|
||||
<Icon type="edit" />
|
||||
</Button>
|
||||
@@ -488,7 +472,7 @@ class TitleEditor extends React.Component {
|
||||
const disabled = mapping.type === MappingType.StaticValue;
|
||||
|
||||
return (
|
||||
<div className={classNames('parameter-mapping-title', { disabled })}>
|
||||
<div className={classNames("parameter-mapping-title", { disabled })}>
|
||||
<span className="text">{this.getMappingTitle()}</span>
|
||||
{this.renderEditButton()}
|
||||
</div>
|
||||
@@ -512,17 +496,17 @@ export class ParameterMappingListInput extends React.Component {
|
||||
static getStringValue(value) {
|
||||
// null
|
||||
if (!value) {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
|
||||
// range
|
||||
if (value instanceof Object && 'start' in value && 'end' in value) {
|
||||
if (value instanceof Object && "start" in value && "end" in value) {
|
||||
return `${value.start} ~ ${value.end}`;
|
||||
}
|
||||
|
||||
// just to be safe, array or object
|
||||
if (typeof value === 'object') {
|
||||
return map(value, v => this.getStringValue(v)).join(', ');
|
||||
if (typeof value === "object") {
|
||||
return map(value, v => this.getStringValue(v)).join(", ");
|
||||
}
|
||||
|
||||
// rest
|
||||
@@ -536,13 +520,14 @@ export class ParameterMappingListInput extends React.Component {
|
||||
// if mapped to another param, swap 'em
|
||||
if (type === MappingType.DashboardMapToExisting && mapTo !== name) {
|
||||
const mappedTo = find(existingParams, { name: mapTo });
|
||||
if (mappedTo) { // just being safe
|
||||
if (mappedTo) {
|
||||
// just being safe
|
||||
param = mappedTo;
|
||||
}
|
||||
|
||||
// static type is different since it's fed param.normalizedValue
|
||||
// static type is different since it's fed param.normalizedValue
|
||||
} else if (type === MappingType.StaticValue) {
|
||||
param = param.clone().setValue(mapping.value);
|
||||
param = cloneParameter(param).setValue(mapping.value);
|
||||
}
|
||||
|
||||
let value = Parameter.getExecutionValue(param);
|
||||
@@ -561,16 +546,15 @@ export class ParameterMappingListInput extends React.Component {
|
||||
case MappingType.DashboardMapToExisting:
|
||||
return (
|
||||
<Fragment>
|
||||
Dashboard{' '}
|
||||
<Tag className="tag">{mapTo}</Tag>
|
||||
Dashboard <Tag className="tag">{mapTo}</Tag>
|
||||
</Fragment>
|
||||
);
|
||||
case MappingType.WidgetLevel:
|
||||
return 'Widget parameter';
|
||||
return "Widget parameter";
|
||||
case MappingType.StaticValue:
|
||||
return 'Static value';
|
||||
return "Static value";
|
||||
default:
|
||||
return ''; // won't happen (typescript-ftw)
|
||||
return ""; // won't happen (typescript-ftw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,12 +576,7 @@ export class ParameterMappingListInput extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="parameters-mapping-list">
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
size="middle"
|
||||
pagination={false}
|
||||
rowKey={(record, idx) => `row${idx}`}
|
||||
>
|
||||
<Table dataSource={dataSource} size="middle" pagination={false} rowKey={(record, idx) => `row${idx}`}>
|
||||
<Table.Column
|
||||
title="Title"
|
||||
dataIndex="mapping"
|
||||
@@ -621,22 +600,20 @@ export class ParameterMappingListInput extends React.Component {
|
||||
title="Default Value"
|
||||
dataIndex="mapping"
|
||||
key="value"
|
||||
render={mapping => (
|
||||
this.constructor.getDefaultValue(mapping, this.props.existingParams)
|
||||
)}
|
||||
render={mapping => this.constructor.getDefaultValue(mapping, this.props.existingParams)}
|
||||
/>
|
||||
<Table.Column
|
||||
title="Value Source"
|
||||
dataIndex="mapping"
|
||||
key="source"
|
||||
render={(mapping) => {
|
||||
render={mapping => {
|
||||
const existingParamsNames = existingParams
|
||||
.filter(({ type }) => type === mapping.param.type) // exclude mismatching param types
|
||||
.map(({ name }) => name); // keep names only
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{this.constructor.getSourceTypeLabel(mapping)}{' '}
|
||||
{this.constructor.getSourceTypeLabel(mapping)}{" "}
|
||||
<MappingEditor
|
||||
mapping={mapping}
|
||||
existingParamNames={existingParamsNames}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
import DateParameter from '@/components/dynamic-parameters/DateParameter';
|
||||
import DateRangeParameter from '@/components/dynamic-parameters/DateRangeParameter';
|
||||
import { isEqual } from 'lodash';
|
||||
import { QueryBasedParameterInput } from './QueryBasedParameterInput';
|
||||
import { isEqual } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Select from "antd/lib/select";
|
||||
import Input from "antd/lib/input";
|
||||
import InputNumber from "antd/lib/input-number";
|
||||
import DateParameter from "@/components/dynamic-parameters/DateParameter";
|
||||
import DateRangeParameter from "@/components/dynamic-parameters/DateRangeParameter";
|
||||
import QueryBasedParameterInput from "./QueryBasedParameterInput";
|
||||
|
||||
import './ParameterValueInput.less';
|
||||
import "./ParameterValueInput.less";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -30,13 +30,13 @@ class ParameterValueInput extends React.Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
type: 'text',
|
||||
type: "text",
|
||||
value: null,
|
||||
enumOptions: '',
|
||||
enumOptions: "",
|
||||
queryId: null,
|
||||
parameter: null,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -47,7 +47,7 @@ class ParameterValueInput extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
componentDidUpdate = prevProps => {
|
||||
const { value, parameter } = this.props;
|
||||
// if value prop updated, reset dirty state
|
||||
if (prevProps.value !== value || prevProps.parameter !== parameter) {
|
||||
@@ -56,13 +56,13 @@ class ParameterValueInput extends React.Component {
|
||||
isDirty: parameter.hasPendingValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onSelect = (value) => {
|
||||
onSelect = value => {
|
||||
const isDirty = !isEqual(value, this.props.value);
|
||||
this.setState({ value, isDirty });
|
||||
this.props.onSelect(value, isDirty);
|
||||
}
|
||||
};
|
||||
|
||||
renderDateParameter() {
|
||||
const { type, parameter } = this.props;
|
||||
@@ -95,13 +95,13 @@ class ParameterValueInput extends React.Component {
|
||||
renderEnumInput() {
|
||||
const { enumOptions, parameter } = this.props;
|
||||
const { value } = this.state;
|
||||
const enumOptionsArray = enumOptions.split('\n').filter(v => v !== '');
|
||||
const enumOptionsArray = enumOptions.split("\n").filter(v => v !== "");
|
||||
// Antd Select doesn't handle null in multiple mode
|
||||
const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val);
|
||||
return (
|
||||
<Select
|
||||
className={this.props.className}
|
||||
mode={parameter.multiValuesOptions ? 'multiple' : 'default'}
|
||||
mode={parameter.multiValuesOptions ? "multiple" : "default"}
|
||||
optionFilterProp="children"
|
||||
disabled={enumOptionsArray.length === 0}
|
||||
value={normalize(value)}
|
||||
@@ -111,9 +111,12 @@ class ParameterValueInput extends React.Component {
|
||||
showArrow
|
||||
style={{ minWidth: 60 }}
|
||||
notFoundContent={null}
|
||||
{...multipleValuesProps}
|
||||
>
|
||||
{enumOptionsArray.map(option => (<Option key={option} value={option}>{ option }</Option>))}
|
||||
{...multipleValuesProps}>
|
||||
{enumOptionsArray.map(option => (
|
||||
<Option key={option} value={option}>
|
||||
{option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -124,7 +127,7 @@ class ParameterValueInput extends React.Component {
|
||||
return (
|
||||
<QueryBasedParameterInput
|
||||
className={this.props.className}
|
||||
mode={parameter.multiValuesOptions ? 'multiple' : 'default'}
|
||||
mode={parameter.multiValuesOptions ? "multiple" : "default"}
|
||||
optionFilterProp="children"
|
||||
parameter={parameter}
|
||||
value={value}
|
||||
@@ -143,11 +146,7 @@ class ParameterValueInput extends React.Component {
|
||||
const normalize = val => (isNaN(val) ? undefined : val);
|
||||
|
||||
return (
|
||||
<InputNumber
|
||||
className={className}
|
||||
value={normalize(value)}
|
||||
onChange={val => this.onSelect(normalize(val))}
|
||||
/>
|
||||
<InputNumber className={className} value={normalize(value)} onChange={val => this.onSelect(normalize(val))} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,16 +167,22 @@ class ParameterValueInput extends React.Component {
|
||||
renderInput() {
|
||||
const { type } = this.props;
|
||||
switch (type) {
|
||||
case 'datetime-with-seconds':
|
||||
case 'datetime-local':
|
||||
case 'date': return this.renderDateParameter();
|
||||
case 'datetime-range-with-seconds':
|
||||
case 'datetime-range':
|
||||
case 'date-range': return this.renderDateRangeParameter();
|
||||
case 'enum': return this.renderEnumInput();
|
||||
case 'query': return this.renderQueryBasedInput();
|
||||
case 'number': return this.renderNumberInput();
|
||||
default: return this.renderTextInput();
|
||||
case "datetime-with-seconds":
|
||||
case "datetime-local":
|
||||
case "date":
|
||||
return this.renderDateParameter();
|
||||
case "datetime-range-with-seconds":
|
||||
case "datetime-range":
|
||||
case "date-range":
|
||||
return this.renderDateRangeParameter();
|
||||
case "enum":
|
||||
return this.renderEnumInput();
|
||||
case "query":
|
||||
return this.renderQueryBasedInput();
|
||||
case "number":
|
||||
return this.renderNumberInput();
|
||||
default:
|
||||
return this.renderTextInput();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user