mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Compare commits
335 Commits
23.10.0-de
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43ee21ac20 | ||
|
|
262d46f465 | ||
|
|
bc68b1c38b | ||
|
|
4353a82c7a | ||
|
|
761eb0b68b | ||
|
|
9743820efe | ||
|
|
9d49e0457f | ||
|
|
b5781a8ebe | ||
|
|
b6f4159be9 | ||
|
|
d5fbf547cf | ||
|
|
772b160a79 | ||
|
|
bac2160e2a | ||
|
|
c5aa5da6a2 | ||
|
|
9503cc9fb8 | ||
|
|
b353057f9a | ||
|
|
8747d02bbe | ||
|
|
5b463b0d83 | ||
|
|
ea589ad477 | ||
|
|
617124850b | ||
|
|
1cc200843c | ||
|
|
e0410e2ffe | ||
|
|
7e39b3668d | ||
|
|
92f15a3ccb | ||
|
|
9a1d33381c | ||
|
|
56c06adc24 | ||
|
|
5e8915afe5 | ||
|
|
b8ebf49436 | ||
|
|
59951eda3d | ||
|
|
777153e7a0 | ||
|
|
47b1309f13 | ||
|
|
120250152f | ||
|
|
ac81f0b223 | ||
|
|
7838058953 | ||
|
|
f95156e924 | ||
|
|
74de676bdf | ||
|
|
2762f1fc85 | ||
|
|
438efd0826 | ||
|
|
e586ab708b | ||
|
|
24ca5135aa | ||
|
|
fae354fcce | ||
|
|
4ae372f022 | ||
|
|
0b5907f12b | ||
|
|
00a97d9266 | ||
|
|
35afe880a1 | ||
|
|
a6298f2753 | ||
|
|
e69283f488 | ||
|
|
09ed3c4b81 | ||
|
|
f5e2a4c0fc | ||
|
|
4e200b4a08 | ||
|
|
5ae1f70d9e | ||
|
|
3f781d262b | ||
|
|
a34c1591e3 | ||
|
|
9f76fda18c | ||
|
|
d8ae679937 | ||
|
|
f3b0b60abd | ||
|
|
df8be91a07 | ||
|
|
c9ddd2a7d6 | ||
|
|
6b1e910126 | ||
|
|
14550a9a6c | ||
|
|
b80c5f6a7c | ||
|
|
e46d44f208 | ||
|
|
a1a4bc9d3e | ||
|
|
0900178d24 | ||
|
|
5d31429ca8 | ||
|
|
2f35ceb803 | ||
|
|
8e6c02ecde | ||
|
|
231fd36d46 | ||
|
|
0b6a53a079 | ||
|
|
6167edf97c | ||
|
|
4ed0ad3c9c | ||
|
|
2375f0b05f | ||
|
|
eced377ae4 | ||
|
|
84262fe143 | ||
|
|
612eb8c630 | ||
|
|
866fb48afb | ||
|
|
353776e8e1 | ||
|
|
594e2f24ef | ||
|
|
3275a9e459 | ||
|
|
3bad8c8e8c | ||
|
|
d0af4499d6 | ||
|
|
4357ea56ae | ||
|
|
5df5ca87a2 | ||
|
|
8387fe6fcb | ||
|
|
e95de2ee4c | ||
|
|
71902e5933 | ||
|
|
53eab14cef | ||
|
|
925bb91d8e | ||
|
|
ec2ca6f986 | ||
|
|
96ea0194e8 | ||
|
|
2776992101 | ||
|
|
85f001982e | ||
|
|
d03a2c4096 | ||
|
|
8c5890482a | ||
|
|
10ce280a96 | ||
|
|
0dd7ac3d2e | ||
|
|
4ee53a9445 | ||
|
|
c08292d90e | ||
|
|
3142131cdd | ||
|
|
530c1a0734 | ||
|
|
52dc1769a1 | ||
|
|
b9583c0b48 | ||
|
|
89d7f54e90 | ||
|
|
d884da2b0b | ||
|
|
f7d485082c | ||
|
|
130ab1fe1a | ||
|
|
2ff83679fe | ||
|
|
de49b73855 | ||
|
|
c12e68f5d1 | ||
|
|
baa9bbd505 | ||
|
|
349cd5d031 | ||
|
|
49277d27f8 | ||
|
|
2aae5705c9 | ||
|
|
38d0579660 | ||
|
|
673ba769c7 | ||
|
|
b922730482 | ||
|
|
ba973eb1fe | ||
|
|
d8dde6c544 | ||
|
|
d359a716a7 | ||
|
|
ba4293912b | ||
|
|
ee359120ee | ||
|
|
04a25f4327 | ||
|
|
7c22756e66 | ||
|
|
a03668f5b2 | ||
|
|
e4a841a0c5 | ||
|
|
38dc31a49b | ||
|
|
c42b15125c | ||
|
|
590d39bc8d | ||
|
|
79bbb248bb | ||
|
|
5cf0b7b038 | ||
|
|
fb1a056561 | ||
|
|
75e1ce4c9c | ||
|
|
d6c6e3bb7a | ||
|
|
821c1a9488 | ||
|
|
76eeea1f64 | ||
|
|
2ab07f9fc3 | ||
|
|
a85b9d7801 | ||
|
|
3330815081 | ||
|
|
c25c65bc04 | ||
|
|
79a4c4c9c9 | ||
|
|
58a7438cc8 | ||
|
|
c073c1e154 | ||
|
|
159a329e26 | ||
|
|
9de135c0bd | ||
|
|
285c2b6e56 | ||
|
|
b1fe2d4162 | ||
|
|
a4f92a8fb5 | ||
|
|
51ef625a30 | ||
|
|
a2611b89a3 | ||
|
|
a531597016 | ||
|
|
e59c02f497 | ||
|
|
c1a60bf6d2 | ||
|
|
72203655ec | ||
|
|
5257e39282 | ||
|
|
ec70ff4408 | ||
|
|
ed8c05f634 | ||
|
|
86b75db82e | ||
|
|
660d04b0f1 | ||
|
|
fc1e1f7a01 | ||
|
|
8725fa4737 | ||
|
|
ea0b3cbe3a | ||
|
|
714b950fde | ||
|
|
a9c9f085af | ||
|
|
a69f7fb2fe | ||
|
|
c244e75352 | ||
|
|
80f7ba1b91 | ||
|
|
d2745e5acc | ||
|
|
4114227471 | ||
|
|
8fc4ce1494 | ||
|
|
ebb0e2c9ad | ||
|
|
57a79bc96b | ||
|
|
77f108dd09 | ||
|
|
dd1a9b96da | ||
|
|
d9282b2688 | ||
|
|
28c39219af | ||
|
|
a37ef3b235 | ||
|
|
0056aa68f8 | ||
|
|
76b5a30fd9 | ||
|
|
db4fdd003e | ||
|
|
4cb32fc1c3 | ||
|
|
a6c728b99c | ||
|
|
01e036d0a9 | ||
|
|
17fe69f551 | ||
|
|
bceaab0496 | ||
|
|
70dd05916f | ||
|
|
60a12e906e | ||
|
|
ec051a8939 | ||
|
|
60d3c66a8b | ||
|
|
bd4ba96c43 | ||
|
|
10a46fd33c | ||
|
|
c874eb6b11 | ||
|
|
f3a323695f | ||
|
|
408ba78bd0 | ||
|
|
58cc49bc88 | ||
|
|
753ea846ff | ||
|
|
1b946b59ec | ||
|
|
4569191113 | ||
|
|
62890c3ec4 | ||
|
|
bd115e7f5f | ||
|
|
bd17662005 | ||
|
|
b7f22b1896 | ||
|
|
897c683980 | ||
|
|
2b974e12ed | ||
|
|
372adfed6b | ||
|
|
dbab9cadb4 | ||
|
|
06244716e6 | ||
|
|
f09760389a | ||
|
|
84e6d3cad5 | ||
|
|
3399e3761e | ||
|
|
1c48b2218b | ||
|
|
5ac5d86f5e | ||
|
|
5e4764af9c | ||
|
|
e2a39de7d1 | ||
|
|
6c68b48917 | ||
|
|
7e8a61c73d | ||
|
|
991e94dd6a | ||
|
|
2ffeecb813 | ||
|
|
3dd855aef1 | ||
|
|
713aca440a | ||
|
|
70bb684d9e | ||
|
|
4034f791c3 | ||
|
|
b9875a231b | ||
|
|
062a70cf20 | ||
|
|
c12d45077a | ||
|
|
6d6412753d | ||
|
|
275e12e7c1 | ||
|
|
77d7508cee | ||
|
|
9601660751 | ||
|
|
45c6fa0591 | ||
|
|
95ecb8e229 | ||
|
|
cb0707176c | ||
|
|
d7247f8b84 | ||
|
|
776703fab7 | ||
|
|
34cde71238 | ||
|
|
f631075be3 | ||
|
|
3f19534301 | ||
|
|
24dec192ee | ||
|
|
82d88ed4eb | ||
|
|
af0773c58a | ||
|
|
15e6583d72 | ||
|
|
4eb5f4e47f | ||
|
|
a0f5c706ff | ||
|
|
702a550659 | ||
|
|
38a06c7ab9 | ||
|
|
a6074878bb | ||
|
|
fb348c7116 | ||
|
|
24419863ec | ||
|
|
c4d3d9c683 | ||
|
|
1672cd9280 | ||
|
|
6575a6499a | ||
|
|
e360e4658e | ||
|
|
107933c363 | ||
|
|
667a696ca5 | ||
|
|
7d0d242072 | ||
|
|
d554136f70 | ||
|
|
34723e2f3e | ||
|
|
11794b3fe3 | ||
|
|
3997916d77 | ||
|
|
b09a2256dc | ||
|
|
95a45bb4dc | ||
|
|
7cd03c797c | ||
|
|
1200f9887a | ||
|
|
81d22f1eb2 | ||
|
|
2fe0326280 | ||
|
|
094984f564 | ||
|
|
52cd6ff006 | ||
|
|
939bec2114 | ||
|
|
320fddfd52 | ||
|
|
ab39283ae6 | ||
|
|
6386905616 | ||
|
|
d986b976e5 | ||
|
|
a600921c0b | ||
|
|
af2f4af8a2 | ||
|
|
49a5e74283 | ||
|
|
b98b5f2ba4 | ||
|
|
d245ff7bb1 | ||
|
|
97db492531 | ||
|
|
30e7392933 | ||
|
|
a54171f2c2 | ||
|
|
cd03da3260 | ||
|
|
4c47bef582 | ||
|
|
ec1c4d07de | ||
|
|
4d5103978b | ||
|
|
3c2c2786ed | ||
|
|
cd482e780a | ||
|
|
4d81c3148d | ||
|
|
1b1b9bd98d | ||
|
|
473cf29c9f | ||
|
|
cbde237b12 | ||
|
|
998dc31eb0 | ||
|
|
2505e8ab3b | ||
|
|
858fc4d78f | ||
|
|
3e500ea18e | ||
|
|
58bf96c298 | ||
|
|
66ef942572 | ||
|
|
9bbdb4b765 | ||
|
|
2b4b1cf7e3 | ||
|
|
9b29f26217 | ||
|
|
392b930f2d | ||
|
|
9df6f80bb7 | ||
|
|
f7b47c0436 | ||
|
|
09addaadc3 | ||
|
|
a07b8a6bd3 | ||
|
|
8bfc57430d | ||
|
|
a8c6dd0043 | ||
|
|
2d879510e4 | ||
|
|
13e61fc3a0 | ||
|
|
de1958e995 | ||
|
|
198b422eaf | ||
|
|
63cef6632e | ||
|
|
2611dcc0f1 | ||
|
|
55193fbf66 | ||
|
|
8b8dd4f68c | ||
|
|
ae77e72821 | ||
|
|
39e4ea155c | ||
|
|
a5b01bf8ee | ||
|
|
5516b427d8 | ||
|
|
de84c40868 | ||
|
|
39766a2d97 | ||
|
|
593b6ae6ed | ||
|
|
8bb1767c69 | ||
|
|
7b03e60f9d | ||
|
|
ac9f24a781 | ||
|
|
54c4a4249a | ||
|
|
36dd3e9609 | ||
|
|
69d1e03e60 | ||
|
|
a2c0c488eb | ||
|
|
ddbe0f6ce5 | ||
|
|
42108089ed | ||
|
|
d4ade51fba | ||
|
|
84d1693419 | ||
|
|
12f1050000 | ||
|
|
6b981972f0 | ||
|
|
eafe30d52c | ||
|
|
abbd4d3146 | ||
|
|
1d350853bd |
@@ -1,11 +1,11 @@
|
|||||||
FROM cypress/browsers:node16.18.0-chrome90-ff88
|
FROM cypress/browsers:node18.12.0-chrome106-ff106
|
||||||
|
|
||||||
ENV APP /usr/src/app
|
ENV APP /usr/src/app
|
||||||
WORKDIR $APP
|
WORKDIR $APP
|
||||||
|
|
||||||
COPY package.json yarn.lock .yarnrc $APP/
|
COPY package.json yarn.lock .yarnrc $APP/
|
||||||
COPY viz-lib $APP/viz-lib
|
COPY viz-lib $APP/viz-lib
|
||||||
RUN npm install yarn@1.22.19 -g && yarn --frozen-lockfile --network-concurrency 1 > /dev/null
|
RUN npm install yarn@1.22.22 -g && yarn --frozen-lockfile --network-concurrency 1 > /dev/null
|
||||||
|
|
||||||
COPY . $APP
|
COPY . $APP
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '2.2'
|
|
||||||
services:
|
services:
|
||||||
redash:
|
redash:
|
||||||
build: ../
|
build: ../
|
||||||
@@ -19,7 +18,7 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
postgres:
|
postgres:
|
||||||
image: pgautoupgrade/pgautoupgrade:15-alpine3.8
|
image: postgres:18-alpine
|
||||||
command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF"
|
command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
version: "2.2"
|
|
||||||
x-redash-service: &redash-service
|
x-redash-service: &redash-service
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
@@ -67,7 +66,7 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
postgres:
|
postgres:
|
||||||
image: pgautoupgrade/pgautoupgrade:15-alpine3.8
|
image: postgres:18-alpine
|
||||||
command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF"
|
command: "postgres -c fsync=off -c full_page_writes=off -c synchronous_commit=OFF"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
client/.tmp/
|
client/.tmp/
|
||||||
client/dist/
|
|
||||||
node_modules/
|
node_modules/
|
||||||
viz-lib/node_modules/
|
viz-lib/node_modules/
|
||||||
.tmp/
|
.tmp/
|
||||||
|
|||||||
162
.github/workflows/ci.yml
vendored
162
.github/workflows/ci.yml
vendored
@@ -4,16 +4,23 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: 16.20.1
|
NODE_VERSION: 18
|
||||||
|
YARN_VERSION: 1.22.22
|
||||||
jobs:
|
jobs:
|
||||||
backend-lint:
|
backend-lint:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- if: github.event.pull_request.mergeable == 'false'
|
||||||
|
name: Exit if PR is not mergeable
|
||||||
|
run: exit 1
|
||||||
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
- uses: actions/setup-python@v4
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
- run: sudo pip install black==23.1.0 ruff==0.0.287
|
- run: sudo pip install black==23.1.0 ruff==0.0.287
|
||||||
@@ -24,14 +31,18 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: backend-lint
|
needs: backend-lint
|
||||||
env:
|
env:
|
||||||
COMPOSE_FILE: .ci/docker-compose.ci.yml
|
COMPOSE_FILE: .ci/compose.ci.yaml
|
||||||
COMPOSE_PROJECT_NAME: redash
|
COMPOSE_PROJECT_NAME: redash
|
||||||
COMPOSE_DOCKER_CLI_BUILD: 1
|
COMPOSE_DOCKER_CLI_BUILD: 1
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- if: github.event.pull_request.mergeable == 'false'
|
||||||
|
name: Exit if PR is not mergeable
|
||||||
|
run: exit 1
|
||||||
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- name: Build Docker Images
|
- name: Build Docker Images
|
||||||
run: |
|
run: |
|
||||||
set -x
|
set -x
|
||||||
@@ -49,15 +60,17 @@ jobs:
|
|||||||
mkdir -p /tmp/test-results/unit-tests
|
mkdir -p /tmp/test-results/unit-tests
|
||||||
docker cp tests:/app/coverage.xml ./coverage.xml
|
docker cp tests:/app/coverage.xml ./coverage.xml
|
||||||
docker cp tests:/app/junit.xml /tmp/test-results/unit-tests/results.xml
|
docker cp tests:/app/junit.xml /tmp/test-results/unit-tests/results.xml
|
||||||
- name: Upload coverage reports to Codecov
|
# - name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@v3
|
# uses: codecov/codecov-action@v3
|
||||||
|
# with:
|
||||||
|
# token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
- name: Store Test Results
|
- name: Store Test Results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: backend-test-results
|
||||||
path: /tmp/test-results
|
path: /tmp/test-results
|
||||||
- name: Store Coverage Results
|
- name: Store Coverage Results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: coverage
|
name: coverage
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
@@ -65,39 +78,47 @@ jobs:
|
|||||||
frontend-lint:
|
frontend-lint:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- if: github.event.pull_request.mergeable == 'false'
|
||||||
|
name: Exit if PR is not mergeable
|
||||||
|
run: exit 1
|
||||||
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
- uses: actions/setup-node@v3
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
npm install --global --force yarn@1.22.19
|
npm install --global --force yarn@$YARN_VERSION
|
||||||
yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
|
yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
|
||||||
- name: Run Lint
|
- name: Run Lint
|
||||||
run: yarn lint:ci
|
run: yarn lint:ci
|
||||||
- name: Store Test Results
|
- name: Store Test Results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-results
|
name: frontend-test-results
|
||||||
path: /tmp/test-results
|
path: /tmp/test-results
|
||||||
|
|
||||||
frontend-unit-tests:
|
frontend-unit-tests:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: frontend-lint
|
needs: frontend-lint
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- if: github.event.pull_request.mergeable == 'false'
|
||||||
|
name: Exit if PR is not mergeable
|
||||||
|
run: exit 1
|
||||||
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
- uses: actions/setup-node@v3
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
npm install --global --force yarn@1.22.19
|
npm install --global --force yarn@$YARN_VERSION
|
||||||
yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
|
yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
|
||||||
- name: Run App Tests
|
- name: Run App Tests
|
||||||
run: yarn test
|
run: yarn test
|
||||||
@@ -109,18 +130,22 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: frontend-lint
|
needs: frontend-lint
|
||||||
env:
|
env:
|
||||||
COMPOSE_FILE: .ci/docker-compose.cypress.yml
|
COMPOSE_FILE: .ci/compose.cypress.yaml
|
||||||
COMPOSE_PROJECT_NAME: cypress
|
COMPOSE_PROJECT_NAME: cypress
|
||||||
PERCY_TOKEN_ENCODED: ZGRiY2ZmZDQ0OTdjMzM5ZWE0ZGQzNTZiOWNkMDRjOTk4Zjg0ZjMxMWRmMDZiM2RjOTYxNDZhOGExMjI4ZDE3MA==
|
|
||||||
CYPRESS_PROJECT_ID_ENCODED: OTI0Y2th
|
|
||||||
CYPRESS_RECORD_KEY_ENCODED: YzA1OTIxMTUtYTA1Yy00NzQ2LWEyMDMtZmZjMDgwZGI2ODgx
|
|
||||||
CYPRESS_INSTALL_BINARY: 0
|
CYPRESS_INSTALL_BINARY: 0
|
||||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
|
||||||
|
# PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||||
|
# CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
|
||||||
|
# CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- if: github.event.pull_request.mergeable == 'false'
|
||||||
|
name: Exit if PR is not mergeable
|
||||||
|
run: exit 1
|
||||||
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
- uses: actions/setup-node@v3
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
@@ -130,7 +155,7 @@ jobs:
|
|||||||
echo "CODE_COVERAGE=true" >> "$GITHUB_ENV"
|
echo "CODE_COVERAGE=true" >> "$GITHUB_ENV"
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: |
|
run: |
|
||||||
npm install --global --force yarn@1.22.19
|
npm install --global --force yarn@$YARN_VERSION
|
||||||
yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
|
yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
|
||||||
- name: Setup Redash Server
|
- name: Setup Redash Server
|
||||||
run: |
|
run: |
|
||||||
@@ -146,92 +171,7 @@ jobs:
|
|||||||
- name: Copy Code Coverage Results
|
- name: Copy Code Coverage Results
|
||||||
run: docker cp cypress:/usr/src/app/coverage ./coverage || true
|
run: docker cp cypress:/usr/src/app/coverage ./coverage || true
|
||||||
- name: Store Coverage Results
|
- name: Store Coverage Results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: coverage
|
name: coverage
|
||||||
path: coverage
|
path: coverage
|
||||||
|
|
||||||
build-skip-check:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
outputs:
|
|
||||||
skip: ${{ steps.skip-check.outputs.skip }}
|
|
||||||
steps:
|
|
||||||
- name: Skip?
|
|
||||||
id: skip-check
|
|
||||||
run: |
|
|
||||||
if [[ "${{ vars.DOCKER_USER }}" == '' ]]; then
|
|
||||||
echo 'Docker user is empty. Skipping build+push'
|
|
||||||
echo skip=true >> "$GITHUB_OUTPUT"
|
|
||||||
elif [[ "${{ secrets.DOCKER_PASS }}" == '' ]]; then
|
|
||||||
echo 'Docker password is empty. Skipping build+push'
|
|
||||||
echo skip=true >> "$GITHUB_OUTPUT"
|
|
||||||
elif [[ "${{ github.ref_name }}" != 'master' ]]; then
|
|
||||||
echo 'Ref name is not `master`. Skipping build+push'
|
|
||||||
echo skip=true >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo 'Docker user and password are set and branch is `master`.'
|
|
||||||
echo 'Building + pushing `preview` image.'
|
|
||||||
echo skip=false >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
build-docker-image:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
needs:
|
|
||||||
- backend-unit-tests
|
|
||||||
- frontend-unit-tests
|
|
||||||
- frontend-e2e-tests
|
|
||||||
- build-skip-check
|
|
||||||
if: needs.build-skip-check.outputs.skip == 'false'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
|
||||||
cache: 'yarn'
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
npm install --global --force yarn@1.22.19
|
|
||||||
yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
timeout-minutes: 1
|
|
||||||
uses: docker/setup-qemu-action@v2.2.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ vars.DOCKER_USER }}
|
|
||||||
password: ${{ secrets.DOCKER_PASS }}
|
|
||||||
|
|
||||||
- name: Bump version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
.ci/update_version
|
|
||||||
VERSION=$(jq -r .version package.json)
|
|
||||||
VERSION_TAG="${VERSION}.b${GITHUB_RUN_ID}.${GITHUB_RUN_NUMBER}"
|
|
||||||
echo "VERSION_TAG=$VERSION_TAG" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Build and push preview image to Docker Hub
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
redash/redash:preview
|
|
||||||
redash/preview:${{ steps.version.outputs.VERSION_TAG }}
|
|
||||||
context: .
|
|
||||||
build-args: |
|
|
||||||
test_all_deps=true
|
|
||||||
cache-from: type=ghq
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
env:
|
|
||||||
DOCKER_CONTENT_TRUST: true
|
|
||||||
|
|
||||||
- name: "Failure: output container logs to console"
|
|
||||||
if: failure()
|
|
||||||
run: docker compose logs
|
|
||||||
|
|||||||
85
.github/workflows/periodic-snapshot.yml
vendored
85
.github/workflows/periodic-snapshot.yml
vendored
@@ -1,27 +1,86 @@
|
|||||||
name: Periodic Snapshot
|
name: Periodic Snapshot
|
||||||
|
|
||||||
# 10 minutes after midnight on the first of every month
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "10 0 1 * *"
|
- cron: '10 0 1 * *' # 10 minutes after midnight on the first day of every month
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
bump:
|
||||||
|
description: 'Bump the last digit of the version'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
version:
|
||||||
|
description: 'Specific version to set'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: write
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump-version-and-tag:
|
bump-version-and-tag:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref_name == github.event.repository.default_branch
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ssh-key: ${{ secrets.ACTION_PUSH_KEY }}
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
date="$(date +%y.%m).0-dev"
|
git config user.name 'github-actions[bot]'
|
||||||
gawk -i inplace -F: -v q=\" -v tag=$date '/^ "version": / { print $1 FS, q tag q ","; next} { print }' package.json
|
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||||
gawk -i inplace -F= -v q=\" -v tag=$date '/^__version__ =/ { print $1 FS, q tag q; next} { print }' redash/__init__.py
|
|
||||||
gawk -i inplace -F= -v q=\" -v tag=$date '/^version =/ { print $1 FS, q tag q; next} { print }' pyproject.toml
|
# Function to bump the version
|
||||||
git config user.name github-actions
|
bump_version() {
|
||||||
git config user.email github-actions@github.com
|
local version="$1"
|
||||||
|
local IFS=.
|
||||||
|
read -r major minor patch <<< "$version"
|
||||||
|
patch=$((patch + 1))
|
||||||
|
echo "$major.$minor.$patch-dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine the new version tag
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
BUMP_INPUT="${{ github.event.inputs.bump }}"
|
||||||
|
SPECIFIC_VERSION="${{ github.event.inputs.version }}"
|
||||||
|
|
||||||
|
# Check if both bump and specific version are provided
|
||||||
|
if [ "$BUMP_INPUT" = "true" ] && [ -n "$SPECIFIC_VERSION" ]; then
|
||||||
|
echo "::error::Error: Cannot specify both bump and specific version."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$SPECIFIC_VERSION" ]; then
|
||||||
|
TAG_NAME="$SPECIFIC_VERSION-dev"
|
||||||
|
elif [ "$BUMP_INPUT" = "true" ]; then
|
||||||
|
CURRENT_VERSION=$(grep '"version":' package.json | awk -F\" '{print $4}')
|
||||||
|
TAG_NAME=$(bump_version "$CURRENT_VERSION")
|
||||||
|
else
|
||||||
|
echo "No version bump or specific version provided for manual dispatch."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TAG_NAME="$(date +%y.%m).0-dev"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "New version tag: $TAG_NAME"
|
||||||
|
|
||||||
|
# Update version in files
|
||||||
|
gawk -i inplace -F: -v q=\" -v tag=${TAG_NAME} '/^ "version": / { print $1 FS, q tag q ","; next} { print }' package.json
|
||||||
|
gawk -i inplace -F= -v q=\" -v tag=${TAG_NAME} '/^__version__ =/ { print $1 FS, q tag q; next} { print }' redash/__init__.py
|
||||||
|
gawk -i inplace -F= -v q=\" -v tag=${TAG_NAME} '/^version =/ { print $1 FS, q tag q; next} { print }' pyproject.toml
|
||||||
|
|
||||||
git add package.json redash/__init__.py pyproject.toml
|
git add package.json redash/__init__.py pyproject.toml
|
||||||
git commit -m "Snapshot: ${date}"
|
git commit -m "Snapshot: ${TAG_NAME}"
|
||||||
git push origin
|
git tag ${TAG_NAME}
|
||||||
git tag $date
|
git push --atomic origin master refs/tags/${TAG_NAME}
|
||||||
git push origin $date
|
|
||||||
|
# Run the 'preview-image' workflow if run this workflow manually
|
||||||
|
# For more information, please see the: https://docs.github.com/en/actions/security-guides/automatic-token-authentication
|
||||||
|
if [ "$BUMP_INPUT" = "true" ] || [ -n "$SPECIFIC_VERSION" ]; then
|
||||||
|
gh workflow run preview-image.yml --ref $TAG_NAME
|
||||||
|
fi
|
||||||
|
|||||||
185
.github/workflows/preview-image.yml
vendored
Normal file
185
.github/workflows/preview-image.yml
vendored
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
name: Preview Image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*-dev'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dockerRepository:
|
||||||
|
description: 'Docker repository'
|
||||||
|
required: true
|
||||||
|
default: 'preview'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- preview
|
||||||
|
- redash
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 18
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-skip-check:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
skip: ${{ steps.skip-check.outputs.skip }}
|
||||||
|
steps:
|
||||||
|
- name: Skip?
|
||||||
|
id: skip-check
|
||||||
|
run: |
|
||||||
|
if [[ "${{ vars.DOCKER_USER }}" == '' ]]; then
|
||||||
|
echo 'Docker user is empty. Skipping build+push'
|
||||||
|
echo skip=true >> "$GITHUB_OUTPUT"
|
||||||
|
elif [[ "${{ secrets.DOCKER_PASS }}" == '' ]]; then
|
||||||
|
echo 'Docker password is empty. Skipping build+push'
|
||||||
|
echo skip=true >> "$GITHUB_OUTPUT"
|
||||||
|
elif [[ "${{ vars.DOCKER_REPOSITORY }}" == '' ]]; then
|
||||||
|
echo 'Docker repository is empty. Skipping build+push'
|
||||||
|
echo skip=true >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo 'Docker user and password are set and branch is `master`.'
|
||||||
|
echo 'Building + pushing `preview` image.'
|
||||||
|
echo skip=false >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
build-docker-image:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
include:
|
||||||
|
- arch: amd64
|
||||||
|
os: ubuntu-22.04
|
||||||
|
- arch: arm64
|
||||||
|
os: ubuntu-22.04-arm
|
||||||
|
outputs:
|
||||||
|
VERSION_TAG: ${{ steps.version.outputs.VERSION_TAG }}
|
||||||
|
needs:
|
||||||
|
- build-skip-check
|
||||||
|
if: needs.build-skip-check.outputs.skip == 'false'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
ref: ${{ github.event.push.after }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_PASS }}
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||||
|
run: |
|
||||||
|
npm install --global --force yarn@1.22.22
|
||||||
|
yarn cache clean && yarn --frozen-lockfile --network-concurrency 1
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
.ci/update_version
|
||||||
|
VERSION_TAG=$(jq -r .version package.json)
|
||||||
|
echo "VERSION_TAG=$VERSION_TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build and push preview image to Docker Hub
|
||||||
|
id: build-preview
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
if: ${{ github.event.inputs.dockerRepository == 'preview' || !github.event.workflow_run }}
|
||||||
|
with:
|
||||||
|
tags: |
|
||||||
|
${{ vars.DOCKER_REPOSITORY }}/redash
|
||||||
|
${{ vars.DOCKER_REPOSITORY }}/preview
|
||||||
|
context: .
|
||||||
|
build-args: |
|
||||||
|
test_all_deps=true
|
||||||
|
outputs: type=image,push-by-digest=true,push=true
|
||||||
|
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||||
|
env:
|
||||||
|
DOCKER_CONTENT_TRUST: true
|
||||||
|
|
||||||
|
- name: Build and push release image to Docker Hub
|
||||||
|
id: build-release
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
if: ${{ github.event.inputs.dockerRepository == 'redash' }}
|
||||||
|
with:
|
||||||
|
tags: |
|
||||||
|
${{ vars.DOCKER_REPOSITORY }}/redash:${{ steps.version.outputs.VERSION_TAG }}
|
||||||
|
context: .
|
||||||
|
build-args: |
|
||||||
|
test_all_deps=true
|
||||||
|
outputs: type=image,push-by-digest=false,push=true
|
||||||
|
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||||
|
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||||
|
env:
|
||||||
|
DOCKER_CONTENT_TRUST: true
|
||||||
|
|
||||||
|
- name: "Failure: output container logs to console"
|
||||||
|
if: failure()
|
||||||
|
run: docker compose logs
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
|
if [[ "${{ github.event.inputs.dockerRepository }}" == 'preview' || !github.event.workflow_run ]]; then
|
||||||
|
digest="${{ steps.build-preview.outputs.digest}}"
|
||||||
|
else
|
||||||
|
digest="${{ steps.build-release.outputs.digest}}"
|
||||||
|
fi
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: digests-${{ matrix.arch }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
merge-docker-image:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: build-docker-image
|
||||||
|
steps:
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_PASS }}
|
||||||
|
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Create and push manifest for the preview image
|
||||||
|
if: ${{ github.event.inputs.dockerRepository == 'preview' || !github.event.workflow_run }}
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t ${{ vars.DOCKER_REPOSITORY }}/redash:preview \
|
||||||
|
$(printf '${{ vars.DOCKER_REPOSITORY }}/redash:preview@sha256:%s ' *)
|
||||||
|
docker buildx imagetools create -t ${{ vars.DOCKER_REPOSITORY }}/preview:${{ needs.build-docker-image.outputs.VERSION_TAG }} \
|
||||||
|
$(printf '${{ vars.DOCKER_REPOSITORY }}/preview:${{ needs.build-docker-image.outputs.VERSION_TAG }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
- name: Create and push manifest for the release image
|
||||||
|
if: ${{ github.event.inputs.dockerRepository == 'redash' }}
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t ${{ vars.DOCKER_REPOSITORY }}/redash:${{ needs.build-docker-image.outputs.VERSION_TAG }} \
|
||||||
|
$(printf '${{ vars.DOCKER_REPOSITORY }}/redash:${{ needs.build-docker-image.outputs.VERSION_TAG }}@sha256:%s ' *)
|
||||||
36
.github/workflows/restyled.yml
vendored
Normal file
36
.github/workflows/restyled.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Restyled
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
restyled:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- uses: restyled-io/actions/setup@v4
|
||||||
|
- id: restyler
|
||||||
|
uses: restyled-io/actions/run@v4
|
||||||
|
with:
|
||||||
|
fail-on-differences: true
|
||||||
|
|
||||||
|
- if: |
|
||||||
|
!cancelled() &&
|
||||||
|
steps.restyler.outputs.success == 'true' &&
|
||||||
|
github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
with:
|
||||||
|
base: ${{ steps.restyler.outputs.restyled-base }}
|
||||||
|
branch: ${{ steps.restyler.outputs.restyled-head }}
|
||||||
|
title: ${{ steps.restyler.outputs.restyled-title }}
|
||||||
|
body: ${{ steps.restyler.outputs.restyled-body }}
|
||||||
|
labels: "restyled"
|
||||||
|
reviewers: ${{ github.event.pull_request.user.login }}
|
||||||
|
delete-branch: true
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ client/dist
|
|||||||
_build
|
_build
|
||||||
.vscode
|
.vscode
|
||||||
.env
|
.env
|
||||||
|
.tool-versions
|
||||||
|
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ request_review: author
|
|||||||
#
|
#
|
||||||
# These can be used to tell other automation to avoid our PRs.
|
# These can be used to tell other automation to avoid our PRs.
|
||||||
#
|
#
|
||||||
labels: ["Skip CI"]
|
labels:
|
||||||
|
- restyled
|
||||||
|
- "Skip CI"
|
||||||
|
|
||||||
# Labels to ignore
|
# Labels to ignore
|
||||||
#
|
#
|
||||||
@@ -50,13 +52,13 @@ labels: ["Skip CI"]
|
|||||||
# Restylers to run, and how
|
# Restylers to run, and how
|
||||||
restylers:
|
restylers:
|
||||||
- name: black
|
- name: black
|
||||||
image: restyled/restyler-black:v19.10b0
|
image: restyled/restyler-black:v24.4.2
|
||||||
include:
|
include:
|
||||||
- redash
|
- redash
|
||||||
- tests
|
- tests
|
||||||
- migrations/versions
|
- migrations/versions
|
||||||
- name: prettier
|
- name: prettier
|
||||||
image: restyled/restyler-prettier:v1.19.1-2
|
image: restyled/restyler-prettier:v3.3.2-2
|
||||||
command:
|
command:
|
||||||
- prettier
|
- prettier
|
||||||
- --write
|
- --write
|
||||||
|
|||||||
56
Dockerfile
56
Dockerfile
@@ -1,6 +1,6 @@
|
|||||||
FROM node:16.20.1 as frontend-builder
|
FROM node:18-bookworm AS frontend-builder
|
||||||
|
|
||||||
RUN npm install --global --force yarn@1.22.19
|
RUN npm install --global --force yarn@1.22.22
|
||||||
|
|
||||||
# Controls whether to build the frontend assets
|
# Controls whether to build the frontend assets
|
||||||
ARG skip_frontend_build
|
ARG skip_frontend_build
|
||||||
@@ -14,18 +14,30 @@ USER redash
|
|||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
COPY --chown=redash package.json yarn.lock .yarnrc /frontend/
|
COPY --chown=redash package.json yarn.lock .yarnrc /frontend/
|
||||||
COPY --chown=redash viz-lib /frontend/viz-lib
|
COPY --chown=redash viz-lib /frontend/viz-lib
|
||||||
|
COPY --chown=redash scripts /frontend/scripts
|
||||||
|
|
||||||
# Controls whether to instrument code for coverage information
|
# Controls whether to instrument code for coverage information
|
||||||
ARG code_coverage
|
ARG code_coverage
|
||||||
ENV BABEL_ENV=${code_coverage:+test}
|
ENV BABEL_ENV=${code_coverage:+test}
|
||||||
|
|
||||||
|
# Avoid issues caused by lags in disk and network I/O speeds when working on top of QEMU emulation for multi-platform image building.
|
||||||
|
RUN yarn config set network-timeout 300000
|
||||||
|
|
||||||
RUN if [ "x$skip_frontend_build" = "x" ] ; then yarn --frozen-lockfile --network-concurrency 1; fi
|
RUN if [ "x$skip_frontend_build" = "x" ] ; then yarn --frozen-lockfile --network-concurrency 1; fi
|
||||||
|
|
||||||
COPY --chown=redash client /frontend/client
|
COPY --chown=redash client /frontend/client
|
||||||
COPY --chown=redash webpack.config.js /frontend/
|
COPY --chown=redash webpack.config.js /frontend/
|
||||||
RUN if [ "x$skip_frontend_build" = "x" ] ; then yarn build; else mkdir -p /frontend/client/dist && touch /frontend/client/dist/multi_org.html && touch /frontend/client/dist/index.html; fi
|
RUN <<EOF
|
||||||
|
if [ "x$skip_frontend_build" = "x" ]; then
|
||||||
|
yarn build
|
||||||
|
else
|
||||||
|
mkdir -p /frontend/client/dist
|
||||||
|
touch /frontend/client/dist/multi_org.html
|
||||||
|
touch /frontend/client/dist/index.html
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
FROM python:3.8-slim-buster
|
FROM python:3.10-slim-bookworm
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
@@ -63,28 +75,34 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG databricks_odbc_driver_url=https://databricks-bi-artifacts.s3.us-east-2.amazonaws.com/simbaspark-drivers/odbc/2.6.26/SimbaSparkODBC-2.6.26.1045-Debian-64bit.zip
|
ARG databricks_odbc_driver_url=https://databricks-bi-artifacts.s3.us-east-2.amazonaws.com/simbaspark-drivers/odbc/2.6.26/SimbaSparkODBC-2.6.26.1045-Debian-64bit.zip
|
||||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
RUN <<EOF
|
||||||
curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \
|
if [ "$TARGETPLATFORM" = "linux/amd64" ]; then
|
||||||
&& curl https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list \
|
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
|
||||||
&& apt-get update \
|
curl https://packages.microsoft.com/config/debian/12/prod.list > /etc/apt/sources.list.d/mssql-release.list
|
||||||
&& ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql17 \
|
apt-get update
|
||||||
&& apt-get clean \
|
ACCEPT_EULA=Y apt-get install -y --no-install-recommends msodbcsql18
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
apt-get clean
|
||||||
&& curl "$databricks_odbc_driver_url" --location --output /tmp/simba_odbc.zip \
|
rm -rf /var/lib/apt/lists/*
|
||||||
&& chmod 600 /tmp/simba_odbc.zip \
|
curl "$databricks_odbc_driver_url" --location --output /tmp/simba_odbc.zip
|
||||||
&& unzip /tmp/simba_odbc.zip -d /tmp/simba \
|
chmod 600 /tmp/simba_odbc.zip
|
||||||
&& dpkg -i /tmp/simba/*.deb \
|
unzip /tmp/simba_odbc.zip -d /tmp/simba
|
||||||
&& printf "[Simba]\nDriver = /opt/simba/spark/lib/64/libsparkodbc_sb64.so" >> /etc/odbcinst.ini \
|
dpkg -i /tmp/simba/*.deb
|
||||||
&& rm /tmp/simba_odbc.zip \
|
printf "[Simba]\nDriver = /opt/simba/spark/lib/64/libsparkodbc_sb64.so" >> /etc/odbcinst.ini
|
||||||
&& rm -rf /tmp/simba; fi
|
rm /tmp/simba_odbc.zip
|
||||||
|
rm -rf /tmp/simba
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV POETRY_VERSION=1.6.1
|
ENV POETRY_VERSION=2.1.4
|
||||||
ENV POETRY_HOME=/etc/poetry
|
ENV POETRY_HOME=/etc/poetry
|
||||||
ENV POETRY_VIRTUALENVS_CREATE=false
|
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
|
# Avoid crashes, including corrupted cache artifacts, when building multi-platform images with GitHub Actions.
|
||||||
|
RUN /etc/poetry/bin/poetry cache clear pypi --all
|
||||||
|
|
||||||
COPY pyproject.toml poetry.lock ./
|
COPY pyproject.toml poetry.lock ./
|
||||||
|
|
||||||
ARG POETRY_OPTIONS="--no-root --no-interaction --no-ansi"
|
ARG POETRY_OPTIONS="--no-root --no-interaction --no-ansi"
|
||||||
|
|||||||
17
Makefile
17
Makefile
@@ -4,7 +4,11 @@ compose_build: .env
|
|||||||
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose build
|
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose build
|
||||||
|
|
||||||
up:
|
up:
|
||||||
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose up -d --build
|
docker compose up -d redis postgres --remove-orphans
|
||||||
|
docker compose exec -u postgres postgres psql postgres --csv \
|
||||||
|
-1tqc "SELECT table_name FROM information_schema.tables WHERE table_name = 'organizations'" 2> /dev/null \
|
||||||
|
| grep -q "organizations" || make create_database
|
||||||
|
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose up -d --build --remove-orphans
|
||||||
|
|
||||||
test_db:
|
test_db:
|
||||||
@for i in `seq 1 5`; do \
|
@for i in `seq 1 5`; do \
|
||||||
@@ -17,7 +21,16 @@ create_database: .env
|
|||||||
docker compose run server create_db
|
docker compose run server create_db
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
docker compose down && docker compose rm
|
docker compose down
|
||||||
|
docker compose --project-name cypress down
|
||||||
|
docker compose rm --stop --force
|
||||||
|
docker compose --project-name cypress rm --stop --force
|
||||||
|
docker image rm --force \
|
||||||
|
cypress-server:latest cypress-worker:latest cypress-scheduler:latest \
|
||||||
|
redash-server:latest redash-worker:latest redash-scheduler:latest
|
||||||
|
docker container prune --force
|
||||||
|
docker image prune --force
|
||||||
|
docker volume prune --force
|
||||||
|
|
||||||
down:
|
down:
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://redash.io/help/)
|
[](https://redash.io/help/)
|
||||||
[](https://datree.io/?src=badge)
|
|
||||||
[](https://github.com/getredash/redash/actions)
|
[](https://github.com/getredash/redash/actions)
|
||||||
|
|
||||||
Redash is designed to enable anyone, regardless of the level of technical sophistication, to harness the power of data big and small. SQL users leverage Redash to explore, query, visualize, and share data from any data sources. Their work in turn enables anybody in their organization to use the data. Every day, millions of users at thousands of organizations around the world use Redash to develop insights and make data-driven decisions.
|
Redash is designed to enable anyone, regardless of the level of technical sophistication, to harness the power of data big and small. SQL users leverage Redash to explore, query, visualize, and share data from any data sources. Their work in turn enables anybody in their organization to use the data. Every day, millions of users at thousands of organizations around the world use Redash to develop insights and make data-driven decisions.
|
||||||
@@ -47,6 +46,7 @@ Redash supports more than 35 SQL and NoSQL [data sources](https://redash.io/help
|
|||||||
- Dgraph
|
- Dgraph
|
||||||
- Apache Drill
|
- Apache Drill
|
||||||
- Apache Druid
|
- Apache Druid
|
||||||
|
- e6data
|
||||||
- Eccenca Corporate Memory
|
- Eccenca Corporate Memory
|
||||||
- Elasticsearch
|
- Elasticsearch
|
||||||
- Exasol
|
- Exasol
|
||||||
@@ -61,6 +61,7 @@ Redash supports more than 35 SQL and NoSQL [data sources](https://redash.io/help
|
|||||||
- Apache Hive
|
- Apache Hive
|
||||||
- Apache Impala
|
- Apache Impala
|
||||||
- InfluxDB
|
- InfluxDB
|
||||||
|
- InfluxDBv2
|
||||||
- IBM Netezza Performance Server
|
- IBM Netezza Performance Server
|
||||||
- JIRA (JQL)
|
- JIRA (JQL)
|
||||||
- JSON
|
- JSON
|
||||||
@@ -83,6 +84,7 @@ Redash supports more than 35 SQL and NoSQL [data sources](https://redash.io/help
|
|||||||
- Python
|
- Python
|
||||||
- Qubole
|
- Qubole
|
||||||
- Rockset
|
- Rockset
|
||||||
|
- RisingWave
|
||||||
- Salesforce
|
- Salesforce
|
||||||
- ScyllaDB
|
- ScyllaDB
|
||||||
- Shell Scripts
|
- Shell Scripts
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ server() {
|
|||||||
MAX_REQUESTS=${MAX_REQUESTS:-1000}
|
MAX_REQUESTS=${MAX_REQUESTS:-1000}
|
||||||
MAX_REQUESTS_JITTER=${MAX_REQUESTS_JITTER:-100}
|
MAX_REQUESTS_JITTER=${MAX_REQUESTS_JITTER:-100}
|
||||||
TIMEOUT=${REDASH_GUNICORN_TIMEOUT:-60}
|
TIMEOUT=${REDASH_GUNICORN_TIMEOUT:-60}
|
||||||
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w${REDASH_WEB_WORKERS:-4} redash.wsgi:app --max-requests $MAX_REQUESTS --max-requests-jitter $MAX_REQUESTS_JITTER --timeout $TIMEOUT
|
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w${REDASH_WEB_WORKERS:-4} redash.wsgi:app --max-requests $MAX_REQUESTS --max-requests-jitter $MAX_REQUESTS_JITTER --timeout $TIMEOUT --limit-request-line ${REDASH_GUNICORN_LIMIT_REQUEST_LINE:-0}
|
||||||
}
|
}
|
||||||
|
|
||||||
create_db() {
|
create_db() {
|
||||||
@@ -67,7 +67,7 @@ help() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo "shell -- open shell"
|
echo "shell -- open shell"
|
||||||
echo "dev_server -- start Flask development server with debugger and auto reload"
|
echo "dev_server -- start Flask development server with debugger and auto reload"
|
||||||
echo "debug -- start Flask development server with remote debugger via ptvsd"
|
echo "debug -- start Flask development server with remote debugger via debugpy"
|
||||||
echo "create_db -- create database tables"
|
echo "create_db -- create database tables"
|
||||||
echo "manage -- CLI to manage redash"
|
echo "manage -- CLI to manage redash"
|
||||||
echo "tests -- run tests"
|
echo "tests -- run tests"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ body {
|
|||||||
display: table;
|
display: table;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: calc(100vh - 116px);
|
height: calc(100% - 116px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
|
|||||||
BIN
client/app/assets/images/db-logos/duckdb.png
Normal file
BIN
client/app/assets/images/db-logos/duckdb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
client/app/assets/images/db-logos/e6data.png
Normal file
BIN
client/app/assets/images/db-logos/e6data.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
client/app/assets/images/db-logos/influxdbv2.png
Normal file
BIN
client/app/assets/images/db-logos/influxdbv2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
BIN
client/app/assets/images/db-logos/risingwave.png
Normal file
BIN
client/app/assets/images/db-logos/risingwave.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
BIN
client/app/assets/images/db-logos/yandex_disk.png
Normal file
BIN
client/app/assets/images/db-logos/yandex_disk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
@@ -20,7 +20,7 @@ html {
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -35,7 +35,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#application-root {
|
#application-root {
|
||||||
min-height: 100vh;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#application-root,
|
#application-root,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,4 +135,4 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ body.fixed-layout {
|
|||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
|
|
||||||
.application-layout-content > div {
|
.application-layout-content > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -90,7 +90,7 @@ body.fixed-layout {
|
|||||||
.embed__vis {
|
.embed__vis {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: calc(~'100vh - 25px');
|
height: calc(~'100% - 25px');
|
||||||
|
|
||||||
> .embed-heading {
|
> .embed-heading {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -223,6 +223,7 @@ body.fixed-layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor__left__schema {
|
.editor__left__schema {
|
||||||
|
min-height: 120px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ body #application-root {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
|
|
||||||
.application-layout-side-menu {
|
.application-layout-side-menu {
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@media @mobileBreakpoint {
|
@media @mobileBreakpoint {
|
||||||
@@ -47,6 +47,10 @@ body #application-root {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body > section {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body.fixed-layout #application-root {
|
body.fixed-layout #application-root {
|
||||||
.application-layout-content {
|
.application-layout-content {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function BeaconConsent() {
|
|||||||
setHide(true);
|
setHide(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmConsent = confirm => {
|
const confirmConsent = (confirm) => {
|
||||||
let message = "🙏 Thank you.";
|
let message = "🙏 Thank you.";
|
||||||
|
|
||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
@@ -47,7 +47,8 @@ function BeaconConsent() {
|
|||||||
<HelpTrigger type="USAGE_DATA_SHARING" />
|
<HelpTrigger type="USAGE_DATA_SHARING" />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
bordered={false}>
|
bordered={false}
|
||||||
|
>
|
||||||
<Text>Help Redash improve by automatically sending anonymous usage data:</Text>
|
<Text>Help Redash improve by automatically sending anonymous usage data:</Text>
|
||||||
<div className="m-t-5">
|
<div className="m-t-5">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -66,8 +67,7 @@ function BeaconConsent() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="m-t-15">
|
<div className="m-t-15">
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
You can change this setting anytime from the{" "}
|
You can change this setting anytime from the <Link href="settings/general">Settings</Link> page.
|
||||||
<Link href="settings/organization">Organization Settings</Link> page.
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
|||||||
import QuerySelector from "@/components/QuerySelector";
|
import QuerySelector from "@/components/QuerySelector";
|
||||||
import { Query } from "@/services/query";
|
import { Query } from "@/services/query";
|
||||||
import { useUniqueId } from "@/lib/hooks/useUniqueId";
|
import { useUniqueId } from "@/lib/hooks/useUniqueId";
|
||||||
|
import "./EditParameterSettingsDialog.less";
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
|
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
|
||||||
@@ -26,7 +27,7 @@ function isTypeDateRange(type) {
|
|||||||
|
|
||||||
function joinExampleList(multiValuesOptions) {
|
function joinExampleList(multiValuesOptions) {
|
||||||
const { prefix, suffix } = 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 }) {
|
function NameInput({ name, type, onChange, existingNames, setValidation }) {
|
||||||
@@ -54,7 +55,7 @@ function NameInput({ name, type, onChange, existingNames, setValidation }) {
|
|||||||
|
|
||||||
return (
|
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 />
|
<Input onChange={(e) => onChange(e.target.value)} autoFocus />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -71,6 +72,8 @@ function EditParameterSettingsDialog(props) {
|
|||||||
const [param, setParam] = useState(clone(props.parameter));
|
const [param, setParam] = useState(clone(props.parameter));
|
||||||
const [isNameValid, setIsNameValid] = useState(true);
|
const [isNameValid, setIsNameValid] = useState(true);
|
||||||
const [initialQuery, setInitialQuery] = useState();
|
const [initialQuery, setInitialQuery] = useState();
|
||||||
|
const [userInput, setUserInput] = useState(param.regex || "");
|
||||||
|
const [isValidRegex, setIsValidRegex] = useState(true);
|
||||||
|
|
||||||
const isNew = !props.parameter.name;
|
const isNew = !props.parameter.name;
|
||||||
|
|
||||||
@@ -114,6 +117,17 @@ function EditParameterSettingsDialog(props) {
|
|||||||
|
|
||||||
const paramFormId = useUniqueId("paramForm");
|
const paramFormId = useUniqueId("paramForm");
|
||||||
|
|
||||||
|
const handleRegexChange = (e) => {
|
||||||
|
setUserInput(e.target.value);
|
||||||
|
try {
|
||||||
|
new RegExp(e.target.value);
|
||||||
|
setParam({ ...param, regex: e.target.value });
|
||||||
|
setIsValidRegex(true);
|
||||||
|
} catch (error) {
|
||||||
|
setIsValidRegex(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
{...props.dialog.props}
|
{...props.dialog.props}
|
||||||
@@ -129,15 +143,17 @@ function EditParameterSettingsDialog(props) {
|
|||||||
disabled={!isFulfilled()}
|
disabled={!isFulfilled()}
|
||||||
type="primary"
|
type="primary"
|
||||||
form={paramFormId}
|
form={paramFormId}
|
||||||
data-test="SaveParameterSettings">
|
data-test="SaveParameterSettings"
|
||||||
|
>
|
||||||
{isNew ? "Add Parameter" : "OK"}
|
{isNew ? "Add Parameter" : "OK"}
|
||||||
</Button>,
|
</Button>,
|
||||||
]}>
|
]}
|
||||||
|
>
|
||||||
<Form layout="horizontal" onFinish={onConfirm} id={paramFormId}>
|
<Form layout="horizontal" onFinish={onConfirm} id={paramFormId}>
|
||||||
{isNew && (
|
{isNew && (
|
||||||
<NameInput
|
<NameInput
|
||||||
name={param.name}
|
name={param.name}
|
||||||
onChange={name => setParam({ ...param, name })}
|
onChange={(name) => setParam({ ...param, name })}
|
||||||
setValidation={setIsNameValid}
|
setValidation={setIsNameValid}
|
||||||
existingNames={props.existingParams}
|
existingNames={props.existingParams}
|
||||||
type={param.type}
|
type={param.type}
|
||||||
@@ -146,15 +162,16 @@ function EditParameterSettingsDialog(props) {
|
|||||||
<Form.Item required label="Title" {...formItemProps}>
|
<Form.Item required label="Title" {...formItemProps}>
|
||||||
<Input
|
<Input
|
||||||
value={isNull(param.title) ? getDefaultTitle(param.name) : param.title}
|
value={isNull(param.title) ? getDefaultTitle(param.name) : param.title}
|
||||||
onChange={e => setParam({ ...param, title: e.target.value })}
|
onChange={(e) => setParam({ ...param, title: e.target.value })}
|
||||||
data-test="ParameterTitleInput"
|
data-test="ParameterTitleInput"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Type" {...formItemProps}>
|
<Form.Item label="Type" {...formItemProps}>
|
||||||
<Select value={param.type} onChange={type => setParam({ ...param, type })} data-test="ParameterTypeSelect">
|
<Select value={param.type} onChange={(type) => setParam({ ...param, type })} data-test="ParameterTypeSelect">
|
||||||
<Option value="text" data-test="TextParameterTypeOption">
|
<Option value="text" data-test="TextParameterTypeOption">
|
||||||
Text
|
Text
|
||||||
</Option>
|
</Option>
|
||||||
|
<Option value="text-pattern">Text Pattern</Option>
|
||||||
<Option value="number" data-test="NumberParameterTypeOption">
|
<Option value="number" data-test="NumberParameterTypeOption">
|
||||||
Number
|
Number
|
||||||
</Option>
|
</Option>
|
||||||
@@ -180,12 +197,26 @@ function EditParameterSettingsDialog(props) {
|
|||||||
<Option value="datetime-range-with-seconds">Date and Time Range (with seconds)</Option>
|
<Option value="datetime-range-with-seconds">Date and Time Range (with seconds)</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{param.type === "text-pattern" && (
|
||||||
|
<Form.Item
|
||||||
|
label="Regex"
|
||||||
|
help={!isValidRegex ? "Invalid Regex Pattern" : "Valid Regex Pattern"}
|
||||||
|
{...formItemProps}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={userInput}
|
||||||
|
onChange={handleRegexChange}
|
||||||
|
className={!isValidRegex ? "input-error" : ""}
|
||||||
|
data-test="RegexPatternInput"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
{param.type === "enum" && (
|
{param.type === "enum" && (
|
||||||
<Form.Item label="Values" help="Dropdown list values (newline delimited)" {...formItemProps}>
|
<Form.Item label="Values" help="Dropdown list values (newline delimited)" {...formItemProps}>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={param.enumOptions}
|
value={param.enumOptions}
|
||||||
onChange={e => setParam({ ...param, enumOptions: e.target.value })}
|
onChange={(e) => setParam({ ...param, enumOptions: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
@@ -193,7 +224,7 @@ function EditParameterSettingsDialog(props) {
|
|||||||
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
|
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
|
||||||
<QuerySelector
|
<QuerySelector
|
||||||
selectedQuery={initialQuery}
|
selectedQuery={initialQuery}
|
||||||
onChange={q => setParam({ ...param, queryId: q && q.id })}
|
onChange={(q) => setParam({ ...param, queryId: q && q.id })}
|
||||||
type="select"
|
type="select"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -202,7 +233,7 @@ function EditParameterSettingsDialog(props) {
|
|||||||
<Form.Item className="m-b-0" label=" " colon={false} {...formItemProps}>
|
<Form.Item className="m-b-0" label=" " colon={false} {...formItemProps}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
defaultChecked={!!param.multiValuesOptions}
|
defaultChecked={!!param.multiValuesOptions}
|
||||||
onChange={e =>
|
onChange={(e) =>
|
||||||
setParam({
|
setParam({
|
||||||
...param,
|
...param,
|
||||||
multiValuesOptions: e.target.checked
|
multiValuesOptions: e.target.checked
|
||||||
@@ -214,7 +245,8 @@ function EditParameterSettingsDialog(props) {
|
|||||||
: null,
|
: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
data-test="AllowMultipleValuesCheckbox">
|
data-test="AllowMultipleValuesCheckbox"
|
||||||
|
>
|
||||||
Allow multiple values
|
Allow multiple values
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -227,10 +259,11 @@ function EditParameterSettingsDialog(props) {
|
|||||||
Placed in query as: <code>{joinExampleList(param.multiValuesOptions)}</code>
|
Placed in query as: <code>{joinExampleList(param.multiValuesOptions)}</code>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
{...formItemProps}>
|
{...formItemProps}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
value={param.multiValuesOptions.prefix}
|
value={param.multiValuesOptions.prefix}
|
||||||
onChange={quoteOption =>
|
onChange={(quoteOption) =>
|
||||||
setParam({
|
setParam({
|
||||||
...param,
|
...param,
|
||||||
multiValuesOptions: {
|
multiValuesOptions: {
|
||||||
@@ -240,7 +273,8 @@ function EditParameterSettingsDialog(props) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
data-test="QuotationSelect">
|
data-test="QuotationSelect"
|
||||||
|
>
|
||||||
<Option value="">None (default)</Option>
|
<Option value="">None (default)</Option>
|
||||||
<Option value="'">Single Quotation Mark</Option>
|
<Option value="'">Single Quotation Mark</Option>
|
||||||
<Option value={'"'} data-test="DoubleQuotationMarkOption">
|
<Option value={'"'} data-test="DoubleQuotationMarkOption">
|
||||||
|
|||||||
3
client/app/components/EditParameterSettingsDialog.less
Normal file
3
client/app/components/EditParameterSettingsDialog.less
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.input-error {
|
||||||
|
border-color: red !important;
|
||||||
|
}
|
||||||
@@ -101,7 +101,7 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
|
|||||||
clearTimeout(this.iframeLoadingTimeout);
|
clearTimeout(this.iframeLoadingTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadIframe = url => {
|
loadIframe = (url) => {
|
||||||
clearTimeout(this.iframeLoadingTimeout);
|
clearTimeout(this.iframeLoadingTimeout);
|
||||||
this.setState({ loading: true, error: false });
|
this.setState({ loading: true, error: false });
|
||||||
|
|
||||||
@@ -116,8 +116,8 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
|
|||||||
clearTimeout(this.iframeLoadingTimeout);
|
clearTimeout(this.iframeLoadingTimeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
onPostMessageReceived = event => {
|
onPostMessageReceived = (event) => {
|
||||||
if (!some(allowedDomains, domain => startsWith(event.origin, domain))) {
|
if (!some(allowedDomains, (domain) => startsWith(event.origin, domain))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
|
|||||||
return helpTriggerType ? helpTriggerType[0] : this.props.href;
|
return helpTriggerType ? helpTriggerType[0] : this.props.href;
|
||||||
};
|
};
|
||||||
|
|
||||||
openDrawer = e => {
|
openDrawer = (e) => {
|
||||||
// keep "open in new tab" behavior
|
// keep "open in new tab" behavior
|
||||||
if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -144,7 +144,7 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
closeDrawer = event => {
|
closeDrawer = (event) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
|
|||||||
const tooltip = get(types, `${this.props.type}[1]`, this.props.title);
|
const tooltip = get(types, `${this.props.type}[1]`, this.props.title);
|
||||||
const className = cx("help-trigger", this.props.className);
|
const className = cx("help-trigger", this.props.className);
|
||||||
const url = this.state.currentUrl;
|
const url = this.state.currentUrl;
|
||||||
const isAllowedDomain = some(allowedDomains, domain => startsWith(url || targetUrl, domain));
|
const isAllowedDomain = some(allowedDomains, (domain) => startsWith(url || targetUrl, domain));
|
||||||
const shouldRenderAsLink = this.props.renderAsLink || !isAllowedDomain;
|
const shouldRenderAsLink = this.props.renderAsLink || !isAllowedDomain;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -180,13 +180,15 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : null
|
) : null
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
href={url || this.getUrl()}
|
href={url || this.getUrl()}
|
||||||
className={className}
|
className={className}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={shouldRenderAsLink ? () => {} : this.openDrawer}>
|
onClick={shouldRenderAsLink ? () => {} : this.openDrawer}
|
||||||
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -197,7 +199,8 @@ export function helpTriggerWithTypes(types, allowedDomains = [], drawerClassName
|
|||||||
visible={this.state.visible}
|
visible={this.state.visible}
|
||||||
className={cx("help-drawer", drawerClassName)}
|
className={cx("help-drawer", drawerClassName)}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
width={400}>
|
width={400}
|
||||||
|
>
|
||||||
<div className="drawer-wrapper">
|
<div className="drawer-wrapper">
|
||||||
<div className="drawer-menu">
|
<div className="drawer-menu">
|
||||||
{url && (
|
{url && (
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ export const MappingType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function parameterMappingsToEditableMappings(mappings, parameters, existingParameterNames = []) {
|
export function parameterMappingsToEditableMappings(mappings, parameters, existingParameterNames = []) {
|
||||||
return map(mappings, mapping => {
|
return map(mappings, (mapping) => {
|
||||||
const result = extend({}, mapping);
|
const result = extend({}, mapping);
|
||||||
const alreadyExists = includes(existingParameterNames, mapping.mapTo);
|
const alreadyExists = includes(existingParameterNames, mapping.mapTo);
|
||||||
result.param = find(parameters, p => p.name === mapping.name);
|
result.param = find(parameters, (p) => p.name === mapping.name);
|
||||||
switch (mapping.type) {
|
switch (mapping.type) {
|
||||||
case ParameterMappingType.DashboardLevel:
|
case ParameterMappingType.DashboardLevel:
|
||||||
result.type = alreadyExists ? MappingType.DashboardMapToExisting : MappingType.DashboardAddNew;
|
result.type = alreadyExists ? MappingType.DashboardMapToExisting : MappingType.DashboardAddNew;
|
||||||
@@ -62,7 +62,7 @@ export function editableMappingsToParameterMappings(mappings) {
|
|||||||
map(
|
map(
|
||||||
// convert to map
|
// convert to map
|
||||||
mappings,
|
mappings,
|
||||||
mapping => {
|
(mapping) => {
|
||||||
const result = extend({}, mapping);
|
const result = extend({}, mapping);
|
||||||
switch (mapping.type) {
|
switch (mapping.type) {
|
||||||
case MappingType.DashboardAddNew:
|
case MappingType.DashboardAddNew:
|
||||||
@@ -95,11 +95,11 @@ export function editableMappingsToParameterMappings(mappings) {
|
|||||||
export function synchronizeWidgetTitles(sourceMappings, widgets) {
|
export function synchronizeWidgetTitles(sourceMappings, widgets) {
|
||||||
const affectedWidgets = [];
|
const affectedWidgets = [];
|
||||||
|
|
||||||
each(sourceMappings, sourceMapping => {
|
each(sourceMappings, (sourceMapping) => {
|
||||||
if (sourceMapping.type === ParameterMappingType.DashboardLevel) {
|
if (sourceMapping.type === ParameterMappingType.DashboardLevel) {
|
||||||
each(widgets, widget => {
|
each(widgets, (widget) => {
|
||||||
const widgetMappings = widget.options.parameterMappings;
|
const widgetMappings = widget.options.parameterMappings;
|
||||||
each(widgetMappings, widgetMapping => {
|
each(widgetMappings, (widgetMapping) => {
|
||||||
// check if mapped to the same dashboard-level parameter
|
// check if mapped to the same dashboard-level parameter
|
||||||
if (
|
if (
|
||||||
widgetMapping.type === ParameterMappingType.DashboardLevel &&
|
widgetMapping.type === ParameterMappingType.DashboardLevel &&
|
||||||
@@ -140,7 +140,7 @@ export class ParameterMappingInput extends React.Component {
|
|||||||
className: "form-item",
|
className: "form-item",
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSourceType = type => {
|
updateSourceType = (type) => {
|
||||||
let {
|
let {
|
||||||
mapping: { mapTo },
|
mapping: { mapTo },
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -155,7 +155,7 @@ export class ParameterMappingInput extends React.Component {
|
|||||||
this.updateParamMapping({ type, mapTo });
|
this.updateParamMapping({ type, mapTo });
|
||||||
};
|
};
|
||||||
|
|
||||||
updateParamMapping = update => {
|
updateParamMapping = (update) => {
|
||||||
const { onChange, mapping } = this.props;
|
const { onChange, mapping } = this.props;
|
||||||
const newMapping = extend({}, mapping, update);
|
const newMapping = extend({}, mapping, update);
|
||||||
if (newMapping.value !== mapping.value) {
|
if (newMapping.value !== mapping.value) {
|
||||||
@@ -175,7 +175,7 @@ export class ParameterMappingInput extends React.Component {
|
|||||||
renderMappingTypeSelector() {
|
renderMappingTypeSelector() {
|
||||||
const noExisting = isEmpty(this.props.existingParamNames);
|
const noExisting = isEmpty(this.props.existingParamNames);
|
||||||
return (
|
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">
|
<Radio className="radio" value={MappingType.DashboardAddNew} data-test="NewDashboardParameterOption">
|
||||||
New dashboard parameter
|
New dashboard parameter
|
||||||
</Radio>
|
</Radio>
|
||||||
@@ -205,16 +205,16 @@ export class ParameterMappingInput extends React.Component {
|
|||||||
<Input
|
<Input
|
||||||
value={mapTo}
|
value={mapTo}
|
||||||
aria-label="Parameter name (key)"
|
aria-label="Parameter name (key)"
|
||||||
onChange={e => this.updateParamMapping({ mapTo: e.target.value })}
|
onChange={(e) => this.updateParamMapping({ mapTo: e.target.value })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDashboardMapToExisting() {
|
renderDashboardMapToExisting() {
|
||||||
const { mapping, existingParamNames } = this.props;
|
const { mapping, existingParamNames } = this.props;
|
||||||
const options = map(existingParamNames, paramName => ({ label: paramName, value: paramName }));
|
const options = map(existingParamNames, (paramName) => ({ label: paramName, value: paramName }));
|
||||||
|
|
||||||
return <Select value={mapping.mapTo} onChange={mapTo => this.updateParamMapping({ mapTo })} options={options} />;
|
return <Select value={mapping.mapTo} onChange={(mapTo) => this.updateParamMapping({ mapTo })} options={options} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStaticValue() {
|
renderStaticValue() {
|
||||||
@@ -226,7 +226,8 @@ export class ParameterMappingInput extends React.Component {
|
|||||||
enumOptions={mapping.param.enumOptions}
|
enumOptions={mapping.param.enumOptions}
|
||||||
queryId={mapping.param.queryId}
|
queryId={mapping.param.queryId}
|
||||||
parameter={mapping.param}
|
parameter={mapping.param}
|
||||||
onSelect={value => this.updateParamMapping({ value })}
|
onSelect={(value) => this.updateParamMapping({ value })}
|
||||||
|
regex={mapping.param.regex}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -284,12 +285,12 @@ class MappingEditor extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChange = visible => {
|
onVisibleChange = (visible) => {
|
||||||
if (visible) this.show();
|
if (visible) this.show();
|
||||||
else this.hide();
|
else this.hide();
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange = mapping => {
|
onChange = (mapping) => {
|
||||||
let inputError = null;
|
let inputError = null;
|
||||||
|
|
||||||
if (mapping.type === MappingType.DashboardAddNew) {
|
if (mapping.type === MappingType.DashboardAddNew) {
|
||||||
@@ -351,7 +352,8 @@ class MappingEditor extends React.Component {
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
content={this.renderContent()}
|
content={this.renderContent()}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onVisibleChange={this.onVisibleChange}>
|
onVisibleChange={this.onVisibleChange}
|
||||||
|
>
|
||||||
<Button size="small" type="dashed" data-test={`EditParamMappingButton-${mapping.param.name}`}>
|
<Button size="small" type="dashed" data-test={`EditParamMappingButton-${mapping.param.name}`}>
|
||||||
<EditOutlinedIcon />
|
<EditOutlinedIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -376,14 +378,14 @@ class TitleEditor extends React.Component {
|
|||||||
title: "", // will be set on editing
|
title: "", // will be set on editing
|
||||||
};
|
};
|
||||||
|
|
||||||
onPopupVisibleChange = showPopup => {
|
onPopupVisibleChange = (showPopup) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showPopup,
|
showPopup,
|
||||||
title: showPopup ? this.getMappingTitle() : "",
|
title: showPopup ? this.getMappingTitle() : "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onEditingTitleChange = event => {
|
onEditingTitleChange = (event) => {
|
||||||
this.setState({ title: event.target.value });
|
this.setState({ title: event.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -460,7 +462,8 @@ class TitleEditor extends React.Component {
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
content={this.renderPopover()}
|
content={this.renderPopover()}
|
||||||
visible={this.state.showPopup}
|
visible={this.state.showPopup}
|
||||||
onVisibleChange={this.onPopupVisibleChange}>
|
onVisibleChange={this.onPopupVisibleChange}
|
||||||
|
>
|
||||||
<Button size="small" type="dashed">
|
<Button size="small" type="dashed">
|
||||||
<EditOutlinedIcon />
|
<EditOutlinedIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -508,7 +511,7 @@ export class ParameterMappingListInput extends React.Component {
|
|||||||
|
|
||||||
// just to be safe, array or object
|
// just to be safe, array or object
|
||||||
if (typeof value === "object") {
|
if (typeof value === "object") {
|
||||||
return map(value, v => this.getStringValue(v)).join(", ");
|
return map(value, (v) => this.getStringValue(v)).join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// rest
|
// rest
|
||||||
@@ -574,7 +577,7 @@ export class ParameterMappingListInput extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { existingParams } = this.props; // eslint-disable-line react/prop-types
|
const { existingParams } = this.props; // eslint-disable-line react/prop-types
|
||||||
const dataSource = this.props.mappings.map(mapping => ({ mapping }));
|
const dataSource = this.props.mappings.map((mapping) => ({ mapping }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="parameters-mapping-list">
|
<div className="parameters-mapping-list">
|
||||||
@@ -583,11 +586,11 @@ export class ParameterMappingListInput extends React.Component {
|
|||||||
title="Title"
|
title="Title"
|
||||||
dataIndex="mapping"
|
dataIndex="mapping"
|
||||||
key="title"
|
key="title"
|
||||||
render={mapping => (
|
render={(mapping) => (
|
||||||
<TitleEditor
|
<TitleEditor
|
||||||
existingParams={existingParams}
|
existingParams={existingParams}
|
||||||
mapping={mapping}
|
mapping={mapping}
|
||||||
onChange={newMapping => this.updateParamMapping(mapping, newMapping)}
|
onChange={(newMapping) => this.updateParamMapping(mapping, newMapping)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -596,19 +599,19 @@ export class ParameterMappingListInput extends React.Component {
|
|||||||
dataIndex="mapping"
|
dataIndex="mapping"
|
||||||
key="keyword"
|
key="keyword"
|
||||||
className="keyword"
|
className="keyword"
|
||||||
render={mapping => <code>{`{{ ${mapping.name} }}`}</code>}
|
render={(mapping) => <code>{`{{ ${mapping.name} }}`}</code>}
|
||||||
/>
|
/>
|
||||||
<Table.Column
|
<Table.Column
|
||||||
title="Default Value"
|
title="Default Value"
|
||||||
dataIndex="mapping"
|
dataIndex="mapping"
|
||||||
key="value"
|
key="value"
|
||||||
render={mapping => this.constructor.getDefaultValue(mapping, this.props.existingParams)}
|
render={(mapping) => this.constructor.getDefaultValue(mapping, this.props.existingParams)}
|
||||||
/>
|
/>
|
||||||
<Table.Column
|
<Table.Column
|
||||||
title="Value Source"
|
title="Value Source"
|
||||||
dataIndex="mapping"
|
dataIndex="mapping"
|
||||||
key="source"
|
key="source"
|
||||||
render={mapping => {
|
render={(mapping) => {
|
||||||
const existingParamsNames = existingParams
|
const existingParamsNames = existingParams
|
||||||
.filter(({ type }) => type === mapping.param.type) // exclude mismatching param types
|
.filter(({ type }) => type === mapping.param.type) // exclude mismatching param types
|
||||||
.map(({ name }) => name); // keep names only
|
.map(({ name }) => name); // keep names only
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import DateRangeParameter from "@/components/dynamic-parameters/DateRangeParamet
|
|||||||
import QueryBasedParameterInput from "./QueryBasedParameterInput";
|
import QueryBasedParameterInput from "./QueryBasedParameterInput";
|
||||||
|
|
||||||
import "./ParameterValueInput.less";
|
import "./ParameterValueInput.less";
|
||||||
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
const multipleValuesProps = {
|
const multipleValuesProps = {
|
||||||
maxTagCount: 3,
|
maxTagCount: 3,
|
||||||
maxTagTextLength: 10,
|
maxTagTextLength: 10,
|
||||||
maxTagPlaceholder: num => `+${num.length} more`,
|
maxTagPlaceholder: (num) => `+${num.length} more`,
|
||||||
};
|
};
|
||||||
|
|
||||||
class ParameterValueInput extends React.Component {
|
class ParameterValueInput extends React.Component {
|
||||||
@@ -25,6 +26,7 @@ class ParameterValueInput extends React.Component {
|
|||||||
parameter: PropTypes.any, // eslint-disable-line react/forbid-prop-types
|
parameter: PropTypes.any, // eslint-disable-line react/forbid-prop-types
|
||||||
onSelect: PropTypes.func,
|
onSelect: PropTypes.func,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
regex: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -35,6 +37,7 @@ class ParameterValueInput extends React.Component {
|
|||||||
parameter: null,
|
parameter: null,
|
||||||
onSelect: () => {},
|
onSelect: () => {},
|
||||||
className: "",
|
className: "",
|
||||||
|
regex: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -45,7 +48,7 @@ class ParameterValueInput extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate = prevProps => {
|
componentDidUpdate = (prevProps) => {
|
||||||
const { value, parameter } = this.props;
|
const { value, parameter } = this.props;
|
||||||
// if value prop updated, reset dirty state
|
// if value prop updated, reset dirty state
|
||||||
if (prevProps.value !== value || prevProps.parameter !== parameter) {
|
if (prevProps.value !== value || prevProps.parameter !== parameter) {
|
||||||
@@ -56,7 +59,7 @@ class ParameterValueInput extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelect = value => {
|
onSelect = (value) => {
|
||||||
const isDirty = !isEqual(value, this.props.value);
|
const isDirty = !isEqual(value, this.props.value);
|
||||||
this.setState({ value, isDirty });
|
this.setState({ value, isDirty });
|
||||||
this.props.onSelect(value, isDirty);
|
this.props.onSelect(value, isDirty);
|
||||||
@@ -93,9 +96,9 @@ class ParameterValueInput extends React.Component {
|
|||||||
renderEnumInput() {
|
renderEnumInput() {
|
||||||
const { enumOptions, parameter } = this.props;
|
const { enumOptions, parameter } = this.props;
|
||||||
const { value } = this.state;
|
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
|
// Antd Select doesn't handle null in multiple mode
|
||||||
const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val);
|
const normalize = (val) => (parameter.multiValuesOptions && val === null ? [] : val);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectWithVirtualScroll
|
<SelectWithVirtualScroll
|
||||||
@@ -103,7 +106,7 @@ class ParameterValueInput extends React.Component {
|
|||||||
mode={parameter.multiValuesOptions ? "multiple" : "default"}
|
mode={parameter.multiValuesOptions ? "multiple" : "default"}
|
||||||
value={normalize(value)}
|
value={normalize(value)}
|
||||||
onChange={this.onSelect}
|
onChange={this.onSelect}
|
||||||
options={map(enumOptionsArray, opt => ({ label: String(opt), value: opt }))}
|
options={map(enumOptionsArray, (opt) => ({ label: String(opt), value: opt }))}
|
||||||
showSearch
|
showSearch
|
||||||
showArrow
|
showArrow
|
||||||
notFoundContent={isEmpty(enumOptionsArray) ? "No options available" : null}
|
notFoundContent={isEmpty(enumOptionsArray) ? "No options available" : null}
|
||||||
@@ -133,18 +136,36 @@ class ParameterValueInput extends React.Component {
|
|||||||
const { className } = this.props;
|
const { className } = this.props;
|
||||||
const { value } = this.state;
|
const { value } = this.state;
|
||||||
|
|
||||||
const normalize = val => (isNaN(val) ? undefined : val);
|
const normalize = (val) => (isNaN(val) ? undefined : val);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputNumber
|
<InputNumber
|
||||||
className={className}
|
className={className}
|
||||||
value={normalize(value)}
|
value={normalize(value)}
|
||||||
aria-label="Parameter number value"
|
aria-label="Parameter number value"
|
||||||
onChange={val => this.onSelect(normalize(val))}
|
onChange={(val) => this.onSelect(normalize(val))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderTextPatternInput() {
|
||||||
|
const { className } = this.props;
|
||||||
|
const { value } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Tooltip title={`Regex to match: ${this.props.regex}`} placement="right">
|
||||||
|
<Input
|
||||||
|
className={className}
|
||||||
|
value={value}
|
||||||
|
aria-label="Parameter text pattern value"
|
||||||
|
onChange={(e) => this.onSelect(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderTextInput() {
|
renderTextInput() {
|
||||||
const { className } = this.props;
|
const { className } = this.props;
|
||||||
const { value } = this.state;
|
const { value } = this.state;
|
||||||
@@ -155,7 +176,7 @@ class ParameterValueInput extends React.Component {
|
|||||||
value={value}
|
value={value}
|
||||||
aria-label="Parameter text value"
|
aria-label="Parameter text value"
|
||||||
data-test="TextParamInput"
|
data-test="TextParamInput"
|
||||||
onChange={e => this.onSelect(e.target.value)}
|
onChange={(e) => this.onSelect(e.target.value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -177,6 +198,8 @@ class ParameterValueInput extends React.Component {
|
|||||||
return this.renderQueryBasedInput();
|
return this.renderQueryBasedInput();
|
||||||
case "number":
|
case "number":
|
||||||
return this.renderNumberInput();
|
return this.renderNumberInput();
|
||||||
|
case "text-pattern":
|
||||||
|
return this.renderTextPatternInput();
|
||||||
default:
|
default:
|
||||||
return this.renderTextInput();
|
return this.renderTextInput();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import "./Parameters.less";
|
|||||||
|
|
||||||
function updateUrl(parameters) {
|
function updateUrl(parameters) {
|
||||||
const params = extend({}, location.search);
|
const params = extend({}, location.search);
|
||||||
parameters.forEach(param => {
|
parameters.forEach((param) => {
|
||||||
extend(params, param.toUrlParams());
|
extend(params, param.toUrlParams());
|
||||||
});
|
});
|
||||||
location.setSearch(params, true);
|
location.setSearch(params, true);
|
||||||
@@ -43,7 +43,7 @@ export default class Parameters extends React.Component {
|
|||||||
appendSortableToParent: true,
|
appendSortableToParent: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
toCamelCase = str => {
|
toCamelCase = (str) => {
|
||||||
if (isEmpty(str)) {
|
if (isEmpty(str)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -59,10 +59,10 @@ export default class Parameters extends React.Component {
|
|||||||
}
|
}
|
||||||
const hideRegex = /hide_filter=([^&]+)/g;
|
const hideRegex = /hide_filter=([^&]+)/g;
|
||||||
const matches = window.location.search.matchAll(hideRegex);
|
const matches = window.location.search.matchAll(hideRegex);
|
||||||
this.hideValues = Array.from(matches, match => match[1]);
|
this.hideValues = Array.from(matches, (match) => match[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate = prevProps => {
|
componentDidUpdate = (prevProps) => {
|
||||||
const { parameters, disableUrlUpdate } = this.props;
|
const { parameters, disableUrlUpdate } = this.props;
|
||||||
const parametersChanged = prevProps.parameters !== parameters;
|
const parametersChanged = prevProps.parameters !== parameters;
|
||||||
const disableUrlUpdateChanged = prevProps.disableUrlUpdate !== disableUrlUpdate;
|
const disableUrlUpdateChanged = prevProps.disableUrlUpdate !== disableUrlUpdate;
|
||||||
@@ -74,7 +74,7 @@ export default class Parameters extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleKeyDown = e => {
|
handleKeyDown = (e) => {
|
||||||
// Cmd/Ctrl/Alt + Enter
|
// Cmd/Ctrl/Alt + Enter
|
||||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) {
|
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -109,8 +109,8 @@ export default class Parameters extends React.Component {
|
|||||||
applyChanges = () => {
|
applyChanges = () => {
|
||||||
const { onValuesChange, disableUrlUpdate } = this.props;
|
const { onValuesChange, disableUrlUpdate } = this.props;
|
||||||
this.setState(({ parameters }) => {
|
this.setState(({ parameters }) => {
|
||||||
const parametersWithPendingValues = parameters.filter(p => p.hasPendingValue);
|
const parametersWithPendingValues = parameters.filter((p) => p.hasPendingValue);
|
||||||
forEach(parameters, p => p.applyPendingValue());
|
forEach(parameters, (p) => p.applyPendingValue());
|
||||||
if (!disableUrlUpdate) {
|
if (!disableUrlUpdate) {
|
||||||
updateUrl(parameters);
|
updateUrl(parameters);
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ export default class Parameters extends React.Component {
|
|||||||
|
|
||||||
showParameterSettings = (parameter, index) => {
|
showParameterSettings = (parameter, index) => {
|
||||||
const { onParametersEdit } = this.props;
|
const { onParametersEdit } = this.props;
|
||||||
EditParameterSettingsDialog.showModal({ parameter }).onClose(updated => {
|
EditParameterSettingsDialog.showModal({ parameter }).onClose((updated) => {
|
||||||
this.setState(({ parameters }) => {
|
this.setState(({ parameters }) => {
|
||||||
const updatedParameter = extend(parameter, updated);
|
const updatedParameter = extend(parameter, updated);
|
||||||
parameters[index] = createParameter(updatedParameter, updatedParameter.parentQueryId);
|
parameters[index] = createParameter(updatedParameter, updatedParameter.parentQueryId);
|
||||||
@@ -132,7 +132,7 @@ export default class Parameters extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderParameter(param, index) {
|
renderParameter(param, index) {
|
||||||
if (this.hideValues.some(value => this.toCamelCase(value) === this.toCamelCase(param.name))) {
|
if (this.hideValues.some((value) => this.toCamelCase(value) === this.toCamelCase(param.name))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { editable } = this.props;
|
const { editable } = this.props;
|
||||||
@@ -149,7 +149,8 @@ export default class Parameters extends React.Component {
|
|||||||
aria-label="Edit"
|
aria-label="Edit"
|
||||||
onClick={() => this.showParameterSettings(param, index)}
|
onClick={() => this.showParameterSettings(param, index)}
|
||||||
data-test={`ParameterSettings-${param.name}`}
|
data-test={`ParameterSettings-${param.name}`}
|
||||||
type="button">
|
type="button"
|
||||||
|
>
|
||||||
<i className="fa fa-cog" aria-hidden="true" />
|
<i className="fa fa-cog" aria-hidden="true" />
|
||||||
</PlainButton>
|
</PlainButton>
|
||||||
)}
|
)}
|
||||||
@@ -162,6 +163,7 @@ export default class Parameters extends React.Component {
|
|||||||
enumOptions={param.enumOptions}
|
enumOptions={param.enumOptions}
|
||||||
queryId={param.queryId}
|
queryId={param.queryId}
|
||||||
onSelect={(value, isDirty) => this.setPendingValue(param, value, isDirty)}
|
onSelect={(value, isDirty) => this.setPendingValue(param, value, isDirty)}
|
||||||
|
regex={param.regex}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -178,20 +180,22 @@ export default class Parameters extends React.Component {
|
|||||||
useDragHandle
|
useDragHandle
|
||||||
lockToContainerEdges
|
lockToContainerEdges
|
||||||
helperClass="parameter-dragged"
|
helperClass="parameter-dragged"
|
||||||
helperContainer={containerEl => (appendSortableToParent ? containerEl : document.body)}
|
helperContainer={(containerEl) => (appendSortableToParent ? containerEl : document.body)}
|
||||||
updateBeforeSortStart={this.onBeforeSortStart}
|
updateBeforeSortStart={this.onBeforeSortStart}
|
||||||
onSortEnd={this.moveParameter}
|
onSortEnd={this.moveParameter}
|
||||||
containerProps={{
|
containerProps={{
|
||||||
className: "parameter-container",
|
className: "parameter-container",
|
||||||
onKeyDown: dirtyParamCount ? this.handleKeyDown : null,
|
onKeyDown: dirtyParamCount ? this.handleKeyDown : null,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{parameters &&
|
{parameters &&
|
||||||
parameters.map((param, index) => (
|
parameters.map((param, index) => (
|
||||||
<SortableElement key={param.name} index={index}>
|
<SortableElement key={param.name} index={index}>
|
||||||
<div
|
<div
|
||||||
className="parameter-block"
|
className="parameter-block"
|
||||||
data-editable={sortable || null}
|
data-editable={sortable || null}
|
||||||
data-test={`ParameterBlock-${param.name}`}>
|
data-test={`ParameterBlock-${param.name}`}
|
||||||
|
>
|
||||||
{sortable && <DragHandle data-test={`DragHandle-${param.name}`} />}
|
{sortable && <DragHandle data-test={`DragHandle-${param.name}`} />}
|
||||||
{this.renderParameter(param, index)}
|
{this.renderParameter(param, index)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ UserPreviewCard.defaultProps = {
|
|||||||
// DataSourcePreviewCard
|
// DataSourcePreviewCard
|
||||||
|
|
||||||
export function DataSourcePreviewCard({ dataSource, withLink, children, ...props }) {
|
export function DataSourcePreviewCard({ dataSource, withLink, children, ...props }) {
|
||||||
const imageUrl = `static/images/db-logos/${dataSource.type}.png`;
|
const imageUrl = `/static/images/db-logos/${dataSource.type}.png`;
|
||||||
const title = withLink ? <Link href={"data_sources/" + dataSource.id}>{dataSource.name}</Link> : dataSource.name;
|
const title = withLink ? <Link href={"data_sources/" + dataSource.id}>{dataSource.name}</Link> : dataSource.name;
|
||||||
return (
|
return (
|
||||||
<PreviewCard {...props} imageUrl={imageUrl} title={title}>
|
<PreviewCard {...props} imageUrl={imageUrl} title={title}>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px),
|
background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px),
|
||||||
linear-gradient(to left, #b3babf, #b3babf 1px, transparent 1px, transparent);
|
linear-gradient(to left, #b3babf, #b3babf 1px, transparent 1px, transparent);
|
||||||
background-size: calc((100% + 15px) / 6) 5px;
|
background-size: calc((100% + 15px) / 12) 5px;
|
||||||
background-position: -7px 1px;
|
background-position: -7px 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,6 +123,7 @@
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
bottom: 15px;
|
bottom: 15px;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,121 +9,85 @@ const DYNAMIC_DATE_OPTIONS = [
|
|||||||
name: "This week",
|
name: "This week",
|
||||||
value: getDynamicDateRangeFromString("d_this_week"),
|
value: getDynamicDateRangeFromString("d_this_week"),
|
||||||
label: () =>
|
label: () =>
|
||||||
getDynamicDateRangeFromString("d_this_week")
|
getDynamicDateRangeFromString("d_this_week").value()[0].format("MMM D") +
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") +
|
|
||||||
" - " +
|
" - " +
|
||||||
getDynamicDateRangeFromString("d_this_week")
|
getDynamicDateRangeFromString("d_this_week").value()[1].format("MMM D"),
|
||||||
.value()[1]
|
|
||||||
.format("MMM D"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "This month",
|
name: "This month",
|
||||||
value: getDynamicDateRangeFromString("d_this_month"),
|
value: getDynamicDateRangeFromString("d_this_month"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_this_month").value()[0].format("MMMM"),
|
||||||
getDynamicDateRangeFromString("d_this_month")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMMM"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "This year",
|
name: "This year",
|
||||||
value: getDynamicDateRangeFromString("d_this_year"),
|
value: getDynamicDateRangeFromString("d_this_year"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_this_year").value()[0].format("YYYY"),
|
||||||
getDynamicDateRangeFromString("d_this_year")
|
|
||||||
.value()[0]
|
|
||||||
.format("YYYY"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last week",
|
name: "Last week",
|
||||||
value: getDynamicDateRangeFromString("d_last_week"),
|
value: getDynamicDateRangeFromString("d_last_week"),
|
||||||
label: () =>
|
label: () =>
|
||||||
getDynamicDateRangeFromString("d_last_week")
|
getDynamicDateRangeFromString("d_last_week").value()[0].format("MMM D") +
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") +
|
|
||||||
" - " +
|
" - " +
|
||||||
getDynamicDateRangeFromString("d_last_week")
|
getDynamicDateRangeFromString("d_last_week").value()[1].format("MMM D"),
|
||||||
.value()[1]
|
|
||||||
.format("MMM D"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last month",
|
name: "Last month",
|
||||||
value: getDynamicDateRangeFromString("d_last_month"),
|
value: getDynamicDateRangeFromString("d_last_month"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_month").value()[0].format("MMMM"),
|
||||||
getDynamicDateRangeFromString("d_last_month")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMMM"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last year",
|
name: "Last year",
|
||||||
value: getDynamicDateRangeFromString("d_last_year"),
|
value: getDynamicDateRangeFromString("d_last_year"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_year").value()[0].format("YYYY"),
|
||||||
getDynamicDateRangeFromString("d_last_year")
|
|
||||||
.value()[0]
|
|
||||||
.format("YYYY"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 7 days",
|
name: "Last 7 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_7_days"),
|
value: getDynamicDateRangeFromString("d_last_7_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_7_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_7_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 14 days",
|
name: "Last 14 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_14_days"),
|
value: getDynamicDateRangeFromString("d_last_14_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_14_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_14_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 30 days",
|
name: "Last 30 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_30_days"),
|
value: getDynamicDateRangeFromString("d_last_30_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_30_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_30_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 60 days",
|
name: "Last 60 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_60_days"),
|
value: getDynamicDateRangeFromString("d_last_60_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_60_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_60_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 90 days",
|
name: "Last 90 days",
|
||||||
value: getDynamicDateRangeFromString("d_last_90_days"),
|
value: getDynamicDateRangeFromString("d_last_90_days"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_last_90_days").value()[0].format("MMM D") + " - Today",
|
||||||
getDynamicDateRangeFromString("d_last_90_days")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D") + " - Today",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Last 12 months",
|
name: "Last 12 months",
|
||||||
value: getDynamicDateRangeFromString("d_last_12_months"),
|
value: getDynamicDateRangeFromString("d_last_12_months"),
|
||||||
label: null,
|
label: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Last 10 years",
|
||||||
|
value: getDynamicDateRangeFromString("d_last_10_years"),
|
||||||
|
label: null,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const DYNAMIC_DATETIME_OPTIONS = [
|
const DYNAMIC_DATETIME_OPTIONS = [
|
||||||
{
|
{
|
||||||
name: "Today",
|
name: "Today",
|
||||||
value: getDynamicDateRangeFromString("d_today"),
|
value: getDynamicDateRangeFromString("d_today"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_today").value()[0].format("MMM D"),
|
||||||
getDynamicDateRangeFromString("d_today")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Yesterday",
|
name: "Yesterday",
|
||||||
value: getDynamicDateRangeFromString("d_yesterday"),
|
value: getDynamicDateRangeFromString("d_yesterday"),
|
||||||
label: () =>
|
label: () => getDynamicDateRangeFromString("d_yesterday").value()[0].format("MMM D"),
|
||||||
getDynamicDateRangeFromString("d_yesterday")
|
|
||||||
.value()[0]
|
|
||||||
.format("MMM D"),
|
|
||||||
},
|
},
|
||||||
...DYNAMIC_DATE_OPTIONS,
|
...DYNAMIC_DATE_OPTIONS,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function EmptyState({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Show if `onboardingMode=false` or any requested step not completed
|
// Show if `onboardingMode=false` or any requested step not completed
|
||||||
const shouldShow = !onboardingMode || some(keys(isAvailable), step => isAvailable[step] && !isCompleted[step]);
|
const shouldShow = !onboardingMode || some(keys(isAvailable), (step) => isAvailable[step] && !isCompleted[step]);
|
||||||
|
|
||||||
if (!shouldShow) {
|
if (!shouldShow) {
|
||||||
return null;
|
return null;
|
||||||
@@ -181,7 +181,7 @@ function EmptyState({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const stepsItems = getStepsItems ? getStepsItems(defaultStepsItems) : defaultStepsItems;
|
const stepsItems = getStepsItems ? getStepsItems(defaultStepsItems) : defaultStepsItems;
|
||||||
const imageSource = illustrationPath ? illustrationPath : "static/images/illustrations/" + illustration + ".svg";
|
const imageSource = illustrationPath ? illustrationPath : "/static/images/illustrations/" + illustration + ".svg";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="empty-state-wrapper">
|
<div className="empty-state-wrapper">
|
||||||
@@ -196,7 +196,7 @@ function EmptyState({
|
|||||||
</div>
|
</div>
|
||||||
<div className="empty-state__steps">
|
<div className="empty-state__steps">
|
||||||
<h4>Let's get started</h4>
|
<h4>Let's get started</h4>
|
||||||
<ol>{stepsItems.map(item => item.node)}</ol>
|
<ol>{stepsItems.map((item) => item.node)}</ol>
|
||||||
{helpMessage}
|
{helpMessage}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export interface PaginationOptions {
|
|||||||
itemsPerPage?: number;
|
itemsPerPage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchOptions {
|
||||||
|
isServerSideFTS?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Controller<I, P = any> {
|
export interface Controller<I, P = any> {
|
||||||
params: P; // TODO: Find out what params is (except merging with props)
|
params: P; // TODO: Find out what params is (except merging with props)
|
||||||
|
|
||||||
@@ -18,7 +22,7 @@ export interface Controller<I, P = any> {
|
|||||||
|
|
||||||
// search
|
// search
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
updateSearch: (searchTerm: string) => void;
|
updateSearch: (searchTerm: string, searchOptions?: SearchOptions) => void;
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
selectedTags: string[];
|
selectedTags: string[];
|
||||||
@@ -28,6 +32,7 @@ export interface Controller<I, P = any> {
|
|||||||
orderByField?: string;
|
orderByField?: string;
|
||||||
orderByReverse: boolean;
|
orderByReverse: boolean;
|
||||||
toggleSorting: (orderByField: string) => void;
|
toggleSorting: (orderByField: string) => void;
|
||||||
|
setSorting: (orderByField: string, orderByReverse: boolean) => void;
|
||||||
|
|
||||||
// pagination
|
// pagination
|
||||||
page: number;
|
page: number;
|
||||||
@@ -93,7 +98,7 @@ export interface ItemsListWrappedComponentProps<I, P = any> {
|
|||||||
export function wrap<I, P = any>(
|
export function wrap<I, P = any>(
|
||||||
WrappedComponent: React.ComponentType<ItemsListWrappedComponentProps<I>>,
|
WrappedComponent: React.ComponentType<ItemsListWrappedComponentProps<I>>,
|
||||||
createItemsSource: () => ItemsSource,
|
createItemsSource: () => ItemsSource,
|
||||||
createStateStorage: () => StateStorage
|
createStateStorage: ( { ...props }) => StateStorage
|
||||||
) {
|
) {
|
||||||
class ItemsListWrapper extends React.Component<ItemsListWrapperProps, ItemsListWrapperState<I, P>> {
|
class ItemsListWrapper extends React.Component<ItemsListWrapperProps, ItemsListWrapperState<I, P>> {
|
||||||
private _itemsSource: ItemsSource;
|
private _itemsSource: ItemsSource;
|
||||||
@@ -116,7 +121,7 @@ export function wrap<I, P = any>(
|
|||||||
constructor(props: ItemsListWrapperProps) {
|
constructor(props: ItemsListWrapperProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const stateStorage = createStateStorage();
|
const stateStorage = createStateStorage({ ...props });
|
||||||
const itemsSource = createItemsSource();
|
const itemsSource = createItemsSource();
|
||||||
this._itemsSource = itemsSource;
|
this._itemsSource = itemsSource;
|
||||||
|
|
||||||
@@ -139,11 +144,33 @@ export function wrap<I, P = any>(
|
|||||||
this.props.onError!(error);
|
this.props.onError!(error);
|
||||||
|
|
||||||
const initialState = this.getState({ ...itemsSource.getState(), isLoaded: false });
|
const initialState = this.getState({ ...itemsSource.getState(), isLoaded: false });
|
||||||
const { updatePagination, toggleSorting, updateSearch, updateSelectedTags, update, handleError } = itemsSource;
|
const { updatePagination, toggleSorting, setSorting, updateSearch, updateSelectedTags, update, handleError } = itemsSource;
|
||||||
|
|
||||||
|
let isRunningUpdateSearch = false;
|
||||||
|
let pendingUpdateSearchParams: any[] | null = null;
|
||||||
|
const debouncedUpdateSearch = debounce(async (...params) => {
|
||||||
|
// Avoid running multiple updateSerch concurrently.
|
||||||
|
// If an updateSearch is already running, we save the params for the latest call.
|
||||||
|
// When the current updateSearch is finished, we call debouncedUpdateSearch again with the saved params.
|
||||||
|
if (isRunningUpdateSearch) {
|
||||||
|
pendingUpdateSearchParams = params;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRunningUpdateSearch = true;
|
||||||
|
await updateSearch(...params);
|
||||||
|
isRunningUpdateSearch = false;
|
||||||
|
if (pendingUpdateSearchParams) {
|
||||||
|
const pendingParams = pendingUpdateSearchParams;
|
||||||
|
pendingUpdateSearchParams = null;
|
||||||
|
debouncedUpdateSearch(...pendingParams);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
...initialState,
|
...initialState,
|
||||||
toggleSorting, // eslint-disable-line react/no-unused-state
|
toggleSorting, // eslint-disable-line react/no-unused-state
|
||||||
updateSearch: debounce(updateSearch, 200), // eslint-disable-line react/no-unused-state
|
setSorting, // eslint-disable-line react/no-unused-state
|
||||||
|
updateSearch: debouncedUpdateSearch, // eslint-disable-line react/no-unused-state
|
||||||
updateSelectedTags, // eslint-disable-line react/no-unused-state
|
updateSelectedTags, // eslint-disable-line react/no-unused-state
|
||||||
updatePagination, // eslint-disable-line react/no-unused-state
|
updatePagination, // eslint-disable-line react/no-unused-state
|
||||||
update, // eslint-disable-line react/no-unused-state
|
update, // eslint-disable-line react/no-unused-state
|
||||||
|
|||||||
@@ -39,14 +39,12 @@ export class ItemsSource {
|
|||||||
const customParams = {};
|
const customParams = {};
|
||||||
const context = {
|
const context = {
|
||||||
...this.getCallbackContext(),
|
...this.getCallbackContext(),
|
||||||
setCustomParams: params => {
|
setCustomParams: (params) => {
|
||||||
extend(customParams, params);
|
extend(customParams, params);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return this._beforeUpdate().then(() => {
|
return this._beforeUpdate().then(() => {
|
||||||
const fetchToken = Math.random()
|
const fetchToken = Math.random().toString(36).substr(2);
|
||||||
.toString(36)
|
|
||||||
.substr(2);
|
|
||||||
this._currentFetchToken = fetchToken;
|
this._currentFetchToken = fetchToken;
|
||||||
return this._fetcher
|
return this._fetcher
|
||||||
.fetch(changes, state, context)
|
.fetch(changes, state, context)
|
||||||
@@ -59,7 +57,7 @@ export class ItemsSource {
|
|||||||
return this._afterUpdate();
|
return this._afterUpdate();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => this.handleError(error));
|
.catch((error) => this.handleError(error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,28 +122,35 @@ export class ItemsSource {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleSorting = orderByField => {
|
toggleSorting = (orderByField) => {
|
||||||
this._sorter.toggleField(orderByField);
|
this._sorter.toggleField(orderByField);
|
||||||
this._savedOrderByField = this._sorter.field;
|
this._savedOrderByField = this._sorter.field;
|
||||||
this._changed({ sorting: true });
|
this._changed({ sorting: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSearch = searchTerm => {
|
setSorting = (orderByField, orderByReverse) => {
|
||||||
|
this._sorter.setField(orderByField);
|
||||||
|
this._sorter.setReverse(orderByReverse);
|
||||||
|
this._savedOrderByField = this._sorter.field;
|
||||||
|
this._changed({ sorting: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
updateSearch = (searchTerm, options) => {
|
||||||
// here we update state directly, but later `fetchData` will update it properly
|
// here we update state directly, but later `fetchData` will update it properly
|
||||||
this._searchTerm = searchTerm;
|
this._searchTerm = searchTerm;
|
||||||
// in search mode ignore the ordering and use the ranking order
|
// in search mode ignore the ordering and use the ranking order
|
||||||
// provided by the server-side FTS backend instead, unless it was
|
// provided by the server-side FTS backend instead, unless it was
|
||||||
// requested by the user by actively ordering in search mode
|
// requested by the user by actively ordering in search mode
|
||||||
if (searchTerm === "") {
|
if (searchTerm === "" || !options?.isServerSideFTS) {
|
||||||
this._sorter.setField(this._savedOrderByField); // restore ordering
|
this._sorter.setField(this._savedOrderByField); // restore ordering
|
||||||
} else {
|
} else {
|
||||||
this._sorter.setField(null);
|
this._sorter.setField(null);
|
||||||
}
|
}
|
||||||
this._paginator.setPage(1);
|
this._paginator.setPage(1);
|
||||||
this._changed({ search: true, pagination: { page: true } });
|
return this._changed({ search: true, pagination: { page: true } });
|
||||||
};
|
};
|
||||||
|
|
||||||
updateSelectedTags = selectedTags => {
|
updateSelectedTags = (selectedTags) => {
|
||||||
this._selectedTags = selectedTags;
|
this._selectedTags = selectedTags;
|
||||||
this._paginator.setPage(1);
|
this._paginator.setPage(1);
|
||||||
this._changed({ tags: true, pagination: { page: true } });
|
this._changed({ tags: true, pagination: { page: true } });
|
||||||
@@ -153,7 +158,7 @@ export class ItemsSource {
|
|||||||
|
|
||||||
update = () => this._changed();
|
update = () => this._changed();
|
||||||
|
|
||||||
handleError = error => {
|
handleError = (error) => {
|
||||||
if (isFunction(this.onError)) {
|
if (isFunction(this.onError)) {
|
||||||
this.onError(error);
|
this.onError(error);
|
||||||
}
|
}
|
||||||
@@ -172,7 +177,7 @@ export class ResourceItemsSource extends ItemsSource {
|
|||||||
processResults: (results, context) => {
|
processResults: (results, context) => {
|
||||||
let processItem = getItemProcessor(context);
|
let processItem = getItemProcessor(context);
|
||||||
processItem = isFunction(processItem) ? processItem : identity;
|
processItem = isFunction(processItem) ? processItem : identity;
|
||||||
return map(results, item => processItem(item, context));
|
return map(results, (item) => processItem(item, context));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const Columns = {
|
|||||||
date(overrides) {
|
date(overrides) {
|
||||||
return extend(
|
return extend(
|
||||||
{
|
{
|
||||||
render: text => formatDate(text),
|
render: (text) => formatDate(text),
|
||||||
},
|
},
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
@@ -52,7 +52,7 @@ export const Columns = {
|
|||||||
dateTime(overrides) {
|
dateTime(overrides) {
|
||||||
return extend(
|
return extend(
|
||||||
{
|
{
|
||||||
render: text => formatDateTime(text),
|
render: (text) => formatDateTime(text),
|
||||||
},
|
},
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
@@ -62,7 +62,7 @@ export const Columns = {
|
|||||||
{
|
{
|
||||||
width: "1%",
|
width: "1%",
|
||||||
className: "text-nowrap",
|
className: "text-nowrap",
|
||||||
render: text => durationHumanize(text),
|
render: (text) => durationHumanize(text),
|
||||||
},
|
},
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
@@ -70,7 +70,7 @@ export const Columns = {
|
|||||||
timeAgo(overrides, timeAgoCustomProps = undefined) {
|
timeAgo(overrides, timeAgoCustomProps = undefined) {
|
||||||
return extend(
|
return extend(
|
||||||
{
|
{
|
||||||
render: value => <TimeAgo date={value} {...timeAgoCustomProps} />,
|
render: (value) => <TimeAgo date={value} {...timeAgoCustomProps} />,
|
||||||
},
|
},
|
||||||
overrides
|
overrides
|
||||||
);
|
);
|
||||||
@@ -110,6 +110,7 @@ export default class ItemsTable extends React.Component {
|
|||||||
orderByField: PropTypes.string,
|
orderByField: PropTypes.string,
|
||||||
orderByReverse: PropTypes.bool,
|
orderByReverse: PropTypes.bool,
|
||||||
toggleSorting: PropTypes.func,
|
toggleSorting: PropTypes.func,
|
||||||
|
setSorting: PropTypes.func,
|
||||||
"data-test": PropTypes.string,
|
"data-test": PropTypes.string,
|
||||||
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
rowKey: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||||
};
|
};
|
||||||
@@ -127,18 +128,15 @@ export default class ItemsTable extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
prepareColumns() {
|
prepareColumns() {
|
||||||
const { orderByField, orderByReverse, toggleSorting } = this.props;
|
const { orderByField, orderByReverse } = this.props;
|
||||||
const orderByDirection = orderByReverse ? "descend" : "ascend";
|
const orderByDirection = orderByReverse ? "descend" : "ascend";
|
||||||
|
|
||||||
return map(
|
return map(
|
||||||
map(
|
map(
|
||||||
filter(this.props.columns, column => (isFunction(column.isAvailable) ? column.isAvailable() : true)),
|
filter(this.props.columns, (column) => (isFunction(column.isAvailable) ? column.isAvailable() : true)),
|
||||||
column => extend(column, { orderByField: column.orderByField || column.field })
|
(column) => extend(column, { orderByField: column.orderByField || column.field })
|
||||||
),
|
),
|
||||||
(column, index) => {
|
(column, index) => {
|
||||||
// Bind click events only to sortable columns
|
|
||||||
const onHeaderCell = column.sorter ? () => ({ onClick: () => toggleSorting(column.orderByField) }) : null;
|
|
||||||
|
|
||||||
// Wrap render function to pass correct arguments
|
// Wrap render function to pass correct arguments
|
||||||
const render = isFunction(column.render) ? (text, row) => column.render(text, row.item) : identity;
|
const render = isFunction(column.render) ? (text, row) => column.render(text, row.item) : identity;
|
||||||
|
|
||||||
@@ -146,14 +144,13 @@ export default class ItemsTable extends React.Component {
|
|||||||
key: "column" + index,
|
key: "column" + index,
|
||||||
dataIndex: ["item", column.field],
|
dataIndex: ["item", column.field],
|
||||||
defaultSortOrder: column.orderByField === orderByField ? orderByDirection : null,
|
defaultSortOrder: column.orderByField === orderByField ? orderByDirection : null,
|
||||||
onHeaderCell,
|
|
||||||
render,
|
render,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowKey = record => {
|
getRowKey = (record) => {
|
||||||
const { rowKey } = this.props;
|
const { rowKey } = this.props;
|
||||||
if (rowKey) {
|
if (rowKey) {
|
||||||
if (isFunction(rowKey)) {
|
if (isFunction(rowKey)) {
|
||||||
@@ -172,22 +169,43 @@ export default class ItemsTable extends React.Component {
|
|||||||
|
|
||||||
// Bind events only if `onRowClick` specified
|
// Bind events only if `onRowClick` specified
|
||||||
const onTableRow = isFunction(this.props.onRowClick)
|
const onTableRow = isFunction(this.props.onRowClick)
|
||||||
? row => ({
|
? (row) => ({
|
||||||
onClick: event => {
|
onClick: (event) => {
|
||||||
this.props.onRowClick(event, row.item);
|
this.props.onRowClick(event, row.item);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const onChange = (pagination, filters, sorter, extra) => {
|
||||||
|
const action = extra?.action;
|
||||||
|
if (action === "sort") {
|
||||||
|
const propsColumn = this.props.columns.find((column) => column.field === sorter.field[1]);
|
||||||
|
if (!propsColumn.sorter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let orderByField = propsColumn.orderByField;
|
||||||
|
const orderByReverse = sorter.order === "descend";
|
||||||
|
|
||||||
|
if (orderByReverse === undefined) {
|
||||||
|
orderByField = null;
|
||||||
|
}
|
||||||
|
if (this.props.setSorting) {
|
||||||
|
this.props.setSorting(orderByField, orderByReverse);
|
||||||
|
} else {
|
||||||
|
this.props.toggleSorting(orderByField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { showHeader } = this.props;
|
const { showHeader } = this.props;
|
||||||
if (this.props.loading) {
|
if (this.props.loading) {
|
||||||
if (isEmpty(tableDataProps.dataSource)) {
|
if (isEmpty(tableDataProps.dataSource)) {
|
||||||
tableDataProps.columns = tableDataProps.columns.map(column => ({
|
tableDataProps.columns = tableDataProps.columns.map((column) => ({
|
||||||
...column,
|
...column,
|
||||||
sorter: false,
|
sorter: false,
|
||||||
render: () => <Skeleton active paragraph={false} />,
|
render: () => <Skeleton active paragraph={false} />,
|
||||||
}));
|
}));
|
||||||
tableDataProps.dataSource = range(10).map(key => ({ key: `${key}` }));
|
tableDataProps.dataSource = range(10).map((key) => ({ key: `${key}` }));
|
||||||
} else {
|
} else {
|
||||||
tableDataProps.loading = { indicator: null };
|
tableDataProps.loading = { indicator: null };
|
||||||
}
|
}
|
||||||
@@ -200,6 +218,7 @@ export default class ItemsTable extends React.Component {
|
|||||||
rowKey={this.getRowKey}
|
rowKey={this.getRowKey}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
onRow={onTableRow}
|
onRow={onTableRow}
|
||||||
|
onChange={onChange}
|
||||||
data-test={this.props["data-test"]}
|
data-test={this.props["data-test"]}
|
||||||
{...tableDataProps}
|
{...tableDataProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export const Query = PropTypes.shape({
|
|||||||
|
|
||||||
export const AlertOptions = PropTypes.shape({
|
export const AlertOptions = PropTypes.shape({
|
||||||
column: PropTypes.string,
|
column: PropTypes.string,
|
||||||
|
selector: PropTypes.oneOf(["first", "min", "max"]),
|
||||||
op: PropTypes.oneOf([">", ">=", "<", "<=", "==", "!="]),
|
op: PropTypes.oneOf([">", ">=", "<", "<=", "==", "!="]),
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
custom_subject: PropTypes.string,
|
custom_subject: PropTypes.string,
|
||||||
@@ -83,6 +84,7 @@ export const Alert = PropTypes.shape({
|
|||||||
query: Query,
|
query: Query,
|
||||||
options: PropTypes.shape({
|
options: PropTypes.shape({
|
||||||
column: PropTypes.string,
|
column: PropTypes.string,
|
||||||
|
selector: PropTypes.string,
|
||||||
op: PropTypes.string,
|
op: PropTypes.string,
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
|||||||
@@ -47,20 +47,30 @@ function SchemaItem({ item, expanded, onToggle, onSelect, ...props }) {
|
|||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<div className="schema-list-item">
|
<div className="schema-list-item">
|
||||||
<PlainButton className="table-name" onClick={onToggle}>
|
<Tooltip
|
||||||
<i className="fa fa-table m-r-5" aria-hidden="true" />
|
title={item.description}
|
||||||
<strong>
|
mouseEnterDelay={0}
|
||||||
<span title={item.name}>{tableDisplayName}</span>
|
mouseLeaveDelay={0}
|
||||||
{!isNil(item.size) && <span> ({item.size})</span>}
|
placement="rightTop"
|
||||||
</strong>
|
trigger={item.description ? "hover" : ""}
|
||||||
</PlainButton>
|
overlayStyle={{ whiteSpace: "pre-line" }}
|
||||||
|
>
|
||||||
|
<PlainButton className="table-name" onClick={onToggle}>
|
||||||
|
<i className="fa fa-table m-r-5" aria-hidden="true" />
|
||||||
|
<strong>
|
||||||
|
<span title={item.name}>{tableDisplayName}</span>
|
||||||
|
{!isNil(item.size) && <span> ({item.size})</span>}
|
||||||
|
</strong>
|
||||||
|
</PlainButton>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Insert table name into query text"
|
title="Insert table name into query text"
|
||||||
mouseEnterDelay={0}
|
mouseEnterDelay={0}
|
||||||
mouseLeaveDelay={0}
|
mouseLeaveDelay={0}
|
||||||
placement="topRight"
|
placement="topRight"
|
||||||
arrowPointAtCenter>
|
arrowPointAtCenter
|
||||||
<PlainButton className="copy-to-editor" onClick={e => handleSelect(e, item.name)}>
|
>
|
||||||
|
<PlainButton className="copy-to-editor" onClick={(e) => handleSelect(e, item.name)}>
|
||||||
<i className="fa fa-angle-double-right" aria-hidden="true" />
|
<i className="fa fa-angle-double-right" aria-hidden="true" />
|
||||||
</PlainButton>
|
</PlainButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -70,16 +80,23 @@ function SchemaItem({ item, expanded, onToggle, onSelect, ...props }) {
|
|||||||
{item.loading ? (
|
{item.loading ? (
|
||||||
<div className="table-open">Loading...</div>
|
<div className="table-open">Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
map(item.columns, column => {
|
map(item.columns, (column) => {
|
||||||
const columnName = get(column, "name");
|
const columnName = get(column, "name");
|
||||||
const columnType = get(column, "type");
|
const columnType = get(column, "type");
|
||||||
|
const columnDescription = get(column, "description");
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title="Insert column name into query text"
|
title={"Insert column name into query text" + (columnDescription ? "\n" + columnDescription : "")}
|
||||||
mouseEnterDelay={0}
|
mouseEnterDelay={0}
|
||||||
mouseLeaveDelay={0}
|
mouseLeaveDelay={0}
|
||||||
placement="rightTop">
|
placement="rightTop"
|
||||||
<PlainButton key={columnName} className="table-open-item" onClick={e => handleSelect(e, columnName)}>
|
overlayStyle={{ whiteSpace: "pre-line" }}
|
||||||
|
>
|
||||||
|
<PlainButton
|
||||||
|
key={columnName}
|
||||||
|
className="table-open-item"
|
||||||
|
onClick={(e) => handleSelect(e, columnName)}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{columnName} {columnType && <span className="column-type">{columnType}</span>}
|
{columnName} {columnType && <span className="column-type">{columnType}</span>}
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +185,7 @@ export function SchemaList({ loading, schema, expandedFlags, onTableExpand, onIt
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function applyFilterOnSchema(schema, filterString) {
|
export function applyFilterOnSchema(schema, filterString) {
|
||||||
const filters = filter(filterString.toLowerCase().split(/\s+/), s => s.length > 0);
|
const filters = filter(filterString.toLowerCase().split(/\s+/), (s) => s.length > 0);
|
||||||
|
|
||||||
// Empty string: return original schema
|
// Empty string: return original schema
|
||||||
if (filters.length === 0) {
|
if (filters.length === 0) {
|
||||||
@@ -181,9 +198,9 @@ export function applyFilterOnSchema(schema, filterString) {
|
|||||||
const columnFilter = filters[0];
|
const columnFilter = filters[0];
|
||||||
return filter(
|
return filter(
|
||||||
schema,
|
schema,
|
||||||
item =>
|
(item) =>
|
||||||
includes(item.name.toLowerCase(), nameFilter) ||
|
includes(item.name.toLowerCase(), nameFilter) ||
|
||||||
some(item.columns, column => includes(get(column, "name").toLowerCase(), columnFilter))
|
some(item.columns, (column) => includes(get(column, "name").toLowerCase(), columnFilter))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,11 +208,11 @@ export function applyFilterOnSchema(schema, filterString) {
|
|||||||
const nameFilter = filters[0];
|
const nameFilter = filters[0];
|
||||||
const columnFilter = filters[1];
|
const columnFilter = filters[1];
|
||||||
return filter(
|
return filter(
|
||||||
map(schema, item => {
|
map(schema, (item) => {
|
||||||
if (includes(item.name.toLowerCase(), nameFilter)) {
|
if (includes(item.name.toLowerCase(), nameFilter)) {
|
||||||
item = {
|
item = {
|
||||||
...item,
|
...item,
|
||||||
columns: filter(item.columns, column => includes(get(column, "name").toLowerCase(), columnFilter)),
|
columns: filter(item.columns, (column) => includes(get(column, "name").toLowerCase(), columnFilter)),
|
||||||
};
|
};
|
||||||
return item.columns.length > 0 ? item : null;
|
return item.columns.length > 0 ? item : null;
|
||||||
}
|
}
|
||||||
@@ -243,7 +260,7 @@ export default function SchemaBrowser({
|
|||||||
placeholder="Search schema..."
|
placeholder="Search schema..."
|
||||||
aria-label="Search schema"
|
aria-label="Search schema"
|
||||||
disabled={schema.length === 0}
|
disabled={schema.length === 0}
|
||||||
onChange={event => handleFilterChange(event.target.value)}
|
onChange={(event) => handleFilterChange(event.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip title="Refresh Schema">
|
<Tooltip title="Refresh Schema">
|
||||||
|
|||||||
@@ -148,7 +148,9 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
|
|||||||
|
|
||||||
function dismiss() {
|
function dismiss() {
|
||||||
const optionsChanged = !isEqual(options, defaultState.originalOptions);
|
const optionsChanged = !isEqual(options, defaultState.originalOptions);
|
||||||
confirmDialogClose(nameChanged || optionsChanged).then(dialog.dismiss);
|
confirmDialogClose(nameChanged || optionsChanged)
|
||||||
|
.then(dialog.dismiss)
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// When editing existing visualization chart type selector is disabled, so add only existing visualization's
|
// When editing existing visualization chart type selector is disabled, so add only existing visualization's
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function wrapComponentWithSettings(WrappedComponent) {
|
|||||||
"dateTimeFormat",
|
"dateTimeFormat",
|
||||||
"integerFormat",
|
"integerFormat",
|
||||||
"floatFormat",
|
"floatFormat",
|
||||||
|
"nullValue",
|
||||||
"booleanValues",
|
"booleanValues",
|
||||||
"tableCellMaxJSONSize",
|
"tableCellMaxJSONSize",
|
||||||
"allowCustomJSVisualizations",
|
"allowCustomJSVisualizations",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
columns: 6, // grid columns count
|
columns: 12, // grid columns count
|
||||||
rowHeight: 50, // grid row height (incl. bottom padding)
|
rowHeight: 50, // grid row height (incl. bottom padding)
|
||||||
margins: 15, // widget margins
|
margins: 15, // widget margins
|
||||||
mobileBreakPoint: 800,
|
mobileBreakPoint: 800,
|
||||||
// defaults for widgets
|
// defaults for widgets
|
||||||
defaultSizeX: 3,
|
defaultSizeX: 6,
|
||||||
defaultSizeY: 3,
|
defaultSizeY: 3,
|
||||||
minSizeX: 1,
|
minSizeX: 2,
|
||||||
maxSizeX: 6,
|
maxSizeX: 12,
|
||||||
minSizeY: 1,
|
minSizeY: 2,
|
||||||
maxSizeY: 1000,
|
maxSizeY: 1000,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" translate="no">
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<base href="{{base_href}}" />
|
<base href="{{base_href}}" />
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
<script src="/static/unsupportedRedirect.js" async></script>
|
<script src="<%= htmlWebpackPlugin.options.staticPath %>unsupportedRedirect.js" async></script>
|
||||||
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon-96x96.png" />
|
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon-96x96.png" />
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import MenuButton from "./components/MenuButton";
|
|||||||
import AlertView from "./AlertView";
|
import AlertView from "./AlertView";
|
||||||
import AlertEdit from "./AlertEdit";
|
import AlertEdit from "./AlertEdit";
|
||||||
import AlertNew from "./AlertNew";
|
import AlertNew from "./AlertNew";
|
||||||
|
import notifications from "@/services/notifications";
|
||||||
|
|
||||||
const MODES = {
|
const MODES = {
|
||||||
NEW: 0,
|
NEW: 0,
|
||||||
@@ -64,6 +65,7 @@ class Alert extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
alert: {
|
alert: {
|
||||||
options: {
|
options: {
|
||||||
|
selector: "first",
|
||||||
op: ">",
|
op: ">",
|
||||||
value: 1,
|
value: 1,
|
||||||
muted: false,
|
muted: false,
|
||||||
@@ -75,7 +77,7 @@ class Alert extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
const { alertId } = this.props;
|
const { alertId } = this.props;
|
||||||
AlertService.get({ id: alertId })
|
AlertService.get({ id: alertId })
|
||||||
.then(alert => {
|
.then((alert) => {
|
||||||
if (this._isMounted) {
|
if (this._isMounted) {
|
||||||
const canEdit = currentUser.canEdit(alert);
|
const canEdit = currentUser.canEdit(alert);
|
||||||
|
|
||||||
@@ -93,7 +95,7 @@ class Alert extends React.Component {
|
|||||||
this.onQuerySelected(alert.query);
|
this.onQuerySelected(alert.query);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
if (this._isMounted) {
|
if (this._isMounted) {
|
||||||
this.props.onError(error);
|
this.props.onError(error);
|
||||||
}
|
}
|
||||||
@@ -112,7 +114,7 @@ class Alert extends React.Component {
|
|||||||
alert.rearm = pendingRearm || null;
|
alert.rearm = pendingRearm || null;
|
||||||
|
|
||||||
return AlertService.save(alert)
|
return AlertService.save(alert)
|
||||||
.then(alert => {
|
.then((alert) => {
|
||||||
notification.success("Saved.");
|
notification.success("Saved.");
|
||||||
navigateTo(`alerts/${alert.id}`, true);
|
navigateTo(`alerts/${alert.id}`, true);
|
||||||
this.setState({ alert, mode: MODES.VIEW });
|
this.setState({ alert, mode: MODES.VIEW });
|
||||||
@@ -122,7 +124,7 @@ class Alert extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onQuerySelected = query => {
|
onQuerySelected = (query) => {
|
||||||
this.setState(({ alert }) => ({
|
this.setState(({ alert }) => ({
|
||||||
alert: Object.assign(alert, { query }),
|
alert: Object.assign(alert, { query }),
|
||||||
queryResult: null,
|
queryResult: null,
|
||||||
@@ -130,7 +132,7 @@ class Alert extends React.Component {
|
|||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
// get cached result for column names and values
|
// get cached result for column names and values
|
||||||
new QueryService(query).getQueryResultPromise().then(queryResult => {
|
new QueryService(query).getQueryResultPromise().then((queryResult) => {
|
||||||
if (this._isMounted) {
|
if (this._isMounted) {
|
||||||
this.setState({ queryResult });
|
this.setState({ queryResult });
|
||||||
let { column } = this.state.alert.options;
|
let { column } = this.state.alert.options;
|
||||||
@@ -146,18 +148,18 @@ class Alert extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onNameChange = name => {
|
onNameChange = (name) => {
|
||||||
const { alert } = this.state;
|
const { alert } = this.state;
|
||||||
this.setState({
|
this.setState({
|
||||||
alert: Object.assign(alert, { name }),
|
alert: Object.assign(alert, { name }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onRearmChange = pendingRearm => {
|
onRearmChange = (pendingRearm) => {
|
||||||
this.setState({ pendingRearm });
|
this.setState({ pendingRearm });
|
||||||
};
|
};
|
||||||
|
|
||||||
setAlertOptions = obj => {
|
setAlertOptions = (obj) => {
|
||||||
const { alert } = this.state;
|
const { alert } = this.state;
|
||||||
const options = { ...alert.options, ...obj };
|
const options = { ...alert.options, ...obj };
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -177,6 +179,17 @@ class Alert extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
evaluate = () => {
|
||||||
|
const { alert } = this.state;
|
||||||
|
return AlertService.evaluate(alert)
|
||||||
|
.then(() => {
|
||||||
|
notification.success("Alert evaluated. Refresh page for updated status.");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.error("Failed to evaluate alert.");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
mute = () => {
|
mute = () => {
|
||||||
const { alert } = this.state;
|
const { alert } = this.state;
|
||||||
return AlertService.mute(alert)
|
return AlertService.mute(alert)
|
||||||
@@ -223,7 +236,14 @@ class Alert extends React.Component {
|
|||||||
const { queryResult, mode, canEdit, pendingRearm } = this.state;
|
const { queryResult, mode, canEdit, pendingRearm } = this.state;
|
||||||
|
|
||||||
const menuButton = (
|
const menuButton = (
|
||||||
<MenuButton doDelete={this.delete} muted={muted} mute={this.mute} unmute={this.unmute} canEdit={canEdit} />
|
<MenuButton
|
||||||
|
doDelete={this.delete}
|
||||||
|
muted={muted}
|
||||||
|
mute={this.mute}
|
||||||
|
unmute={this.unmute}
|
||||||
|
canEdit={canEdit}
|
||||||
|
evaluate={this.evaluate}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
@@ -258,7 +278,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/alerts/new",
|
path: "/alerts/new",
|
||||||
title: "New Alert",
|
title: "New Alert",
|
||||||
render: pageProps => <Alert {...pageProps} mode={MODES.NEW} />,
|
render: (pageProps) => <Alert {...pageProps} mode={MODES.NEW} />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -266,7 +286,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/alerts/:alertId",
|
path: "/alerts/:alertId",
|
||||||
title: "Alert",
|
title: "Alert",
|
||||||
render: pageProps => <Alert {...pageProps} mode={MODES.VIEW} />,
|
render: (pageProps) => <Alert {...pageProps} mode={MODES.VIEW} />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -274,6 +294,6 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/alerts/:alertId/edit",
|
path: "/alerts/:alertId/edit",
|
||||||
title: "Alert",
|
title: "Alert",
|
||||||
render: pageProps => <Alert {...pageProps} mode={MODES.EDIT} />,
|
render: (pageProps) => <Alert {...pageProps} mode={MODES.EDIT} />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -68,13 +68,23 @@ export default class AlertView extends React.Component {
|
|||||||
<>
|
<>
|
||||||
<Title name={name} alert={alert}>
|
<Title name={name} alert={alert}>
|
||||||
<DynamicComponent name="AlertView.HeaderExtra" alert={alert} />
|
<DynamicComponent name="AlertView.HeaderExtra" alert={alert} />
|
||||||
<Tooltip title={canEdit ? "" : "You do not have sufficient permissions to edit this alert"}>
|
{canEdit ? (
|
||||||
<Button type="default" onClick={canEdit ? onEdit : null} className={cx({ disabled: !canEdit })}>
|
<>
|
||||||
<i className="fa fa-edit m-r-5" aria-hidden="true" />
|
<Button type="default" onClick={canEdit ? onEdit : null} className={cx({ disabled: !canEdit })}>
|
||||||
Edit
|
<i className="fa fa-edit m-r-5" aria-hidden="true" />
|
||||||
</Button>
|
Edit
|
||||||
{menuButton}
|
</Button>
|
||||||
</Tooltip>
|
{menuButton}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Tooltip title="You do not have sufficient permissions to edit this alert">
|
||||||
|
<Button type="default" onClick={canEdit ? onEdit : null} className={cx({ disabled: !canEdit })}>
|
||||||
|
<i className="fa fa-edit m-r-5" aria-hidden="true" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
{menuButton}
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Title>
|
</Title>
|
||||||
<div className="bg-white tiled p-20">
|
<div className="bg-white tiled p-20">
|
||||||
<Grid.Row type="flex" gutter={16}>
|
<Grid.Row type="flex" gutter={16}>
|
||||||
|
|||||||
@@ -54,23 +54,74 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
|
|||||||
return null;
|
return null;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const columnHint = (
|
let columnHint;
|
||||||
<small className="alert-criteria-hint">
|
|
||||||
Top row value is <code className="p-0">{toString(columnValue) || "unknown"}</code>
|
if (alertOptions.selector === "first") {
|
||||||
</small>
|
columnHint = (
|
||||||
);
|
<small className="alert-criteria-hint">
|
||||||
|
Top row value is <code className="p-0">{toString(columnValue) || "unknown"}</code>
|
||||||
|
</small>
|
||||||
|
);
|
||||||
|
} else if (alertOptions.selector === "max") {
|
||||||
|
columnHint = (
|
||||||
|
<small className="alert-criteria-hint">
|
||||||
|
Max column value is{" "}
|
||||||
|
<code className="p-0">
|
||||||
|
{toString(
|
||||||
|
Math.max(...resultValues.map((o) => Number(o[alertOptions.column])).filter((value) => !isNaN(value)))
|
||||||
|
) || "unknown"}
|
||||||
|
</code>
|
||||||
|
</small>
|
||||||
|
);
|
||||||
|
} else if (alertOptions.selector === "min") {
|
||||||
|
columnHint = (
|
||||||
|
<small className="alert-criteria-hint">
|
||||||
|
Min column value is{" "}
|
||||||
|
<code className="p-0">
|
||||||
|
{toString(
|
||||||
|
Math.min(...resultValues.map((o) => Number(o[alertOptions.column])).filter((value) => !isNaN(value)))
|
||||||
|
) || "unknown"}
|
||||||
|
</code>
|
||||||
|
</small>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-test="Criteria">
|
<div data-test="Criteria">
|
||||||
|
<div className="input-title">
|
||||||
|
<span className="input-label">Selector</span>
|
||||||
|
{editMode ? (
|
||||||
|
<Select
|
||||||
|
value={alertOptions.selector}
|
||||||
|
onChange={(selector) => onChange({ selector })}
|
||||||
|
optionLabelProp="label"
|
||||||
|
dropdownMatchSelectWidth={false}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
>
|
||||||
|
<Select.Option value="first" label="first">
|
||||||
|
first
|
||||||
|
</Select.Option>
|
||||||
|
<Select.Option value="min" label="min">
|
||||||
|
min
|
||||||
|
</Select.Option>
|
||||||
|
<Select.Option value="max" label="max">
|
||||||
|
max
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<DisabledInput minWidth={60}>{alertOptions.selector}</DisabledInput>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="input-title">
|
<div className="input-title">
|
||||||
<span className="input-label">Value column</span>
|
<span className="input-label">Value column</span>
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<Select
|
<Select
|
||||||
value={alertOptions.column}
|
value={alertOptions.column}
|
||||||
onChange={column => onChange({ column })}
|
onChange={(column) => onChange({ column })}
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownMatchSelectWidth={false}
|
||||||
style={{ minWidth: 100 }}>
|
style={{ minWidth: 100 }}
|
||||||
{columnNames.map(name => (
|
>
|
||||||
|
{columnNames.map((name) => (
|
||||||
<Select.Option key={name}>{name}</Select.Option>
|
<Select.Option key={name}>{name}</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -83,10 +134,11 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
|
|||||||
{editMode ? (
|
{editMode ? (
|
||||||
<Select
|
<Select
|
||||||
value={alertOptions.op}
|
value={alertOptions.op}
|
||||||
onChange={op => onChange({ op })}
|
onChange={(op) => onChange({ op })}
|
||||||
optionLabelProp="label"
|
optionLabelProp="label"
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownMatchSelectWidth={false}
|
||||||
style={{ width: 55 }}>
|
style={{ width: 55 }}
|
||||||
|
>
|
||||||
<Select.Option value=">" label={CONDITIONS[">"]}>
|
<Select.Option value=">" label={CONDITIONS[">"]}>
|
||||||
{CONDITIONS[">"]} greater than
|
{CONDITIONS[">"]} greater than
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
@@ -125,7 +177,7 @@ export default function Criteria({ columnNames, resultValues, alertOptions, onCh
|
|||||||
id="threshold-criterion"
|
id="threshold-criterion"
|
||||||
style={{ width: 90 }}
|
style={{ width: 90 }}
|
||||||
value={alertOptions.value}
|
value={alertOptions.value}
|
||||||
onChange={e => onChange({ value: e.target.value })}
|
onChange={(e) => onChange({ value: e.target.value })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DisabledInput minWidth={50}>{alertOptions.value}</DisabledInput>
|
<DisabledInput minWidth={50}>{alertOptions.value}</DisabledInput>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import LoadingOutlinedIcon from "@ant-design/icons/LoadingOutlined";
|
|||||||
import EllipsisOutlinedIcon from "@ant-design/icons/EllipsisOutlined";
|
import EllipsisOutlinedIcon from "@ant-design/icons/EllipsisOutlined";
|
||||||
import PlainButton from "@/components/PlainButton";
|
import PlainButton from "@/components/PlainButton";
|
||||||
|
|
||||||
export default function MenuButton({ doDelete, canEdit, mute, unmute, muted }) {
|
export default function MenuButton({ doDelete, canEdit, mute, unmute, evaluate, muted }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const execute = useCallback(action => {
|
const execute = useCallback(action => {
|
||||||
@@ -55,6 +55,9 @@ export default function MenuButton({ doDelete, canEdit, mute, unmute, muted }) {
|
|||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<PlainButton onClick={confirmDelete}>Delete</PlainButton>
|
<PlainButton onClick={confirmDelete}>Delete</PlainButton>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<PlainButton onClick={() => execute(evaluate)}>Evaluate</PlainButton>
|
||||||
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
}>
|
}>
|
||||||
<Button aria-label="More actions">
|
<Button aria-label="More actions">
|
||||||
@@ -69,6 +72,7 @@ MenuButton.propTypes = {
|
|||||||
canEdit: PropTypes.bool.isRequired,
|
canEdit: PropTypes.bool.isRequired,
|
||||||
mute: PropTypes.func.isRequired,
|
mute: PropTypes.func.isRequired,
|
||||||
unmute: PropTypes.func.isRequired,
|
unmute: PropTypes.func.isRequired,
|
||||||
|
evaluate: PropTypes.func.isRequired,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -81,12 +81,19 @@ function DashboardListExtraActions(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DashboardList({ controller }) {
|
function DashboardList({ controller }) {
|
||||||
|
let usedListColumns = listColumns;
|
||||||
|
if (controller.params.currentPage === "favorites") {
|
||||||
|
usedListColumns = [
|
||||||
|
...usedListColumns,
|
||||||
|
Columns.dateTime.sortable({ title: "Starred At", field: "starred_at", width: "1%" }),
|
||||||
|
];
|
||||||
|
}
|
||||||
const {
|
const {
|
||||||
areExtraActionsAvailable,
|
areExtraActionsAvailable,
|
||||||
listColumns: tableColumns,
|
listColumns: tableColumns,
|
||||||
Component: ExtraActionsComponent,
|
Component: ExtraActionsComponent,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
} = useItemsListExtraActions(controller, listColumns, DashboardListExtraActions);
|
} = useItemsListExtraActions(controller, usedListColumns, DashboardListExtraActions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-dashboard-list">
|
<div className="page-dashboard-list">
|
||||||
@@ -139,9 +146,9 @@ function DashboardList({ controller }) {
|
|||||||
showPageSizeSelect
|
showPageSizeSelect
|
||||||
totalCount={controller.totalItemsCount}
|
totalCount={controller.totalItemsCount}
|
||||||
pageSize={controller.itemsPerPage}
|
pageSize={controller.itemsPerPage}
|
||||||
onPageSizeChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
|
onPageSizeChange={(itemsPerPage) => controller.updatePagination({ itemsPerPage })}
|
||||||
page={controller.page}
|
page={controller.page}
|
||||||
onChange={page => controller.updatePagination({ page })}
|
onChange={(page) => controller.updatePagination({ page })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@@ -170,10 +177,10 @@ const DashboardListPage = itemsList(
|
|||||||
}[currentPage];
|
}[currentPage];
|
||||||
},
|
},
|
||||||
getItemProcessor() {
|
getItemProcessor() {
|
||||||
return item => new Dashboard(item);
|
return (item) => new Dashboard(item);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
() => new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
({ ...props }) => new UrlStateStorage({ orderByField: props.orderByField ?? "created_at", orderByReverse: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -181,7 +188,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboards",
|
path: "/dashboards",
|
||||||
title: "Dashboards",
|
title: "Dashboards",
|
||||||
render: pageProps => <DashboardListPage {...pageProps} currentPage="all" />,
|
render: (pageProps) => <DashboardListPage {...pageProps} currentPage="all" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -189,7 +196,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboards/favorites",
|
path: "/dashboards/favorites",
|
||||||
title: "Favorite Dashboards",
|
title: "Favorite Dashboards",
|
||||||
render: pageProps => <DashboardListPage {...pageProps} currentPage="favorites" />,
|
render: (pageProps) => <DashboardListPage {...pageProps} currentPage="favorites" orderByField="starred_at" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -197,6 +204,6 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboards/my",
|
path: "/dashboards/my",
|
||||||
title: "My Dashboards",
|
title: "My Dashboards",
|
||||||
render: pageProps => <DashboardListPage {...pageProps} currentPage="my" />,
|
render: (pageProps) => <DashboardListPage {...pageProps} currentPage="my" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ function DashboardSettings({ dashboardConfiguration }) {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!!dashboard.dashboard_filters_enabled}
|
checked={!!dashboard.dashboard_filters_enabled}
|
||||||
onChange={({ target }) => updateDashboard({ dashboard_filters_enabled: target.checked })}
|
onChange={({ target }) => updateDashboard({ dashboard_filters_enabled: target.checked })}
|
||||||
data-test="DashboardFiltersCheckbox">
|
data-test="DashboardFiltersCheckbox"
|
||||||
|
>
|
||||||
Use Dashboard Level Filters
|
Use Dashboard Level Filters
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,9 +91,9 @@ function DashboardComponent(props) {
|
|||||||
|
|
||||||
const [pageContainer, setPageContainer] = useState(null);
|
const [pageContainer, setPageContainer] = useState(null);
|
||||||
const [bottomPanelStyles, setBottomPanelStyles] = useState({});
|
const [bottomPanelStyles, setBottomPanelStyles] = useState({});
|
||||||
const onParametersEdit = parameters => {
|
const onParametersEdit = (parameters) => {
|
||||||
const paramOrder = map(parameters, "name");
|
const paramOrder = map(parameters, "name");
|
||||||
updateDashboard({ options: { globalParamOrder: paramOrder } });
|
updateDashboard({ options: { ...dashboard.options, globalParamOrder: paramOrder } });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -175,7 +176,7 @@ function DashboardPage({ dashboardSlug, dashboardId, onError }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Dashboard.get({ id: dashboardId, slug: dashboardSlug })
|
Dashboard.get({ id: dashboardId, slug: dashboardSlug })
|
||||||
.then(dashboardData => {
|
.then((dashboardData) => {
|
||||||
recordEvent("view", "dashboard", dashboardData.id);
|
recordEvent("view", "dashboard", dashboardData.id);
|
||||||
setDashboard(dashboardData);
|
setDashboard(dashboardData);
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ routes.register(
|
|||||||
"Dashboards.LegacyViewOrEdit",
|
"Dashboards.LegacyViewOrEdit",
|
||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboard/:dashboardSlug",
|
path: "/dashboard/:dashboardSlug",
|
||||||
render: pageProps => <DashboardPage {...pageProps} />,
|
render: (pageProps) => <DashboardPage {...pageProps} />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -215,6 +216,6 @@ routes.register(
|
|||||||
"Dashboards.ViewOrEdit",
|
"Dashboards.ViewOrEdit",
|
||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/dashboards/:dashboardId([^-]+)(-.*)?",
|
path: "/dashboards/:dashboardId([^-]+)(-.*)?",
|
||||||
render: pageProps => <DashboardPage {...pageProps} />,
|
render: (pageProps) => <DashboardPage {...pageProps} />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .container {
|
> .container {
|
||||||
min-height: calc(100vh - 95px);
|
min-height: calc(100% - 95px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-message {
|
.loading-message {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { DashboardStatusEnum } from "../hooks/useDashboard";
|
|||||||
import "./DashboardHeader.less";
|
import "./DashboardHeader.less";
|
||||||
|
|
||||||
function getDashboardTags() {
|
function getDashboardTags() {
|
||||||
return getTags("api/dashboards/tags").then(tags => map(tags, t => t.name));
|
return getTags("api/dashboards/tags").then((tags) => map(tags, (t) => t.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buttonType(value) {
|
function buttonType(value) {
|
||||||
@@ -38,7 +38,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
|
|||||||
<h3>
|
<h3>
|
||||||
<EditInPlace
|
<EditInPlace
|
||||||
isEditable={editingLayout}
|
isEditable={editingLayout}
|
||||||
onDone={name => updateDashboard({ name })}
|
onDone={(name) => updateDashboard({ name })}
|
||||||
value={dashboard.name}
|
value={dashboard.name}
|
||||||
ignoreBlanks
|
ignoreBlanks
|
||||||
/>
|
/>
|
||||||
@@ -53,7 +53,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
|
|||||||
isArchived={dashboard.is_archived}
|
isArchived={dashboard.is_archived}
|
||||||
canEdit={canEditDashboard}
|
canEdit={canEditDashboard}
|
||||||
getAvailableTags={getDashboardTags}
|
getAvailableTags={getDashboardTags}
|
||||||
onEdit={tags => updateDashboard({ tags })}
|
onEdit={(tags) => updateDashboard({ tags })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -89,14 +89,15 @@ function RefreshButton({ dashboardConfiguration }) {
|
|||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
overlay={
|
overlay={
|
||||||
<Menu onClick={onRefreshRateSelected} selectedKeys={[`${refreshRate}`]}>
|
<Menu onClick={onRefreshRateSelected} selectedKeys={[`${refreshRate}`]}>
|
||||||
{refreshRateOptions.map(option => (
|
{refreshRateOptions.map((option) => (
|
||||||
<Menu.Item key={`${option}`} disabled={!includes(allowedIntervals, option)}>
|
<Menu.Item key={`${option}`} disabled={!includes(allowedIntervals, option)}>
|
||||||
{durationHumanize(option)}
|
{durationHumanize(option)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
{refreshRate && <Menu.Item key={null}>Disable auto refresh</Menu.Item>}
|
{refreshRate && <Menu.Item key={null}>Disable auto refresh</Menu.Item>}
|
||||||
</Menu>
|
</Menu>
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<Button className="icon-button hidden-xs" type={buttonType(refreshRate)}>
|
<Button className="icon-button hidden-xs" type={buttonType(refreshRate)}>
|
||||||
<i className="fa fa-angle-down" aria-hidden="true" />
|
<i className="fa fa-angle-down" aria-hidden="true" />
|
||||||
<span className="sr-only">Split button!</span>
|
<span className="sr-only">Split button!</span>
|
||||||
@@ -119,6 +120,8 @@ function DashboardMoreOptionsButton({ dashboardConfiguration }) {
|
|||||||
managePermissions,
|
managePermissions,
|
||||||
gridDisabled,
|
gridDisabled,
|
||||||
isDashboardOwnerOrAdmin,
|
isDashboardOwnerOrAdmin,
|
||||||
|
isDuplicating,
|
||||||
|
duplicateDashboard,
|
||||||
} = dashboardConfiguration;
|
} = dashboardConfiguration;
|
||||||
|
|
||||||
const archive = () => {
|
const archive = () => {
|
||||||
@@ -142,6 +145,14 @@ function DashboardMoreOptionsButton({ dashboardConfiguration }) {
|
|||||||
<Menu.Item className={cx({ hidden: gridDisabled })}>
|
<Menu.Item className={cx({ hidden: gridDisabled })}>
|
||||||
<PlainButton onClick={() => setEditingLayout(true)}>Edit</PlainButton>
|
<PlainButton onClick={() => setEditingLayout(true)}>Edit</PlainButton>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
{!isDuplicating && dashboard.canEdit() && (
|
||||||
|
<Menu.Item>
|
||||||
|
<PlainButton onClick={duplicateDashboard}>
|
||||||
|
Fork <i className="fa fa-external-link m-l-5" aria-hidden="true" />
|
||||||
|
<span className="sr-only">(opens in a new tab)</span>
|
||||||
|
</PlainButton>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
{clientConfig.showPermissionsControl && isDashboardOwnerOrAdmin && (
|
{clientConfig.showPermissionsControl && isDashboardOwnerOrAdmin && (
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<PlainButton onClick={managePermissions}>Manage Permissions</PlainButton>
|
<PlainButton onClick={managePermissions}>Manage Permissions</PlainButton>
|
||||||
@@ -156,7 +167,8 @@ function DashboardMoreOptionsButton({ dashboardConfiguration }) {
|
|||||||
<PlainButton onClick={archive}>Archive</PlainButton>
|
<PlainButton onClick={archive}>Archive</PlainButton>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<Button className="icon-button m-l-5" data-test="DashboardMoreButton" aria-label="More actions">
|
<Button className="icon-button m-l-5" data-test="DashboardMoreButton" aria-label="More actions">
|
||||||
<EllipsisOutlinedIcon rotate={90} aria-hidden="true" />
|
<EllipsisOutlinedIcon rotate={90} aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -206,7 +218,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
|
|||||||
type={buttonType(fullscreen)}
|
type={buttonType(fullscreen)}
|
||||||
className="icon-button m-l-5"
|
className="icon-button m-l-5"
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
aria-label="Toggle fullscreen display">
|
aria-label="Toggle fullscreen display"
|
||||||
|
>
|
||||||
<i className="zmdi zmdi-fullscreen" aria-hidden="true" />
|
<i className="zmdi zmdi-fullscreen" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -219,7 +232,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
|
|||||||
type={buttonType(dashboard.publicAccessEnabled)}
|
type={buttonType(dashboard.publicAccessEnabled)}
|
||||||
onClick={showShareDashboardDialog}
|
onClick={showShareDashboardDialog}
|
||||||
data-test="OpenShareForm"
|
data-test="OpenShareForm"
|
||||||
aria-label="Share">
|
aria-label="Share"
|
||||||
|
>
|
||||||
<i className="zmdi zmdi-share" aria-hidden="true" />
|
<i className="zmdi zmdi-share" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -242,7 +256,11 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
|
|||||||
doneBtnClickedWhileSaving,
|
doneBtnClickedWhileSaving,
|
||||||
dashboardStatus,
|
dashboardStatus,
|
||||||
retrySaveDashboardLayout,
|
retrySaveDashboardLayout,
|
||||||
|
saveDashboardParameters,
|
||||||
} = dashboardConfiguration;
|
} = dashboardConfiguration;
|
||||||
|
const handleDoneEditing = () => {
|
||||||
|
saveDashboardParameters().then(() => setEditingLayout(false));
|
||||||
|
};
|
||||||
let status;
|
let status;
|
||||||
if (dashboardStatus === DashboardStatusEnum.SAVED) {
|
if (dashboardStatus === DashboardStatusEnum.SAVED) {
|
||||||
status = <span className="save-status">Saved</span>;
|
status = <span className="save-status">Saved</span>;
|
||||||
@@ -267,7 +285,7 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
|
|||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button loading={doneBtnClickedWhileSaving} type="primary" onClick={() => setEditingLayout(false)}>
|
<Button loading={doneBtnClickedWhileSaving} type="primary" onClick={handleDoneEditing}>
|
||||||
{!doneBtnClickedWhileSaving && <i className="fa fa-check m-r-5" aria-hidden="true" />} Done Editing
|
{!doneBtnClickedWhileSaving && <i className="fa fa-check m-r-5" aria-hidden="true" />} Done Editing
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -118,28 +118,9 @@ class ShareDashboardDialog extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{dashboard.public_url && (
|
{dashboard.public_url && (
|
||||||
<>
|
<Form.Item label="Secret address" {...this.formItemProps}>
|
||||||
<Form.Item>
|
<InputWithCopy value={dashboard.public_url} data-test="SecretAddress" />
|
||||||
<Alert
|
</Form.Item>
|
||||||
message={
|
|
||||||
<div>
|
|
||||||
Custom rule for hiding filter components when sharing links:
|
|
||||||
<br />
|
|
||||||
You can hide filter components by appending `&hide_filter={"{{"} component_name{"}}"}` to the
|
|
||||||
sharing URL.
|
|
||||||
<br />
|
|
||||||
Example: http://{"{{"}ip{"}}"}:{"{{"}port{"}}"}/public/dashboards/{"{{"}id{"}}"}
|
|
||||||
?p_country=ghana&p_site=10&hide_filter=country
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
type="warning"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label="Secret address" {...this.formItemProps}>
|
|
||||||
<InputWithCopy value={dashboard.public_url} data-test="SecretAddress" />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -15,18 +15,19 @@ import ShareDashboardDialog from "../components/ShareDashboardDialog";
|
|||||||
import useFullscreenHandler from "../../../lib/hooks/useFullscreenHandler";
|
import useFullscreenHandler from "../../../lib/hooks/useFullscreenHandler";
|
||||||
import useRefreshRateHandler from "./useRefreshRateHandler";
|
import useRefreshRateHandler from "./useRefreshRateHandler";
|
||||||
import useEditModeHandler from "./useEditModeHandler";
|
import useEditModeHandler from "./useEditModeHandler";
|
||||||
|
import useDuplicateDashboard from "./useDuplicateDashboard";
|
||||||
import { policy } from "@/services/policy";
|
import { policy } from "@/services/policy";
|
||||||
|
|
||||||
export { DashboardStatusEnum } from "./useEditModeHandler";
|
export { DashboardStatusEnum } from "./useEditModeHandler";
|
||||||
|
|
||||||
function getAffectedWidgets(widgets, updatedParameters = []) {
|
function getAffectedWidgets(widgets, updatedParameters = []) {
|
||||||
return !isEmpty(updatedParameters)
|
return !isEmpty(updatedParameters)
|
||||||
? widgets.filter(widget =>
|
? widgets.filter((widget) =>
|
||||||
Object.values(widget.getParameterMappings())
|
Object.values(widget.getParameterMappings())
|
||||||
.filter(({ type }) => type === "dashboard-level")
|
.filter(({ type }) => type === "dashboard-level")
|
||||||
.some(({ mapTo }) =>
|
.some(({ mapTo }) =>
|
||||||
includes(
|
includes(
|
||||||
updatedParameters.map(p => p.name),
|
updatedParameters.map((p) => p.name),
|
||||||
mapTo
|
mapTo
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -49,10 +50,12 @@ function useDashboard(dashboardData) {
|
|||||||
[dashboard]
|
[dashboard]
|
||||||
);
|
);
|
||||||
const hasOnlySafeQueries = useMemo(
|
const hasOnlySafeQueries = useMemo(
|
||||||
() => every(dashboard.widgets, w => (w.getQuery() ? w.getQuery().is_safe : true)),
|
() => every(dashboard.widgets, (w) => (w.getQuery() ? w.getQuery().is_safe : true)),
|
||||||
[dashboard]
|
[dashboard]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isDuplicating, duplicateDashboard] = useDuplicateDashboard(dashboard);
|
||||||
|
|
||||||
const managePermissions = useCallback(() => {
|
const managePermissions = useCallback(() => {
|
||||||
const aclUrl = `api/dashboards/${dashboard.id}/acl`;
|
const aclUrl = `api/dashboards/${dashboard.id}/acl`;
|
||||||
PermissionsEditorDialog.showModal({
|
PermissionsEditorDialog.showModal({
|
||||||
@@ -64,19 +67,19 @@ function useDashboard(dashboardData) {
|
|||||||
|
|
||||||
const updateDashboard = useCallback(
|
const updateDashboard = useCallback(
|
||||||
(data, includeVersion = true) => {
|
(data, includeVersion = true) => {
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard, data));
|
setDashboard((currentDashboard) => extend({}, currentDashboard, data));
|
||||||
data = { ...data, id: dashboard.id };
|
data = { ...data, id: dashboard.id };
|
||||||
if (includeVersion) {
|
if (includeVersion) {
|
||||||
data = { ...data, version: dashboard.version };
|
data = { ...data, version: dashboard.version };
|
||||||
}
|
}
|
||||||
return Dashboard.save(data)
|
return Dashboard.save(data)
|
||||||
.then(updatedDashboard => {
|
.then((updatedDashboard) => {
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
|
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
|
||||||
if (has(data, "name")) {
|
if (has(data, "name")) {
|
||||||
location.setPath(url.parse(updatedDashboard.url).pathname, true);
|
location.setPath(url.parse(updatedDashboard.url).pathname, true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
const status = get(error, "response.status");
|
const status = get(error, "response.status");
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
notification.error("Dashboard update failed", "Permission Denied.");
|
notification.error("Dashboard update failed", "Permission Denied.");
|
||||||
@@ -99,25 +102,25 @@ function useDashboard(dashboardData) {
|
|||||||
|
|
||||||
const loadWidget = useCallback((widget, forceRefresh = false) => {
|
const loadWidget = useCallback((widget, forceRefresh = false) => {
|
||||||
widget.getParametersDefs(); // Force widget to read parameters values from URL
|
widget.getParametersDefs(); // Force widget to read parameters values from URL
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard));
|
setDashboard((currentDashboard) => extend({}, currentDashboard));
|
||||||
return widget
|
return widget
|
||||||
.load(forceRefresh)
|
.load(forceRefresh)
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
// QueryResultErrors are expected
|
// QueryResultErrors are expected
|
||||||
if (error instanceof QueryResultError) {
|
if (error instanceof QueryResultError) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
})
|
})
|
||||||
.finally(() => setDashboard(currentDashboard => extend({}, currentDashboard)));
|
.finally(() => setDashboard((currentDashboard) => extend({}, currentDashboard)));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const refreshWidget = useCallback(widget => loadWidget(widget, true), [loadWidget]);
|
const refreshWidget = useCallback((widget) => loadWidget(widget, true), [loadWidget]);
|
||||||
|
|
||||||
const removeWidget = useCallback(widgetId => {
|
const removeWidget = useCallback((widgetId) => {
|
||||||
setDashboard(currentDashboard =>
|
setDashboard((currentDashboard) =>
|
||||||
extend({}, currentDashboard, {
|
extend({}, currentDashboard, {
|
||||||
widgets: currentDashboard.widgets.filter(widget => widget.id !== undefined && widget.id !== widgetId),
|
widgets: currentDashboard.widgets.filter((widget) => widget.id !== undefined && widget.id !== widgetId),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -129,11 +132,11 @@ function useDashboard(dashboardData) {
|
|||||||
(forceRefresh = false, updatedParameters = []) => {
|
(forceRefresh = false, updatedParameters = []) => {
|
||||||
const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters);
|
const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters);
|
||||||
const loadWidgetPromises = compact(
|
const loadWidgetPromises = compact(
|
||||||
affectedWidgets.map(widget => loadWidget(widget, forceRefresh).catch(error => error))
|
affectedWidgets.map((widget) => loadWidget(widget, forceRefresh).catch((error) => error))
|
||||||
);
|
);
|
||||||
|
|
||||||
return Promise.all(loadWidgetPromises).then(() => {
|
return Promise.all(loadWidgetPromises).then(() => {
|
||||||
const queryResults = compact(map(dashboardRef.current.widgets, widget => widget.getQueryResult()));
|
const queryResults = compact(map(dashboardRef.current.widgets, (widget) => widget.getQueryResult()));
|
||||||
const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search);
|
const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search);
|
||||||
setFilters(updatedFilters);
|
setFilters(updatedFilters);
|
||||||
});
|
});
|
||||||
@@ -142,7 +145,7 @@ function useDashboard(dashboardData) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const refreshDashboard = useCallback(
|
const refreshDashboard = useCallback(
|
||||||
updatedParameters => {
|
(updatedParameters) => {
|
||||||
if (!refreshing) {
|
if (!refreshing) {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
loadDashboard(true, updatedParameters).finally(() => setRefreshing(false));
|
loadDashboard(true, updatedParameters).finally(() => setRefreshing(false));
|
||||||
@@ -151,15 +154,30 @@ function useDashboard(dashboardData) {
|
|||||||
[refreshing, loadDashboard]
|
[refreshing, loadDashboard]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const saveDashboardParameters = useCallback(() => {
|
||||||
|
const currentDashboard = dashboardRef.current;
|
||||||
|
|
||||||
|
return updateDashboard({
|
||||||
|
options: {
|
||||||
|
...currentDashboard.options,
|
||||||
|
parameters: map(globalParameters, (p) => p.toSaveableObject()),
|
||||||
|
},
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Failed to persist parameter values:", error);
|
||||||
|
notification.error("Parameter values could not be saved. Your changes may not be persisted.");
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}, [globalParameters, updateDashboard]);
|
||||||
|
|
||||||
const archiveDashboard = useCallback(() => {
|
const archiveDashboard = useCallback(() => {
|
||||||
recordEvent("archive", "dashboard", dashboard.id);
|
recordEvent("archive", "dashboard", dashboard.id);
|
||||||
Dashboard.delete(dashboard).then(updatedDashboard =>
|
Dashboard.delete(dashboard).then((updatedDashboard) =>
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
|
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
|
||||||
);
|
);
|
||||||
}, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const showShareDashboardDialog = useCallback(() => {
|
const showShareDashboardDialog = useCallback(() => {
|
||||||
const handleDialogClose = () => setDashboard(currentDashboard => extend({}, currentDashboard));
|
const handleDialogClose = () => setDashboard((currentDashboard) => extend({}, currentDashboard));
|
||||||
|
|
||||||
ShareDashboardDialog.showModal({
|
ShareDashboardDialog.showModal({
|
||||||
dashboard,
|
dashboard,
|
||||||
@@ -172,8 +190,8 @@ function useDashboard(dashboardData) {
|
|||||||
const showAddTextboxDialog = useCallback(() => {
|
const showAddTextboxDialog = useCallback(() => {
|
||||||
TextboxDialog.showModal({
|
TextboxDialog.showModal({
|
||||||
isNew: true,
|
isNew: true,
|
||||||
}).onClose(text =>
|
}).onClose((text) =>
|
||||||
dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard)))
|
dashboard.addWidget(text).then(() => setDashboard((currentDashboard) => extend({}, currentDashboard)))
|
||||||
);
|
);
|
||||||
}, [dashboard]);
|
}, [dashboard]);
|
||||||
|
|
||||||
@@ -185,13 +203,13 @@ function useDashboard(dashboardData) {
|
|||||||
.addWidget(visualization, {
|
.addWidget(visualization, {
|
||||||
parameterMappings: editableMappingsToParameterMappings(parameterMappings),
|
parameterMappings: editableMappingsToParameterMappings(parameterMappings),
|
||||||
})
|
})
|
||||||
.then(widget => {
|
.then((widget) => {
|
||||||
const widgetsToSave = [
|
const widgetsToSave = [
|
||||||
widget,
|
widget,
|
||||||
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
|
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
|
||||||
];
|
];
|
||||||
return Promise.all(widgetsToSave.map(w => w.save())).then(() =>
|
return Promise.all(widgetsToSave.map((w) => w.save())).then(() =>
|
||||||
setDashboard(currentDashboard => extend({}, currentDashboard))
|
setDashboard((currentDashboard) => extend({}, currentDashboard))
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -235,6 +253,7 @@ function useDashboard(dashboardData) {
|
|||||||
setRefreshRate,
|
setRefreshRate,
|
||||||
disableRefreshRate,
|
disableRefreshRate,
|
||||||
...editModeHandler,
|
...editModeHandler,
|
||||||
|
saveDashboardParameters,
|
||||||
gridDisabled,
|
gridDisabled,
|
||||||
setGridDisabled,
|
setGridDisabled,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
@@ -243,6 +262,8 @@ function useDashboard(dashboardData) {
|
|||||||
showAddTextboxDialog,
|
showAddTextboxDialog,
|
||||||
showAddWidgetDialog,
|
showAddWidgetDialog,
|
||||||
managePermissions,
|
managePermissions,
|
||||||
|
isDuplicating,
|
||||||
|
duplicateDashboard,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
40
client/app/pages/dashboards/hooks/useDuplicateDashboard.js
Normal file
40
client/app/pages/dashboards/hooks/useDuplicateDashboard.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { noop, extend, pick } from "lodash";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import url from "url";
|
||||||
|
import qs from "query-string";
|
||||||
|
import { Dashboard } from "@/services/dashboard";
|
||||||
|
|
||||||
|
function keepCurrentUrlParams(targetUrl) {
|
||||||
|
const currentUrlParams = qs.parse(window.location.search);
|
||||||
|
targetUrl = url.parse(targetUrl);
|
||||||
|
const targetUrlParams = qs.parse(targetUrl.search);
|
||||||
|
return url.format(
|
||||||
|
extend(pick(targetUrl, ["protocol", "auth", "host", "pathname"]), {
|
||||||
|
search: qs.stringify(extend(currentUrlParams, targetUrlParams)),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useDuplicateDashboard(dashboard) {
|
||||||
|
const [isDuplicating, setIsDuplicating] = useState(false);
|
||||||
|
|
||||||
|
const duplicateDashboard = useCallback(() => {
|
||||||
|
// To prevent opening the same tab, name must be unique for each browser
|
||||||
|
const tabName = `duplicatedDashboardTab/${Math.random().toString()}`;
|
||||||
|
|
||||||
|
// We should open tab here because this moment is a part of user interaction;
|
||||||
|
// later browser will block such attempts
|
||||||
|
const tab = window.open("", tabName);
|
||||||
|
|
||||||
|
setIsDuplicating(true);
|
||||||
|
Dashboard.fork({ id: dashboard.id })
|
||||||
|
.then(newDashboard => {
|
||||||
|
tab.location = keepCurrentUrlParams(newDashboard.getUrl());
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsDuplicating(false);
|
||||||
|
});
|
||||||
|
}, [dashboard.id]);
|
||||||
|
|
||||||
|
return [isDuplicating, isDuplicating ? noop : duplicateDashboard];
|
||||||
|
}
|
||||||
@@ -31,7 +31,8 @@ function DeprecatedEmbedFeatureAlert() {
|
|||||||
<Link
|
<Link
|
||||||
href="https://discuss.redash.io/t/support-for-parameters-in-embedded-visualizations/3337"
|
href="https://discuss.redash.io/t/support-for-parameters-in-embedded-visualizations/3337"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
Read more
|
Read more
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
@@ -43,7 +44,7 @@ function DeprecatedEmbedFeatureAlert() {
|
|||||||
|
|
||||||
function EmailNotVerifiedAlert() {
|
function EmailNotVerifiedAlert() {
|
||||||
const verifyEmail = () => {
|
const verifyEmail = () => {
|
||||||
axios.post("verification_email/").then(data => {
|
axios.post("verification_email/").then((data) => {
|
||||||
notification.success(data.message);
|
notification.success(data.message);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -100,6 +101,6 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/",
|
path: "/",
|
||||||
title: "Redash",
|
title: "Redash",
|
||||||
render: pageProps => <Home {...pageProps} />,
|
render: (pageProps) => <Home {...pageProps} />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function FavoriteList({ title, resource, itemUrl, emptyState }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
resource
|
resource
|
||||||
.favorites()
|
.favorites({ order: "-starred_at" })
|
||||||
.then(({ results }) => setItems(results))
|
.then(({ results }) => setItems(results))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [resource]);
|
}, [resource]);
|
||||||
@@ -28,7 +28,7 @@ export function FavoriteList({ title, resource, itemUrl, emptyState }) {
|
|||||||
</div>
|
</div>
|
||||||
{!isEmpty(items) && (
|
{!isEmpty(items) && (
|
||||||
<div role="list" className="list-group">
|
<div role="list" className="list-group">
|
||||||
{items.map(item => (
|
{items.map((item) => (
|
||||||
<Link key={itemUrl(item)} role="listitem" className="list-group-item" href={itemUrl(item)}>
|
<Link key={itemUrl(item)} role="listitem" className="list-group-item" href={itemUrl(item)}>
|
||||||
<span className="btn-favorite m-r-5">
|
<span className="btn-favorite m-r-5">
|
||||||
<i className="fa fa-star" aria-hidden="true" />
|
<i className="fa fa-star" aria-hidden="true" />
|
||||||
@@ -61,7 +61,7 @@ export function DashboardAndQueryFavoritesList() {
|
|||||||
<FavoriteList
|
<FavoriteList
|
||||||
title="Favorite Dashboards"
|
title="Favorite Dashboards"
|
||||||
resource={Dashboard}
|
resource={Dashboard}
|
||||||
itemUrl={dashboard => dashboard.url}
|
itemUrl={(dashboard) => dashboard.url}
|
||||||
emptyState={
|
emptyState={
|
||||||
<p>
|
<p>
|
||||||
<span className="btn-favorite m-r-5">
|
<span className="btn-favorite m-r-5">
|
||||||
@@ -76,7 +76,7 @@ export function DashboardAndQueryFavoritesList() {
|
|||||||
<FavoriteList
|
<FavoriteList
|
||||||
title="Favorite Queries"
|
title="Favorite Queries"
|
||||||
resource={Query}
|
resource={Query}
|
||||||
itemUrl={query => `queries/${query.id}`}
|
itemUrl={(query) => `queries/${query.id}`}
|
||||||
emptyState={
|
emptyState={
|
||||||
<p>
|
<p>
|
||||||
<span className="btn-favorite m-r-5">
|
<span className="btn-favorite m-r-5">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useCallback, useEffect, useRef } from "react";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
||||||
import routeWithUserSession from "@/components/ApplicationArea/routeWithUserSession";
|
import routeWithUserSession from "@/components/ApplicationArea/routeWithUserSession";
|
||||||
@@ -20,7 +20,7 @@ import ItemsTable, { Columns } from "@/components/items-list/components/ItemsTab
|
|||||||
import Layout from "@/components/layouts/ContentWithSidebar";
|
import Layout from "@/components/layouts/ContentWithSidebar";
|
||||||
|
|
||||||
import { Query } from "@/services/query";
|
import { Query } from "@/services/query";
|
||||||
import { currentUser } from "@/services/auth";
|
import { clientConfig, currentUser } from "@/services/auth";
|
||||||
import location from "@/services/location";
|
import location from "@/services/location";
|
||||||
import routes from "@/services/routes";
|
import routes from "@/services/routes";
|
||||||
|
|
||||||
@@ -95,25 +95,39 @@ function QueriesList({ controller }) {
|
|||||||
const controllerRef = useRef();
|
const controllerRef = useRef();
|
||||||
controllerRef.current = controller;
|
controllerRef.current = controller;
|
||||||
|
|
||||||
|
const updateSearch = useCallback(
|
||||||
|
(searchTemm) => {
|
||||||
|
controller.updateSearch(searchTemm, { isServerSideFTS: !clientConfig.multiByteSearchEnabled });
|
||||||
|
},
|
||||||
|
[controller]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unlistenLocationChanges = location.listen((unused, action) => {
|
const unlistenLocationChanges = location.listen((unused, action) => {
|
||||||
const searchTerm = location.search.q || "";
|
const searchTerm = location.search.q || "";
|
||||||
if (action === "PUSH" && searchTerm !== controllerRef.current.searchTerm) {
|
if (action === "PUSH" && searchTerm !== controllerRef.current.searchTerm) {
|
||||||
controllerRef.current.updateSearch(searchTerm);
|
updateSearch(searchTerm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlistenLocationChanges();
|
unlistenLocationChanges();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [updateSearch]);
|
||||||
|
|
||||||
|
let usedListColumns = listColumns;
|
||||||
|
if (controller.params.currentPage === "favorites") {
|
||||||
|
usedListColumns = [
|
||||||
|
...usedListColumns,
|
||||||
|
Columns.dateTime.sortable({ title: "Starred At", field: "starred_at", width: "1%" }),
|
||||||
|
];
|
||||||
|
}
|
||||||
const {
|
const {
|
||||||
areExtraActionsAvailable,
|
areExtraActionsAvailable,
|
||||||
listColumns: tableColumns,
|
listColumns: tableColumns,
|
||||||
Component: ExtraActionsComponent,
|
Component: ExtraActionsComponent,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
} = useItemsListExtraActions(controller, listColumns, QueriesListExtraActions);
|
} = useItemsListExtraActions(controller, usedListColumns, QueriesListExtraActions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-queries-list">
|
<div className="page-queries-list">
|
||||||
@@ -135,7 +149,7 @@ function QueriesList({ controller }) {
|
|||||||
placeholder="Search Queries..."
|
placeholder="Search Queries..."
|
||||||
label="Search queries"
|
label="Search queries"
|
||||||
value={controller.searchTerm}
|
value={controller.searchTerm}
|
||||||
onChange={controller.updateSearch}
|
onChange={updateSearch}
|
||||||
/>
|
/>
|
||||||
<Sidebar.Menu items={sidebarMenu} selected={controller.params.currentPage} />
|
<Sidebar.Menu items={sidebarMenu} selected={controller.params.currentPage} />
|
||||||
<Sidebar.Tags url="api/queries/tags" onChange={controller.updateSelectedTags} showUnselectAll />
|
<Sidebar.Tags url="api/queries/tags" onChange={controller.updateSelectedTags} showUnselectAll />
|
||||||
@@ -160,14 +174,15 @@ function QueriesList({ controller }) {
|
|||||||
orderByField={controller.orderByField}
|
orderByField={controller.orderByField}
|
||||||
orderByReverse={controller.orderByReverse}
|
orderByReverse={controller.orderByReverse}
|
||||||
toggleSorting={controller.toggleSorting}
|
toggleSorting={controller.toggleSorting}
|
||||||
|
setSorting={controller.setSorting}
|
||||||
/>
|
/>
|
||||||
<Paginator
|
<Paginator
|
||||||
showPageSizeSelect
|
showPageSizeSelect
|
||||||
totalCount={controller.totalItemsCount}
|
totalCount={controller.totalItemsCount}
|
||||||
pageSize={controller.itemsPerPage}
|
pageSize={controller.itemsPerPage}
|
||||||
onPageSizeChange={itemsPerPage => controller.updatePagination({ itemsPerPage })}
|
onPageSizeChange={(itemsPerPage) => controller.updatePagination({ itemsPerPage })}
|
||||||
page={controller.page}
|
page={controller.page}
|
||||||
onChange={page => controller.updatePagination({ page })}
|
onChange={(page) => controller.updatePagination({ page })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@@ -196,10 +211,10 @@ const QueriesListPage = itemsList(
|
|||||||
}[currentPage];
|
}[currentPage];
|
||||||
},
|
},
|
||||||
getItemProcessor() {
|
getItemProcessor() {
|
||||||
return item => new Query(item);
|
return (item) => new Query(item);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
() => new UrlStateStorage({ orderByField: "created_at", orderByReverse: true })
|
({ ...props }) => new UrlStateStorage({ orderByField: props.orderByField ?? "created_at", orderByReverse: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -207,7 +222,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/queries",
|
path: "/queries",
|
||||||
title: "Queries",
|
title: "Queries",
|
||||||
render: pageProps => <QueriesListPage {...pageProps} currentPage="all" />,
|
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="all" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -215,7 +230,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/queries/favorites",
|
path: "/queries/favorites",
|
||||||
title: "Favorite Queries",
|
title: "Favorite Queries",
|
||||||
render: pageProps => <QueriesListPage {...pageProps} currentPage="favorites" />,
|
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="favorites" orderByField="starred_at" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -223,7 +238,7 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/queries/archive",
|
path: "/queries/archive",
|
||||||
title: "Archived Queries",
|
title: "Archived Queries",
|
||||||
render: pageProps => <QueriesListPage {...pageProps} currentPage="archive" />,
|
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="archive" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
routes.register(
|
routes.register(
|
||||||
@@ -231,6 +246,6 @@ routes.register(
|
|||||||
routeWithUserSession({
|
routeWithUserSession({
|
||||||
path: "/queries/my",
|
path: "/queries/my",
|
||||||
title: "My Queries",
|
title: "My Queries",
|
||||||
render: pageProps => <QueriesListPage {...pageProps} currentPage="my" />,
|
render: (pageProps) => <QueriesListPage {...pageProps} currentPage="my" />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,9 +37,10 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
max-height: unset !important;
|
||||||
.ant-input {
|
.ant-input {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
max-height: 150px - 15px * 2;
|
height: 30vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import QueryControlDropdown from "@/components/EditVisualizationButton/QueryCont
|
|||||||
import EditVisualizationButton from "@/components/EditVisualizationButton";
|
import EditVisualizationButton from "@/components/EditVisualizationButton";
|
||||||
import useQueryResultData from "@/lib/useQueryResultData";
|
import useQueryResultData from "@/lib/useQueryResultData";
|
||||||
import { durationHumanize, pluralize, prettySize } from "@/lib/utils";
|
import { durationHumanize, pluralize, prettySize } from "@/lib/utils";
|
||||||
|
import { isUndefined } from "lodash";
|
||||||
|
|
||||||
import "./QueryExecutionMetadata.less";
|
import "./QueryExecutionMetadata.less";
|
||||||
|
|
||||||
@@ -51,7 +52,8 @@ export default function QueryExecutionMetadata({
|
|||||||
"Result truncated to " +
|
"Result truncated to " +
|
||||||
queryResultData.rows.length +
|
queryResultData.rows.length +
|
||||||
" rows. Databricks may truncate query results that are unstably large."
|
" rows. Databricks may truncate query results that are unstably large."
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<WarningTwoTone twoToneColor="#FF9800" />
|
<WarningTwoTone twoToneColor="#FF9800" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
@@ -67,10 +69,9 @@ export default function QueryExecutionMetadata({
|
|||||||
)}
|
)}
|
||||||
{isQueryExecuting && <span>Running…</span>}
|
{isQueryExecuting && <span>Running…</span>}
|
||||||
</span>
|
</span>
|
||||||
{queryResultData.metadata.data_scanned && (
|
{!isUndefined(queryResultData.metadata.data_scanned) && !isQueryExecuting && (
|
||||||
<span className="m-l-5">
|
<span className="m-l-5">
|
||||||
Data Scanned
|
Data Scanned <strong>{prettySize(queryResultData.metadata.data_scanned)}</strong>
|
||||||
<strong>{prettySize(queryResultData.metadata.data_scanned)}</strong>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export function QuerySourceTypeIcon(props) {
|
export function QuerySourceTypeIcon(props) {
|
||||||
return <img src={`static/images/db-logos/${props.type}.png`} width="20" alt={props.alt} />;
|
return <img src={`/static/images/db-logos/${props.type}.png`} width="20" alt={props.alt} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
QuerySourceTypeIcon.propTypes = {
|
QuerySourceTypeIcon.propTypes = {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function EmptyState({ title, message, refreshButton }) {
|
|||||||
<div className="query-results-empty-state">
|
<div className="query-results-empty-state">
|
||||||
<div className="empty-state-content">
|
<div className="empty-state-content">
|
||||||
<div>
|
<div>
|
||||||
<img src="static/images/illustrations/no-query-results.svg" alt="No Query Results Illustration" />
|
<img src="/static/images/illustrations/no-query-results.svg" alt="No Query Results Illustration" />
|
||||||
</div>
|
</div>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
<div className="m-b-20">{message}</div>
|
<div className="m-b-20">{message}</div>
|
||||||
@@ -40,7 +40,7 @@ EmptyState.defaultProps = {
|
|||||||
|
|
||||||
function TabWithDeleteButton({ visualizationName, canDelete, onDelete, ...props }) {
|
function TabWithDeleteButton({ visualizationName, canDelete, onDelete, ...props }) {
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
e => {
|
(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: "Delete Visualization",
|
title: "Delete Visualization",
|
||||||
@@ -111,7 +111,8 @@ export default function QueryVisualizationTabs({
|
|||||||
className="add-visualization-button"
|
className="add-visualization-button"
|
||||||
data-test="NewVisualization"
|
data-test="NewVisualization"
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => onAddVisualization()}>
|
onClick={() => onAddVisualization()}
|
||||||
|
>
|
||||||
<i className="fa fa-plus" aria-hidden="true" />
|
<i className="fa fa-plus" aria-hidden="true" />
|
||||||
<span className="m-l-5 hidden-xs">Add Visualization</span>
|
<span className="m-l-5 hidden-xs">Add Visualization</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -119,7 +120,7 @@ export default function QueryVisualizationTabs({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const orderedVisualizations = useMemo(() => orderBy(visualizations, ["id"]), [visualizations]);
|
const orderedVisualizations = useMemo(() => orderBy(visualizations, ["id"]), [visualizations]);
|
||||||
const isFirstVisualization = useCallback(visId => visId === orderedVisualizations[0].id, [orderedVisualizations]);
|
const isFirstVisualization = useCallback((visId) => visId === orderedVisualizations[0].id, [orderedVisualizations]);
|
||||||
const isMobile = useMedia({ maxWidth: 768 });
|
const isMobile = useMedia({ maxWidth: 768 });
|
||||||
|
|
||||||
const [filters, setFilters] = useState([]);
|
const [filters, setFilters] = useState([]);
|
||||||
@@ -132,9 +133,10 @@ export default function QueryVisualizationTabs({
|
|||||||
data-test="QueryPageVisualizationTabs"
|
data-test="QueryPageVisualizationTabs"
|
||||||
animated={false}
|
animated={false}
|
||||||
tabBarGutter={0}
|
tabBarGutter={0}
|
||||||
onChange={activeKey => onChangeTab(+activeKey)}
|
onChange={(activeKey) => onChangeTab(+activeKey)}
|
||||||
destroyInactiveTabPane>
|
destroyInactiveTabPane
|
||||||
{orderedVisualizations.map(visualization => (
|
>
|
||||||
|
{orderedVisualizations.map((visualization) => (
|
||||||
<TabPane
|
<TabPane
|
||||||
key={`${visualization.id}`}
|
key={`${visualization.id}`}
|
||||||
tab={
|
tab={
|
||||||
@@ -144,7 +146,8 @@ export default function QueryVisualizationTabs({
|
|||||||
visualizationName={visualization.name}
|
visualizationName={visualization.name}
|
||||||
onDelete={() => onDeleteVisualization(visualization.id)}
|
onDelete={() => onDeleteVisualization(visualization.id)}
|
||||||
/>
|
/>
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
{queryResult ? (
|
{queryResult ? (
|
||||||
<VisualizationRenderer
|
<VisualizationRenderer
|
||||||
visualization={visualization}
|
visualization={visualization}
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { reduce } from "lodash";
|
|
||||||
import localOptions from "@/lib/localOptions";
|
import localOptions from "@/lib/localOptions";
|
||||||
|
|
||||||
function calculateTokensCount(schema) {
|
|
||||||
return reduce(schema, (totalLength, table) => totalLength + table.columns.length, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useAutocompleteFlags(schema) {
|
export default function useAutocompleteFlags(schema) {
|
||||||
const isAvailable = useMemo(() => calculateTokensCount(schema) <= 5000, [schema]);
|
const isAvailable = true;
|
||||||
const [isEnabled, setIsEnabled] = useState(localOptions.get("liveAutocomplete", true));
|
const [isEnabled, setIsEnabled] = useState(localOptions.get("liveAutocomplete", true));
|
||||||
|
|
||||||
const toggleAutocomplete = useCallback(state => {
|
const toggleAutocomplete = useCallback((state) => {
|
||||||
setIsEnabled(state);
|
setIsEnabled(state);
|
||||||
localOptions.set("liveAutocomplete", state);
|
localOptions.set("liveAutocomplete", state);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -17,14 +17,16 @@ export default function BeaconConsentSettings(props) {
|
|||||||
Anonymous Usage Data Sharing
|
Anonymous Usage Data Sharing
|
||||||
<HelpTrigger className="m-l-5 m-r-5" type="USAGE_DATA_SHARING" />
|
<HelpTrigger className="m-l-5 m-r-5" type="USAGE_DATA_SHARING" />
|
||||||
</span>
|
</span>
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Skeleton title={{ width: 300 }} paragraph={false} active />
|
<Skeleton title={{ width: 300 }} paragraph={false} active />
|
||||||
) : (
|
) : (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="beacon_consent"
|
name="beacon_consent"
|
||||||
checked={values.beacon_consent}
|
checked={values.beacon_consent}
|
||||||
onChange={e => onChange({ beacon_consent: e.target.checked })}>
|
onChange={(e) => onChange({ beacon_consent: e.target.checked })}
|
||||||
|
>
|
||||||
Help Redash improve by automatically sending anonymous usage data
|
Help Redash improve by automatically sending anonymous usage data
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const Alert = {
|
|||||||
delete: data => axios.delete(`api/alerts/${data.id}`),
|
delete: data => axios.delete(`api/alerts/${data.id}`),
|
||||||
mute: data => axios.post(`api/alerts/${data.id}/mute`),
|
mute: data => axios.post(`api/alerts/${data.id}/mute`),
|
||||||
unmute: data => axios.delete(`api/alerts/${data.id}/mute`),
|
unmute: data => axios.delete(`api/alerts/${data.id}/mute`),
|
||||||
|
evaluate: data => axios.post(`api/alerts/${data.id}/eval`),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Alert;
|
export default Alert;
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ export const urlForDashboard = ({ id, slug }) => `dashboards/${id}-${slug}`;
|
|||||||
|
|
||||||
export function collectDashboardFilters(dashboard, queryResults, urlParams) {
|
export function collectDashboardFilters(dashboard, queryResults, urlParams) {
|
||||||
const filters = {};
|
const filters = {};
|
||||||
_.each(queryResults, queryResult => {
|
_.each(queryResults, (queryResult) => {
|
||||||
const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : [];
|
const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : [];
|
||||||
_.each(queryFilters, queryFilter => {
|
_.each(queryFilters, (queryFilter) => {
|
||||||
const hasQueryStringValue = _.has(urlParams, queryFilter.name);
|
const hasQueryStringValue = _.has(urlParams, queryFilter.name);
|
||||||
|
|
||||||
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
|
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
|
||||||
@@ -44,7 +44,7 @@ function prepareWidgetsForDashboard(widgets) {
|
|||||||
const defaultWidgetSizeY =
|
const defaultWidgetSizeY =
|
||||||
Math.max(
|
Math.max(
|
||||||
_.chain(widgets)
|
_.chain(widgets)
|
||||||
.map(w => w.options.position.sizeY)
|
.map((w) => w.options.position.sizeY)
|
||||||
.max()
|
.max()
|
||||||
.value(),
|
.value(),
|
||||||
20
|
20
|
||||||
@@ -55,11 +55,11 @@ function prepareWidgetsForDashboard(widgets) {
|
|||||||
// 2. update position of widgets in each row - place it right below
|
// 2. update position of widgets in each row - place it right below
|
||||||
// biggest widget from previous row
|
// biggest widget from previous row
|
||||||
_.chain(widgets)
|
_.chain(widgets)
|
||||||
.sortBy(widget => widget.options.position.row)
|
.sortBy((widget) => widget.options.position.row)
|
||||||
.groupBy(widget => widget.options.position.row)
|
.groupBy((widget) => widget.options.position.row)
|
||||||
.reduce((row, widgetsAtRow) => {
|
.reduce((row, widgetsAtRow) => {
|
||||||
let height = 1;
|
let height = 1;
|
||||||
_.each(widgetsAtRow, widget => {
|
_.each(widgetsAtRow, (widget) => {
|
||||||
height = Math.max(
|
height = Math.max(
|
||||||
height,
|
height,
|
||||||
widget.options.position.autoHeight ? defaultWidgetSizeY : widget.options.position.sizeY
|
widget.options.position.autoHeight ? defaultWidgetSizeY : widget.options.position.sizeY
|
||||||
@@ -74,8 +74,8 @@ function prepareWidgetsForDashboard(widgets) {
|
|||||||
.value();
|
.value();
|
||||||
|
|
||||||
// Sort widgets by updated column and row value
|
// Sort widgets by updated column and row value
|
||||||
widgets = _.sortBy(widgets, widget => widget.options.position.col);
|
widgets = _.sortBy(widgets, (widget) => widget.options.position.col);
|
||||||
widgets = _.sortBy(widgets, widget => widget.options.position.row);
|
widgets = _.sortBy(widgets, (widget) => widget.options.position.row);
|
||||||
|
|
||||||
return widgets;
|
return widgets;
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
|
|||||||
|
|
||||||
// Find first free row for each column
|
// Find first free row for each column
|
||||||
const bottomLine = _.chain(existingWidgets)
|
const bottomLine = _.chain(existingWidgets)
|
||||||
.map(w => {
|
.map((w) => {
|
||||||
const options = _.extend({}, w.options);
|
const options = _.extend({}, w.options);
|
||||||
const position = _.extend({ row: 0, sizeY: 0 }, options.position);
|
const position = _.extend({ row: 0, sizeY: 0 }, options.position);
|
||||||
return {
|
return {
|
||||||
@@ -97,21 +97,24 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
|
|||||||
height: position.sizeY,
|
height: position.sizeY,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.reduce((result, item) => {
|
.reduce(
|
||||||
const from = Math.max(item.left, 0);
|
(result, item) => {
|
||||||
const to = Math.min(item.right, result.length + 1);
|
const from = Math.max(item.left, 0);
|
||||||
for (let i = from; i < to; i += 1) {
|
const to = Math.min(item.right, result.length + 1);
|
||||||
result[i] = Math.max(result[i], item.bottom);
|
for (let i = from; i < to; i += 1) {
|
||||||
}
|
result[i] = Math.max(result[i], item.bottom);
|
||||||
return result;
|
}
|
||||||
}, _.map(new Array(dashboardGridOptions.columns), _.constant(0)))
|
return result;
|
||||||
|
},
|
||||||
|
_.map(new Array(dashboardGridOptions.columns), _.constant(0))
|
||||||
|
)
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
// Go through columns, pick them by count necessary to hold new block,
|
// Go through columns, pick them by count necessary to hold new block,
|
||||||
// and calculate bottom-most free row per group.
|
// and calculate bottom-most free row per group.
|
||||||
// Choose group with the top-most free row (comparing to other groups)
|
// Choose group with the top-most free row (comparing to other groups)
|
||||||
return _.chain(_.range(0, dashboardGridOptions.columns - width + 1))
|
return _.chain(_.range(0, dashboardGridOptions.columns - width + 1))
|
||||||
.map(col => ({
|
.map((col) => ({
|
||||||
col,
|
col,
|
||||||
row: _.chain(bottomLine)
|
row: _.chain(bottomLine)
|
||||||
.slice(col, col + width)
|
.slice(col, col + width)
|
||||||
@@ -126,14 +129,14 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
|
|||||||
export function Dashboard(dashboard) {
|
export function Dashboard(dashboard) {
|
||||||
_.extend(this, dashboard);
|
_.extend(this, dashboard);
|
||||||
Object.defineProperty(this, "url", {
|
Object.defineProperty(this, "url", {
|
||||||
get: function() {
|
get: function () {
|
||||||
return urlForDashboard(this);
|
return urlForDashboard(this);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareDashboardWidgets(widgets) {
|
function prepareDashboardWidgets(widgets) {
|
||||||
return prepareWidgetsForDashboard(_.map(widgets, widget => new Widget(widget)));
|
return prepareWidgetsForDashboard(_.map(widgets, (widget) => new Widget(widget)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformSingle(dashboard) {
|
function transformSingle(dashboard) {
|
||||||
@@ -154,7 +157,7 @@ function transformResponse(data) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveOrCreateUrl = data => (data.id ? `api/dashboards/${data.id}` : "api/dashboards");
|
const saveOrCreateUrl = (data) => (data.id ? `api/dashboards/${data.id}` : "api/dashboards");
|
||||||
const DashboardService = {
|
const DashboardService = {
|
||||||
get: ({ id, slug }) => {
|
get: ({ id, slug }) => {
|
||||||
const params = {};
|
const params = {};
|
||||||
@@ -164,14 +167,15 @@ const DashboardService = {
|
|||||||
return axios.get(`api/dashboards/${id || slug}`, { params }).then(transformResponse);
|
return axios.get(`api/dashboards/${id || slug}`, { params }).then(transformResponse);
|
||||||
},
|
},
|
||||||
getByToken: ({ token }) => axios.get(`api/dashboards/public/${token}`).then(transformResponse),
|
getByToken: ({ token }) => axios.get(`api/dashboards/public/${token}`).then(transformResponse),
|
||||||
save: data => axios.post(saveOrCreateUrl(data), data).then(transformResponse),
|
save: (data) => axios.post(saveOrCreateUrl(data), data).then(transformResponse),
|
||||||
delete: ({ id }) => axios.delete(`api/dashboards/${id}`).then(transformResponse),
|
delete: ({ id }) => axios.delete(`api/dashboards/${id}`).then(transformResponse),
|
||||||
query: params => axios.get("api/dashboards", { params }).then(transformResponse),
|
query: (params) => axios.get("api/dashboards", { params }).then(transformResponse),
|
||||||
recent: params => axios.get("api/dashboards/recent", { params }).then(transformResponse),
|
recent: (params) => axios.get("api/dashboards/recent", { params }).then(transformResponse),
|
||||||
myDashboards: params => axios.get("api/dashboards/my", { params }).then(transformResponse),
|
myDashboards: (params) => axios.get("api/dashboards/my", { params }).then(transformResponse),
|
||||||
favorites: params => axios.get("api/dashboards/favorites", { params }).then(transformResponse),
|
favorites: (params) => axios.get("api/dashboards/favorites", { params }).then(transformResponse),
|
||||||
favorite: ({ id }) => axios.post(`api/dashboards/${id}/favorite`),
|
favorite: ({ id }) => axios.post(`api/dashboards/${id}/favorite`),
|
||||||
unfavorite: ({ id }) => axios.delete(`api/dashboards/${id}/favorite`),
|
unfavorite: ({ id }) => axios.delete(`api/dashboards/${id}/favorite`),
|
||||||
|
fork: ({ id }) => axios.post(`api/dashboards/${id}/fork`, { id }).then(transformResponse),
|
||||||
};
|
};
|
||||||
|
|
||||||
_.extend(Dashboard, DashboardService);
|
_.extend(Dashboard, DashboardService);
|
||||||
@@ -186,13 +190,13 @@ Dashboard.prototype.canEdit = function canEdit() {
|
|||||||
Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
||||||
const globalParams = {};
|
const globalParams = {};
|
||||||
const queryParams = location.search;
|
const queryParams = location.search;
|
||||||
_.each(this.widgets, widget => {
|
_.each(this.widgets, (widget) => {
|
||||||
if (widget.getQuery()) {
|
if (widget.getQuery()) {
|
||||||
const mappings = widget.getParameterMappings();
|
const mappings = widget.getParameterMappings();
|
||||||
widget
|
widget
|
||||||
.getQuery()
|
.getQuery()
|
||||||
.getParametersDefs(false)
|
.getParametersDefs(false)
|
||||||
.forEach(param => {
|
.forEach((param) => {
|
||||||
const mapping = mappings[param.name];
|
const mapping = mappings[param.name];
|
||||||
if (mapping.type === Widget.MappingType.DashboardLevel) {
|
if (mapping.type === Widget.MappingType.DashboardLevel) {
|
||||||
// create global param
|
// create global param
|
||||||
@@ -209,15 +213,19 @@ Dashboard.prototype.getParametersDefs = function getParametersDefs() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const mergedValues = {
|
||||||
|
..._.mapValues(globalParams, (p) => p.value),
|
||||||
|
...Object.fromEntries((this.options.parameters || []).map((param) => [param.name, param.value])),
|
||||||
|
};
|
||||||
const resultingGlobalParams = _.values(
|
const resultingGlobalParams = _.values(
|
||||||
_.each(globalParams, param => {
|
_.each(globalParams, (param) => {
|
||||||
param.setValue(param.value); // apply global param value to all locals
|
param.setValue(mergedValues[param.name]); // apply merged value
|
||||||
param.fromUrlParams(queryParams); // try to initialize from url (may do nothing)
|
param.fromUrlParams(queryParams); // allow param-specific parsing logic
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// order dashboard params using paramOrder
|
// order dashboard params using paramOrder
|
||||||
return _.sortBy(resultingGlobalParams, param =>
|
return _.sortBy(resultingGlobalParams, (param) =>
|
||||||
_.includes(this.options.globalParamOrder, param.name)
|
_.includes(this.options.globalParamOrder, param.name)
|
||||||
? _.indexOf(this.options.globalParamOrder, param.name)
|
? _.indexOf(this.options.globalParamOrder, param.name)
|
||||||
: _.size(this.options.globalParamOrder)
|
: _.size(this.options.globalParamOrder)
|
||||||
@@ -265,3 +273,7 @@ Dashboard.prototype.favorite = function favorite() {
|
|||||||
Dashboard.prototype.unfavorite = function unfavorite() {
|
Dashboard.prototype.unfavorite = function unfavorite() {
|
||||||
return Dashboard.unfavorite(this);
|
return Dashboard.unfavorite(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Dashboard.prototype.getUrl = function getUrl() {
|
||||||
|
return urlForDashboard(this);
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ import { fetchDataFromJob } from "@/services/query-result";
|
|||||||
|
|
||||||
export const SCHEMA_NOT_SUPPORTED = 1;
|
export const SCHEMA_NOT_SUPPORTED = 1;
|
||||||
export const SCHEMA_LOAD_ERROR = 2;
|
export const SCHEMA_LOAD_ERROR = 2;
|
||||||
export const IMG_ROOT = "static/images/db-logos";
|
export const IMG_ROOT = "/static/images/db-logos";
|
||||||
|
|
||||||
function mapSchemaColumnsToObject(columns) {
|
function mapSchemaColumnsToObject(columns) {
|
||||||
return map(columns, column => (isObject(column) ? column : { name: column }));
|
return map(columns, (column) => (isObject(column) ? column : { name: column }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataSource = {
|
const DataSource = {
|
||||||
query: () => axios.get("api/data_sources"),
|
query: () => axios.get("api/data_sources"),
|
||||||
get: ({ id }) => axios.get(`api/data_sources/${id}`),
|
get: ({ id }) => axios.get(`api/data_sources/${id}`),
|
||||||
types: () => axios.get("api/data_sources/types"),
|
types: () => axios.get("api/data_sources/types"),
|
||||||
create: data => axios.post(`api/data_sources`, data),
|
create: (data) => axios.post(`api/data_sources`, data),
|
||||||
save: data => axios.post(`api/data_sources/${data.id}`, data),
|
save: (data) => axios.post(`api/data_sources/${data.id}`, data),
|
||||||
test: data => axios.post(`api/data_sources/${data.id}/test`),
|
test: (data) => axios.post(`api/data_sources/${data.id}/test`),
|
||||||
delete: ({ id }) => axios.delete(`api/data_sources/${id}`),
|
delete: ({ id }) => axios.delete(`api/data_sources/${id}`),
|
||||||
fetchSchema: (data, refresh = false) => {
|
fetchSchema: (data, refresh = false) => {
|
||||||
const params = {};
|
const params = {};
|
||||||
@@ -27,15 +27,15 @@ const DataSource = {
|
|||||||
|
|
||||||
return axios
|
return axios
|
||||||
.get(`api/data_sources/${data.id}/schema`, { params })
|
.get(`api/data_sources/${data.id}/schema`, { params })
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
if (has(data, "job")) {
|
if (has(data, "job")) {
|
||||||
return fetchDataFromJob(data.job.id).catch(error =>
|
return fetchDataFromJob(data.job.id).catch((error) =>
|
||||||
error.code === SCHEMA_NOT_SUPPORTED ? [] : Promise.reject(new Error(data.job.error))
|
error.code === SCHEMA_NOT_SUPPORTED ? [] : Promise.reject(new Error(data.job.error))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return has(data, "schema") ? data.schema : Promise.reject();
|
return has(data, "schema") ? data.schema : Promise.reject();
|
||||||
})
|
})
|
||||||
.then(tables => map(tables, table => ({ ...table, columns: mapSchemaColumnsToObject(table.columns) })));
|
.then((tables) => map(tables, (table) => ({ ...table, columns: mapSchemaColumnsToObject(table.columns) })));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ function normalizeLocation(rawLocation) {
|
|||||||
const result = {};
|
const result = {};
|
||||||
|
|
||||||
result.path = pathname;
|
result.path = pathname;
|
||||||
result.search = mapValues(qs.parse(search), value => (isNil(value) ? true : value));
|
result.search = mapValues(qs.parse(search), (value) => (isNil(value) ? true : value));
|
||||||
result.hash = trimStart(hash, "#");
|
result.hash = trimStart(hash, "#");
|
||||||
result.url = `${pathname}${search}${hash}`;
|
result.url = `${pathname}${search}${hash}`;
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ const location = {
|
|||||||
|
|
||||||
confirmChange(handler) {
|
confirmChange(handler) {
|
||||||
if (isFunction(handler)) {
|
if (isFunction(handler)) {
|
||||||
return history.block(nextLocation => {
|
return history.block((nextLocation) => {
|
||||||
return handler(normalizeLocation(nextLocation), location);
|
return handler(normalizeLocation(nextLocation), location);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -60,12 +60,18 @@ const location = {
|
|||||||
// serialize search and keep existing search parameters (!)
|
// serialize search and keep existing search parameters (!)
|
||||||
if (isObject(newLocation.search)) {
|
if (isObject(newLocation.search)) {
|
||||||
newLocation.search = omitBy(extend({}, location.search, newLocation.search), isNil);
|
newLocation.search = omitBy(extend({}, location.search, newLocation.search), isNil);
|
||||||
newLocation.search = mapValues(newLocation.search, value => (value === true ? null : value));
|
newLocation.search = mapValues(newLocation.search, (value) => (value === true ? null : value));
|
||||||
newLocation.search = qs.stringify(newLocation.search);
|
newLocation.search = qs.stringify(newLocation.search);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (replace) {
|
if (replace) {
|
||||||
history.replace(newLocation);
|
if (
|
||||||
|
newLocation.pathname !== location.path ||
|
||||||
|
newLocation.search !== qs.stringify(location.search) ||
|
||||||
|
newLocation.hash !== location.hash
|
||||||
|
) {
|
||||||
|
history.replace(newLocation);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
history.push(newLocation);
|
history.push(newLocation);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class DateParameter extends Parameter {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedValue = moment(value);
|
const normalizedValue = moment(value, moment.ISO_8601, true);
|
||||||
return normalizedValue.isValid() ? normalizedValue : null;
|
return normalizedValue.isValid() ? normalizedValue : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ const DYNAMIC_PREFIX = "d_";
|
|||||||
* @param now {function(): moment.Moment=} moment - defaults to now
|
* @param now {function(): moment.Moment=} moment - defaults to now
|
||||||
* @returns {function(withNow: boolean): [moment.Moment, moment.Moment|undefined]}
|
* @returns {function(withNow: boolean): [moment.Moment, moment.Moment|undefined]}
|
||||||
*/
|
*/
|
||||||
const untilNow = (from, now = () => moment()) => (withNow = true) => [from(), withNow ? now() : undefined];
|
const untilNow =
|
||||||
|
(from, now = () => moment()) =>
|
||||||
|
(withNow = true) => [from(), withNow ? now() : undefined];
|
||||||
|
|
||||||
const DYNAMIC_DATE_RANGES = {
|
const DYNAMIC_DATE_RANGES = {
|
||||||
today: {
|
today: {
|
||||||
@@ -26,14 +28,7 @@ const DYNAMIC_DATE_RANGES = {
|
|||||||
},
|
},
|
||||||
yesterday: {
|
yesterday: {
|
||||||
name: "Yesterday",
|
name: "Yesterday",
|
||||||
value: () => [
|
value: () => [moment().subtract(1, "day").startOf("day"), moment().subtract(1, "day").endOf("day")],
|
||||||
moment()
|
|
||||||
.subtract(1, "day")
|
|
||||||
.startOf("day"),
|
|
||||||
moment()
|
|
||||||
.subtract(1, "day")
|
|
||||||
.endOf("day"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
this_week: {
|
this_week: {
|
||||||
name: "This week",
|
name: "This week",
|
||||||
@@ -49,36 +44,15 @@ const DYNAMIC_DATE_RANGES = {
|
|||||||
},
|
},
|
||||||
last_week: {
|
last_week: {
|
||||||
name: "Last week",
|
name: "Last week",
|
||||||
value: () => [
|
value: () => [moment().subtract(1, "week").startOf("week"), moment().subtract(1, "week").endOf("week")],
|
||||||
moment()
|
|
||||||
.subtract(1, "week")
|
|
||||||
.startOf("week"),
|
|
||||||
moment()
|
|
||||||
.subtract(1, "week")
|
|
||||||
.endOf("week"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
last_month: {
|
last_month: {
|
||||||
name: "Last month",
|
name: "Last month",
|
||||||
value: () => [
|
value: () => [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")],
|
||||||
moment()
|
|
||||||
.subtract(1, "month")
|
|
||||||
.startOf("month"),
|
|
||||||
moment()
|
|
||||||
.subtract(1, "month")
|
|
||||||
.endOf("month"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
last_year: {
|
last_year: {
|
||||||
name: "Last year",
|
name: "Last year",
|
||||||
value: () => [
|
value: () => [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")],
|
||||||
moment()
|
|
||||||
.subtract(1, "year")
|
|
||||||
.startOf("year"),
|
|
||||||
moment()
|
|
||||||
.subtract(1, "year")
|
|
||||||
.endOf("year"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
last_hour: {
|
last_hour: {
|
||||||
name: "Last hour",
|
name: "Last hour",
|
||||||
@@ -94,63 +68,31 @@ const DYNAMIC_DATE_RANGES = {
|
|||||||
},
|
},
|
||||||
last_7_days: {
|
last_7_days: {
|
||||||
name: "Last 7 days",
|
name: "Last 7 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(7, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(7, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_14_days: {
|
last_14_days: {
|
||||||
name: "Last 14 days",
|
name: "Last 14 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(14, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(14, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_30_days: {
|
last_30_days: {
|
||||||
name: "Last 30 days",
|
name: "Last 30 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(30, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(30, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_60_days: {
|
last_60_days: {
|
||||||
name: "Last 60 days",
|
name: "Last 60 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(60, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(60, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_90_days: {
|
last_90_days: {
|
||||||
name: "Last 90 days",
|
name: "Last 90 days",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(90, "days").startOf("day")),
|
||||||
() =>
|
|
||||||
moment()
|
|
||||||
.subtract(90, "days")
|
|
||||||
.startOf("day"),
|
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
last_12_months: {
|
last_12_months: {
|
||||||
name: "Last 12 months",
|
name: "Last 12 months",
|
||||||
value: untilNow(
|
value: untilNow(() => moment().subtract(12, "months").startOf("day")),
|
||||||
() =>
|
},
|
||||||
moment()
|
last_10_years: {
|
||||||
.subtract(12, "months")
|
name: "Last 10 years",
|
||||||
.startOf("day"),
|
value: untilNow(() => moment().subtract(10, "years").startOf("day")),
|
||||||
() => moment().endOf("day")
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +106,7 @@ export function isDynamicDateRangeString(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDynamicDateRangeStringFromName(dynamicRangeName) {
|
export function getDynamicDateRangeStringFromName(dynamicRangeName) {
|
||||||
const key = findKey(DYNAMIC_DATE_RANGES, range => range.name === dynamicRangeName);
|
const key = findKey(DYNAMIC_DATE_RANGES, (range) => range.name === dynamicRangeName);
|
||||||
return key ? DYNAMIC_PREFIX + key : undefined;
|
return key ? DYNAMIC_PREFIX + key : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +175,7 @@ class DateRangeParameter extends Parameter {
|
|||||||
|
|
||||||
getExecutionValue() {
|
getExecutionValue() {
|
||||||
if (this.hasDynamicValue) {
|
if (this.hasDynamicValue) {
|
||||||
const format = date => date.format(DATETIME_FORMATS[this.type]);
|
const format = (date) => date.format(DATETIME_FORMATS[this.type]);
|
||||||
const [start, end] = this.normalizedValue.value().map(format);
|
const [start, end] = this.normalizedValue.value().map(format);
|
||||||
return { start, end };
|
return { start, end };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class Parameter {
|
|||||||
|
|
||||||
updateLocals() {
|
updateLocals() {
|
||||||
if (isArray(this.locals)) {
|
if (isArray(this.locals)) {
|
||||||
each(this.locals, local => {
|
each(this.locals, (local) => {
|
||||||
local.setValue(this.value);
|
local.setValue(this.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ class Parameter {
|
|||||||
|
|
||||||
/** Get a saveable version of the Parameter by omitting unnecessary props */
|
/** Get a saveable version of the Parameter by omitting unnecessary props */
|
||||||
toSaveableObject() {
|
toSaveableObject() {
|
||||||
return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId"]);
|
return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId", "locals"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
client/app/services/parameters/TextPatternParameter.js
Normal file
29
client/app/services/parameters/TextPatternParameter.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { toString, isNull } from "lodash";
|
||||||
|
import Parameter from "./Parameter";
|
||||||
|
|
||||||
|
class TextPatternParameter extends Parameter {
|
||||||
|
constructor(parameter, parentQueryId) {
|
||||||
|
super(parameter, parentQueryId);
|
||||||
|
this.regex = parameter.regex;
|
||||||
|
this.setValue(parameter.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
normalizeValue(value) {
|
||||||
|
const normalizedValue = toString(value);
|
||||||
|
if (isNull(normalizedValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var re = new RegExp(this.regex);
|
||||||
|
|
||||||
|
if (re !== null) {
|
||||||
|
if (re.test(normalizedValue)) {
|
||||||
|
return normalizedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextPatternParameter;
|
||||||
@@ -5,6 +5,7 @@ import EnumParameter from "./EnumParameter";
|
|||||||
import QueryBasedDropdownParameter from "./QueryBasedDropdownParameter";
|
import QueryBasedDropdownParameter from "./QueryBasedDropdownParameter";
|
||||||
import DateParameter from "./DateParameter";
|
import DateParameter from "./DateParameter";
|
||||||
import DateRangeParameter from "./DateRangeParameter";
|
import DateRangeParameter from "./DateRangeParameter";
|
||||||
|
import TextPatternParameter from "./TextPatternParameter";
|
||||||
|
|
||||||
function createParameter(param, parentQueryId) {
|
function createParameter(param, parentQueryId) {
|
||||||
switch (param.type) {
|
switch (param.type) {
|
||||||
@@ -22,6 +23,8 @@ function createParameter(param, parentQueryId) {
|
|||||||
case "datetime-range":
|
case "datetime-range":
|
||||||
case "datetime-range-with-seconds":
|
case "datetime-range-with-seconds":
|
||||||
return new DateRangeParameter(param, parentQueryId);
|
return new DateRangeParameter(param, parentQueryId);
|
||||||
|
case "text-pattern":
|
||||||
|
return new TextPatternParameter({ ...param, type: "text-pattern" }, parentQueryId);
|
||||||
default:
|
default:
|
||||||
return new TextParameter({ ...param, type: "text" }, parentQueryId);
|
return new TextParameter({ ...param, type: "text" }, parentQueryId);
|
||||||
}
|
}
|
||||||
@@ -34,6 +37,7 @@ function cloneParameter(param) {
|
|||||||
export {
|
export {
|
||||||
Parameter,
|
Parameter,
|
||||||
TextParameter,
|
TextParameter,
|
||||||
|
TextPatternParameter,
|
||||||
NumberParameter,
|
NumberParameter,
|
||||||
EnumParameter,
|
EnumParameter,
|
||||||
QueryBasedDropdownParameter,
|
QueryBasedDropdownParameter,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
createParameter,
|
createParameter,
|
||||||
TextParameter,
|
TextParameter,
|
||||||
|
TextPatternParameter,
|
||||||
NumberParameter,
|
NumberParameter,
|
||||||
EnumParameter,
|
EnumParameter,
|
||||||
QueryBasedDropdownParameter,
|
QueryBasedDropdownParameter,
|
||||||
@@ -12,6 +13,7 @@ describe("Parameter", () => {
|
|||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
const parameterTypes = [
|
const parameterTypes = [
|
||||||
["text", TextParameter],
|
["text", TextParameter],
|
||||||
|
["text-pattern", TextPatternParameter],
|
||||||
["number", NumberParameter],
|
["number", NumberParameter],
|
||||||
["enum", EnumParameter],
|
["enum", EnumParameter],
|
||||||
["query", QueryBasedDropdownParameter],
|
["query", QueryBasedDropdownParameter],
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { createParameter } from "..";
|
||||||
|
|
||||||
|
describe("TextPatternParameter", () => {
|
||||||
|
let param;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
param = createParameter({ name: "param", title: "Param", type: "text-pattern", regex: "a+" });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("noramlizeValue", () => {
|
||||||
|
test("converts matching strings", () => {
|
||||||
|
const normalizedValue = param.normalizeValue("art");
|
||||||
|
expect(normalizedValue).toBe("art");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null when string does not match pattern", () => {
|
||||||
|
const normalizedValue = param.normalizeValue("brt");
|
||||||
|
expect(normalizedValue).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,7 +9,7 @@ const logger = debug("redash:services:QueryResult");
|
|||||||
const filterTypes = ["filter", "multi-filter", "multiFilter"];
|
const filterTypes = ["filter", "multi-filter", "multiFilter"];
|
||||||
|
|
||||||
function defer() {
|
function defer() {
|
||||||
const result = { onStatusChange: status => {} };
|
const result = { onStatusChange: (status) => {} };
|
||||||
result.promise = new Promise((resolve, reject) => {
|
result.promise = new Promise((resolve, reject) => {
|
||||||
result.resolve = resolve;
|
result.resolve = resolve;
|
||||||
result.reject = reject;
|
result.reject = reject;
|
||||||
@@ -40,13 +40,13 @@ function getColumnNameWithoutType(column) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getColumnFriendlyName(column) {
|
function getColumnFriendlyName(column) {
|
||||||
return getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, a => a.toUpperCase());
|
return getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, (a) => a.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
const createOrSaveUrl = data => (data.id ? `api/query_results/${data.id}` : "api/query_results");
|
const createOrSaveUrl = (data) => (data.id ? `api/query_results/${data.id}` : "api/query_results");
|
||||||
const QueryResultResource = {
|
const QueryResultResource = {
|
||||||
get: ({ id }) => axios.get(`api/query_results/${id}`),
|
get: ({ id }) => axios.get(`api/query_results/${id}`),
|
||||||
post: data => axios.post(createOrSaveUrl(data), data),
|
post: (data) => axios.post(createOrSaveUrl(data), data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExecutionStatus = {
|
export const ExecutionStatus = {
|
||||||
@@ -97,11 +97,11 @@ function handleErrorResponse(queryResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchDataFromJob(jobId, interval = 1000) {
|
export function fetchDataFromJob(jobId, interval = 1000) {
|
||||||
return axios.get(`api/jobs/${jobId}`).then(data => {
|
return axios.get(`api/jobs/${jobId}`).then((data) => {
|
||||||
const status = statuses[data.job.status];
|
const status = statuses[data.job.status];
|
||||||
if (status === ExecutionStatus.WAITING || status === ExecutionStatus.PROCESSING) {
|
if (status === ExecutionStatus.WAITING || status === ExecutionStatus.PROCESSING) {
|
||||||
return sleep(interval).then(() => fetchDataFromJob(data.job.id));
|
return sleep(interval).then(() => fetchDataFromJob(data.job.id));
|
||||||
@@ -114,7 +114,7 @@ export function fetchDataFromJob(jobId, interval = 1000) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isDateTime(v) {
|
export function isDateTime(v) {
|
||||||
return isString(v) && moment(v).isValid() && /^\d{4}-\d{2}-\d{2}T/.test(v);
|
return isString(v) && moment(v, moment.ISO_8601, true).isValid() && /^\d{4}-\d{2}-\d{2}T/.test(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
class QueryResult {
|
class QueryResult {
|
||||||
@@ -146,7 +146,7 @@ class QueryResult {
|
|||||||
// TODO: we should stop manipulating incoming data, and switch to relaying
|
// TODO: we should stop manipulating incoming data, and switch to relaying
|
||||||
// on the column type set by the backend. This logic is prone to errors,
|
// on the column type set by the backend. This logic is prone to errors,
|
||||||
// and better be removed. Kept for now, for backward compatability.
|
// and better be removed. Kept for now, for backward compatability.
|
||||||
each(this.query_result.data.rows, row => {
|
each(this.query_result.data.rows, (row) => {
|
||||||
forOwn(row, (v, k) => {
|
forOwn(row, (v, k) => {
|
||||||
let newType = null;
|
let newType = null;
|
||||||
if (isNumber(v)) {
|
if (isNumber(v)) {
|
||||||
@@ -173,7 +173,7 @@ class QueryResult {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
each(this.query_result.data.columns, column => {
|
each(this.query_result.data.columns, (column) => {
|
||||||
column.name = "" + column.name;
|
column.name = "" + column.name;
|
||||||
if (columnTypes[column.name]) {
|
if (columnTypes[column.name]) {
|
||||||
if (column.type == null || column.type === "string") {
|
if (column.type == null || column.type === "string") {
|
||||||
@@ -265,14 +265,14 @@ class QueryResult {
|
|||||||
|
|
||||||
getColumnNames() {
|
getColumnNames() {
|
||||||
if (this.columnNames === undefined && this.query_result.data) {
|
if (this.columnNames === undefined && this.query_result.data) {
|
||||||
this.columnNames = this.query_result.data.columns.map(v => v.name);
|
this.columnNames = this.query_result.data.columns.map((v) => v.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.columnNames;
|
return this.columnNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
getColumnFriendlyNames() {
|
getColumnFriendlyNames() {
|
||||||
return this.getColumnNames().map(col => getColumnFriendlyName(col));
|
return this.getColumnNames().map((col) => getColumnFriendlyName(col));
|
||||||
}
|
}
|
||||||
|
|
||||||
getTruncated() {
|
getTruncated() {
|
||||||
@@ -286,7 +286,7 @@ class QueryResult {
|
|||||||
|
|
||||||
const filters = [];
|
const filters = [];
|
||||||
|
|
||||||
this.getColumns().forEach(col => {
|
this.getColumns().forEach((col) => {
|
||||||
const name = col.name;
|
const name = col.name;
|
||||||
const type = name.split("::")[1] || name.split("__")[1];
|
const type = name.split("::")[1] || name.split("__")[1];
|
||||||
if (includes(filterTypes, type)) {
|
if (includes(filterTypes, type)) {
|
||||||
@@ -302,8 +302,8 @@ class QueryResult {
|
|||||||
}
|
}
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
this.getRawData().forEach(row => {
|
this.getRawData().forEach((row) => {
|
||||||
filters.forEach(filter => {
|
filters.forEach((filter) => {
|
||||||
filter.values.push(row[filter.name]);
|
filter.values.push(row[filter.name]);
|
||||||
if (filter.values.length === 1) {
|
if (filter.values.length === 1) {
|
||||||
if (filter.multiple) {
|
if (filter.multiple) {
|
||||||
@@ -315,8 +315,8 @@ class QueryResult {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
filters.forEach(filter => {
|
filters.forEach((filter) => {
|
||||||
filter.values = uniqBy(filter.values, v => {
|
filter.values = uniqBy(filter.values, (v) => {
|
||||||
if (moment.isMoment(v)) {
|
if (moment.isMoment(v)) {
|
||||||
return v.unix();
|
return v.unix();
|
||||||
}
|
}
|
||||||
@@ -345,12 +345,12 @@ class QueryResult {
|
|||||||
|
|
||||||
axios
|
axios
|
||||||
.get(`api/queries/${queryId}/results/${id}.json`)
|
.get(`api/queries/${queryId}/results/${id}.json`)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
// Success handler
|
// Success handler
|
||||||
queryResult.isLoadingResult = false;
|
queryResult.isLoadingResult = false;
|
||||||
queryResult.update(response);
|
queryResult.update(response);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
// Error handler
|
// Error handler
|
||||||
queryResult.isLoadingResult = false;
|
queryResult.isLoadingResult = false;
|
||||||
handleErrorResponse(queryResult, error);
|
handleErrorResponse(queryResult, error);
|
||||||
@@ -362,10 +362,10 @@ class QueryResult {
|
|||||||
loadLatestCachedResult(queryId, parameters) {
|
loadLatestCachedResult(queryId, parameters) {
|
||||||
axios
|
axios
|
||||||
.post(`api/queries/${queryId}/results`, { queryId, parameters })
|
.post(`api/queries/${queryId}/results`, { queryId, parameters })
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
this.update(response);
|
this.update(response);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
handleErrorResponse(this, error);
|
handleErrorResponse(this, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -375,11 +375,11 @@ class QueryResult {
|
|||||||
this.deferred.onStatusChange(ExecutionStatus.LOADING_RESULT);
|
this.deferred.onStatusChange(ExecutionStatus.LOADING_RESULT);
|
||||||
|
|
||||||
QueryResultResource.get({ id: this.job.query_result_id })
|
QueryResultResource.get({ id: this.job.query_result_id })
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
this.update(response);
|
this.update(response);
|
||||||
this.isLoadingResult = false;
|
this.isLoadingResult = false;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
if (tryCount === undefined) {
|
if (tryCount === undefined) {
|
||||||
tryCount = 0;
|
tryCount = 0;
|
||||||
}
|
}
|
||||||
@@ -394,9 +394,12 @@ class QueryResult {
|
|||||||
});
|
});
|
||||||
this.isLoadingResult = false;
|
this.isLoadingResult = false;
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
this.loadResult(tryCount + 1);
|
() => {
|
||||||
}, 1000 * Math.pow(2, tryCount));
|
this.loadResult(tryCount + 1);
|
||||||
|
},
|
||||||
|
1000 * Math.pow(2, tryCount)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -410,19 +413,26 @@ class QueryResult {
|
|||||||
: axios.get(`api/queries/${query}/jobs/${this.job.id}`);
|
: axios.get(`api/queries/${query}/jobs/${this.job.id}`);
|
||||||
|
|
||||||
request
|
request
|
||||||
.then(jobResponse => {
|
.then((jobResponse) => {
|
||||||
this.update(jobResponse);
|
this.update(jobResponse);
|
||||||
|
|
||||||
if (this.getStatus() === "processing" && this.job.query_result_id && this.job.query_result_id !== "None") {
|
if (this.getStatus() === "processing" && this.job.query_result_id && this.job.query_result_id !== "None") {
|
||||||
loadResult();
|
loadResult();
|
||||||
} else if (this.getStatus() !== "failed") {
|
} else if (this.getStatus() !== "failed") {
|
||||||
const waitTime = tryNumber > 10 ? 3000 : 500;
|
let waitTime;
|
||||||
|
if (tryNumber <= 10) {
|
||||||
|
waitTime = 500;
|
||||||
|
} else if (tryNumber <= 50) {
|
||||||
|
waitTime = 1000;
|
||||||
|
} else {
|
||||||
|
waitTime = 3000;
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refreshStatus(query, parameters, tryNumber + 1);
|
this.refreshStatus(query, parameters, tryNumber + 1);
|
||||||
}, waitTime);
|
}, waitTime);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
logger("Connection error", error);
|
logger("Connection error", error);
|
||||||
// TODO: use QueryResultError, or better yet: exception/reject of promise.
|
// TODO: use QueryResultError, or better yet: exception/reject of promise.
|
||||||
this.update({
|
this.update({
|
||||||
@@ -451,14 +461,14 @@ class QueryResult {
|
|||||||
|
|
||||||
axios
|
axios
|
||||||
.post(`api/queries/${id}/results`, { id, parameters, apply_auto_limit: applyAutoLimit, max_age: maxAge })
|
.post(`api/queries/${id}/results`, { id, parameters, apply_auto_limit: applyAutoLimit, max_age: maxAge })
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
queryResult.update(response);
|
queryResult.update(response);
|
||||||
|
|
||||||
if ("job" in response) {
|
if ("job" in response) {
|
||||||
queryResult.refreshStatus(id, parameters);
|
queryResult.refreshStatus(id, parameters);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
handleErrorResponse(queryResult, error);
|
handleErrorResponse(queryResult, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -481,14 +491,14 @@ class QueryResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QueryResultResource.post(params)
|
QueryResultResource.post(params)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
queryResult.update(response);
|
queryResult.update(response);
|
||||||
|
|
||||||
if ("job" in response) {
|
if ("job" in response) {
|
||||||
queryResult.refreshStatus(query, parameters);
|
queryResult.refreshStatus(query, parameters);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
handleErrorResponse(queryResult, error);
|
handleErrorResponse(queryResult, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable import/no-extraneous-dependencies, no-console */
|
/* eslint-disable import/no-extraneous-dependencies, no-console */
|
||||||
const { find } = require("lodash");
|
const { find } = require("lodash");
|
||||||
const atob = require("atob");
|
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
const { get, post } = require("request").defaults({ jar: true });
|
const { get, post } = require("request").defaults({ jar: true });
|
||||||
const { seedData } = require("./seed-data");
|
const { seedData } = require("./seed-data");
|
||||||
@@ -60,23 +59,11 @@ function stopServer() {
|
|||||||
|
|
||||||
function runCypressCI() {
|
function runCypressCI() {
|
||||||
const {
|
const {
|
||||||
PERCY_TOKEN_ENCODED,
|
|
||||||
CYPRESS_PROJECT_ID_ENCODED,
|
|
||||||
CYPRESS_RECORD_KEY_ENCODED,
|
|
||||||
GITHUB_REPOSITORY,
|
GITHUB_REPOSITORY,
|
||||||
CYPRESS_OPTIONS, // eslint-disable-line no-unused-vars
|
CYPRESS_OPTIONS, // eslint-disable-line no-unused-vars
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
if (GITHUB_REPOSITORY === "getredash/redash") {
|
if (GITHUB_REPOSITORY === "getredash/redash" && process.env.CYPRESS_RECORD_KEY) {
|
||||||
if (PERCY_TOKEN_ENCODED) {
|
|
||||||
process.env.PERCY_TOKEN = atob(`${PERCY_TOKEN_ENCODED}`);
|
|
||||||
}
|
|
||||||
if (CYPRESS_PROJECT_ID_ENCODED) {
|
|
||||||
process.env.CYPRESS_PROJECT_ID = atob(`${CYPRESS_PROJECT_ID_ENCODED}`);
|
|
||||||
}
|
|
||||||
if (CYPRESS_RECORD_KEY_ENCODED) {
|
|
||||||
process.env.CYPRESS_RECORD_KEY = atob(`${CYPRESS_RECORD_KEY_ENCODED}`);
|
|
||||||
}
|
|
||||||
process.env.CYPRESS_OPTIONS = "--record";
|
process.env.CYPRESS_OPTIONS = "--record";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe("Dashboard", () => {
|
|||||||
cy.getByTestId("DashboardSaveButton").click();
|
cy.getByTestId("DashboardSaveButton").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.wait("@NewDashboard").then(xhr => {
|
cy.wait("@NewDashboard").then((xhr) => {
|
||||||
const id = Cypress._.get(xhr, "response.body.id");
|
const id = Cypress._.get(xhr, "response.body.id");
|
||||||
assert.isDefined(id, "Dashboard api call returns id");
|
assert.isDefined(id, "Dashboard api call returns id");
|
||||||
|
|
||||||
@@ -40,13 +40,9 @@ describe("Dashboard", () => {
|
|||||||
|
|
||||||
cy.getByTestId("DashboardMoreButton").click();
|
cy.getByTestId("DashboardMoreButton").click();
|
||||||
|
|
||||||
cy.getByTestId("DashboardMoreButtonMenu")
|
cy.getByTestId("DashboardMoreButtonMenu").contains("Archive").click();
|
||||||
.contains("Archive")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".ant-modal .ant-btn")
|
cy.get(".ant-modal .ant-btn").contains("Archive").click({ force: true });
|
||||||
.contains("Archive")
|
|
||||||
.click({ force: true });
|
|
||||||
cy.get(".label-tag-archived").should("exist");
|
cy.get(".label-tag-archived").should("exist");
|
||||||
|
|
||||||
cy.visit("/dashboards");
|
cy.visit("/dashboards");
|
||||||
@@ -60,7 +56,7 @@ describe("Dashboard", () => {
|
|||||||
cy.server();
|
cy.server();
|
||||||
cy.route("GET", "**/api/dashboards/*").as("LoadDashboard");
|
cy.route("GET", "**/api/dashboards/*").as("LoadDashboard");
|
||||||
cy.createDashboard("Dashboard multiple urls").then(({ id, slug }) => {
|
cy.createDashboard("Dashboard multiple urls").then(({ id, slug }) => {
|
||||||
[`/dashboards/${id}`, `/dashboards/${id}-anything-here`, `/dashboard/${slug}`].forEach(url => {
|
[`/dashboards/${id}`, `/dashboards/${id}-anything-here`, `/dashboard/${slug}`].forEach((url) => {
|
||||||
cy.visit(url);
|
cy.visit(url);
|
||||||
cy.wait("@LoadDashboard");
|
cy.wait("@LoadDashboard");
|
||||||
cy.getByTestId(`DashboardId${id}Container`).should("exist");
|
cy.getByTestId(`DashboardId${id}Container`).should("exist");
|
||||||
@@ -72,7 +68,7 @@ describe("Dashboard", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
context("viewport width is at 800px", () => {
|
context("viewport width is at 800px", () => {
|
||||||
before(function() {
|
before(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createDashboard("Foo Bar")
|
cy.createDashboard("Foo Bar")
|
||||||
.then(({ id }) => {
|
.then(({ id }) => {
|
||||||
@@ -80,49 +76,42 @@ describe("Dashboard", () => {
|
|||||||
this.dashboardEditUrl = `/dashboards/${id}?edit`;
|
this.dashboardEditUrl = `/dashboards/${id}?edit`;
|
||||||
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
||||||
})
|
})
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId).as("textboxEl");
|
cy.getByTestId(elTestId).as("textboxEl");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.viewport(800 + menuWidth, 800);
|
cy.viewport(800 + menuWidth, 800);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows widgets with full width", () => {
|
it("shows widgets with full width", () => {
|
||||||
cy.get("@textboxEl").should($el => {
|
cy.get("@textboxEl").should(($el) => {
|
||||||
expect($el.width()).to.eq(770);
|
expect($el.width()).to.eq(770);
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.viewport(801 + menuWidth, 800);
|
cy.viewport(801 + menuWidth, 800);
|
||||||
cy.get("@textboxEl").should($el => {
|
cy.get("@textboxEl").should(($el) => {
|
||||||
expect($el.width()).to.eq(378);
|
expect($el.width()).to.eq(182);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides edit option", () => {
|
it("hides edit option", () => {
|
||||||
cy.getByTestId("DashboardMoreButton")
|
cy.getByTestId("DashboardMoreButton").click().should("be.visible");
|
||||||
.click()
|
|
||||||
.should("be.visible");
|
|
||||||
|
|
||||||
cy.getByTestId("DashboardMoreButtonMenu")
|
cy.getByTestId("DashboardMoreButtonMenu").contains("Edit").as("editButton").should("not.be.visible");
|
||||||
.contains("Edit")
|
|
||||||
.as("editButton")
|
|
||||||
.should("not.be.visible");
|
|
||||||
|
|
||||||
cy.viewport(801 + menuWidth, 800);
|
cy.viewport(801 + menuWidth, 800);
|
||||||
cy.get("@editButton").should("be.visible");
|
cy.get("@editButton").should("be.visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables edit mode", function() {
|
it("disables edit mode", function () {
|
||||||
cy.viewport(801 + menuWidth, 800);
|
cy.viewport(801 + menuWidth, 800);
|
||||||
cy.visit(this.dashboardEditUrl);
|
cy.visit(this.dashboardEditUrl);
|
||||||
cy.contains("button", "Done Editing")
|
cy.contains("button", "Done Editing").as("saveButton").should("exist");
|
||||||
.as("saveButton")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.viewport(800 + menuWidth, 800);
|
cy.viewport(800 + menuWidth, 800);
|
||||||
cy.contains("button", "Done Editing").should("not.exist");
|
cy.contains("button", "Done Editing").should("not.exist");
|
||||||
@@ -130,14 +119,14 @@ describe("Dashboard", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
context("viewport width is at 767px", () => {
|
context("viewport width is at 767px", () => {
|
||||||
before(function() {
|
before(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createDashboard("Foo Bar").then(({ id }) => {
|
cy.createDashboard("Foo Bar").then(({ id }) => {
|
||||||
this.dashboardUrl = `/dashboards/${id}`;
|
this.dashboardUrl = `/dashboards/${id}`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.viewport(767, 800);
|
cy.viewport(767, 800);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe("Dashboard Filters", () => {
|
|||||||
name: "Query Filters",
|
name: "Query Filters",
|
||||||
query: `SELECT stage1 AS "stage1::filter", stage2, value FROM (${SQL}) q`,
|
query: `SELECT stage1 AS "stage1::filter", stage2, value FROM (${SQL}) q`,
|
||||||
};
|
};
|
||||||
cy.createDashboard("Dashboard Filters").then(dashboard => {
|
cy.createDashboard("Dashboard Filters").then((dashboard) => {
|
||||||
createQueryAndAddWidget(dashboard.id, queryData)
|
createQueryAndAddWidget(dashboard.id, queryData)
|
||||||
.as("widget1TestId")
|
.as("widget1TestId")
|
||||||
.then(() => createQueryAndAddWidget(dashboard.id, queryData, { position: { col: 4 } }))
|
.then(() => createQueryAndAddWidget(dashboard.id, queryData, { position: { col: 4 } }))
|
||||||
@@ -32,26 +32,23 @@ describe("Dashboard Filters", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters rows in a Table Visualization", function() {
|
it("filters rows in a Table Visualization", function () {
|
||||||
editDashboard();
|
editDashboard();
|
||||||
cy.getByTestId("DashboardFilters").should("not.exist");
|
cy.getByTestId("DashboardFilters").should("not.exist");
|
||||||
cy.getByTestId("DashboardFiltersCheckbox").click();
|
cy.getByTestId("DashboardFiltersCheckbox").click();
|
||||||
|
|
||||||
cy.getByTestId("DashboardFilters").within(() => {
|
cy.getByTestId("DashboardFilters").within(() => {
|
||||||
cy.getByTestId("FilterName-stage1::filter")
|
cy.getByTestId("FilterName-stage1::filter").find(".ant-select-selection-item").should("have.text", "a");
|
||||||
.find(".ant-select-selection-item")
|
|
||||||
.should("have.text", "a");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId(this.widget1TestId).within(() => {
|
cy.getByTestId(this.widget1TestId).within(() => {
|
||||||
expectTableToHaveLength(4);
|
expectTableToHaveLength(4);
|
||||||
expectFirstColumnToHaveMembers(["a", "a", "a", "a"]);
|
expectFirstColumnToHaveMembers(["a", "a", "a", "a"]);
|
||||||
|
|
||||||
cy.getByTestId("FilterName-stage1::filter")
|
cy.getByTestId("FilterName-stage1::filter").find(".ant-select").click();
|
||||||
.find(".ant-select")
|
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.contains(".ant-select-item-option-content:visible", "b").click();
|
cy.contains(".ant-select-item-option-content:visible", "b").click();
|
||||||
|
|
||||||
cy.getByTestId(this.widget1TestId).within(() => {
|
cy.getByTestId(this.widget1TestId).within(() => {
|
||||||
@@ -69,14 +66,13 @@ describe("Dashboard Filters", () => {
|
|||||||
// assert that changing a global filter affects all widgets
|
// assert that changing a global filter affects all widgets
|
||||||
|
|
||||||
cy.getByTestId("DashboardFilters").within(() => {
|
cy.getByTestId("DashboardFilters").within(() => {
|
||||||
cy.getByTestId("FilterName-stage1::filter")
|
cy.getByTestId("FilterName-stage1::filter").find(".ant-select").click();
|
||||||
.find(".ant-select")
|
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.contains(".ant-select-item-option-content:visible", "c").click();
|
cy.contains(".ant-select-item-option-content:visible", "c").click();
|
||||||
|
|
||||||
[this.widget1TestId, this.widget2TestId].forEach(widgetTestId =>
|
[this.widget1TestId, this.widget2TestId].forEach((widgetTestId) =>
|
||||||
cy.getByTestId(widgetTestId).within(() => {
|
cy.getByTestId(widgetTestId).within(() => {
|
||||||
expectTableToHaveLength(4);
|
expectTableToHaveLength(4);
|
||||||
expectFirstColumnToHaveMembers(["c", "c", "c", "c"]);
|
expectFirstColumnToHaveMembers(["c", "c", "c", "c"]);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getWidgetTestId, editDashboard, resizeBy } from "../../support/dashboar
|
|||||||
const menuWidth = 80;
|
const menuWidth = 80;
|
||||||
|
|
||||||
describe("Grid compliant widgets", () => {
|
describe("Grid compliant widgets", () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.viewport(1215 + menuWidth, 800);
|
cy.viewport(1215 + menuWidth, 800);
|
||||||
cy.createDashboard("Foo Bar")
|
cy.createDashboard("Foo Bar")
|
||||||
@@ -13,7 +13,7 @@ describe("Grid compliant widgets", () => {
|
|||||||
this.dashboardUrl = `/dashboards/${id}`;
|
this.dashboardUrl = `/dashboards/${id}`;
|
||||||
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
||||||
})
|
})
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId).as("textboxEl");
|
cy.getByTestId(elTestId).as("textboxEl");
|
||||||
});
|
});
|
||||||
@@ -27,7 +27,7 @@ describe("Grid compliant widgets", () => {
|
|||||||
|
|
||||||
it("stays put when dragged under snap threshold", () => {
|
it("stays put when dragged under snap threshold", () => {
|
||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.dragBy(90)
|
.dragBy(30)
|
||||||
.invoke("offset")
|
.invoke("offset")
|
||||||
.should("have.property", "left", 15 + menuWidth); // no change, 15 -> 15
|
.should("have.property", "left", 15 + menuWidth); // no change, 15 -> 15
|
||||||
});
|
});
|
||||||
@@ -36,14 +36,14 @@ describe("Grid compliant widgets", () => {
|
|||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.dragBy(110)
|
.dragBy(110)
|
||||||
.invoke("offset")
|
.invoke("offset")
|
||||||
.should("have.property", "left", 215 + menuWidth); // moved by 200, 15 -> 215
|
.should("have.property", "left", 115 + menuWidth); // moved by 100, 15 -> 115
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves two columns when dragged over snap threshold", () => {
|
it("moves two columns when dragged over snap threshold", () => {
|
||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.dragBy(330)
|
.dragBy(200)
|
||||||
.invoke("offset")
|
.invoke("offset")
|
||||||
.should("have.property", "left", 415 + menuWidth); // moved by 400, 15 -> 415
|
.should("have.property", "left", 215 + menuWidth); // moved by 200, 15 -> 215
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ describe("Grid compliant widgets", () => {
|
|||||||
cy.route("POST", "**/api/widgets/*").as("WidgetSave");
|
cy.route("POST", "**/api/widgets/*").as("WidgetSave");
|
||||||
|
|
||||||
editDashboard();
|
editDashboard();
|
||||||
cy.get("@textboxEl").dragBy(330);
|
cy.get("@textboxEl").dragBy(100);
|
||||||
cy.wait("@WidgetSave");
|
cy.wait("@WidgetSave");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -64,24 +64,24 @@ describe("Grid compliant widgets", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("stays put when dragged under snap threshold", () => {
|
it("stays put when dragged under snap threshold", () => {
|
||||||
resizeBy(cy.get("@textboxEl"), 90)
|
resizeBy(cy.get("@textboxEl"), 30)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("width")
|
.invoke("width")
|
||||||
.should("eq", 585); // no change, 585 -> 585
|
.should("eq", 285); // no change, 285 -> 285
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves one column when dragged over snap threshold", () => {
|
it("moves one column when dragged over snap threshold", () => {
|
||||||
resizeBy(cy.get("@textboxEl"), 110)
|
resizeBy(cy.get("@textboxEl"), 110)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("width")
|
.invoke("width")
|
||||||
.should("eq", 785); // resized by 200, 585 -> 785
|
.should("eq", 385); // resized by 200, 185 -> 385
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves two columns when dragged over snap threshold", () => {
|
it("moves two columns when dragged over snap threshold", () => {
|
||||||
resizeBy(cy.get("@textboxEl"), 400)
|
resizeBy(cy.get("@textboxEl"), 400)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("width")
|
.invoke("width")
|
||||||
.should("eq", 985); // resized by 400, 585 -> 985
|
.should("eq", 685); // resized by 400, 285 -> 685
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,16 +101,16 @@ describe("Grid compliant widgets", () => {
|
|||||||
resizeBy(cy.get("@textboxEl"), 0, 30)
|
resizeBy(cy.get("@textboxEl"), 0, 30)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("height")
|
.invoke("height")
|
||||||
.should("eq", 185); // resized by 50, , 135 -> 185
|
.should("eq", 185);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shrinks to minimum", () => {
|
it("shrinks to minimum", () => {
|
||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.then($el => resizeBy(cy.get("@textboxEl"), -$el.width(), -$el.height())) // resize to 0,0
|
.then(($el) => resizeBy(cy.get("@textboxEl"), -$el.width(), -$el.height())) // resize to 0,0
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.should($el => {
|
.should(($el) => {
|
||||||
expect($el.width()).to.eq(185); // min textbox width
|
expect($el.width()).to.eq(185); // min textbox width
|
||||||
expect($el.height()).to.eq(35); // min textbox height
|
expect($el.height()).to.eq(85); // min textbox height
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { getWidgetTestId, editDashboard } from "../../support/dashboard";
|
import { getWidgetTestId, editDashboard } from "../../support/dashboard";
|
||||||
|
|
||||||
describe("Textbox", () => {
|
describe("Textbox", () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createDashboard("Foo Bar").then(({ id }) => {
|
cy.createDashboard("Foo Bar").then(({ id }) => {
|
||||||
this.dashboardId = id;
|
this.dashboardId = id;
|
||||||
@@ -12,12 +12,10 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const confirmDeletionInModal = () => {
|
const confirmDeletionInModal = () => {
|
||||||
cy.get(".ant-modal .ant-btn")
|
cy.get(".ant-modal .ant-btn").contains("Delete").click({ force: true });
|
||||||
.contains("Delete")
|
|
||||||
.click({ force: true });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it("adds textbox", function() {
|
it("adds textbox", function () {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
editDashboard();
|
editDashboard();
|
||||||
cy.getByTestId("AddTextboxButton").click();
|
cy.getByTestId("AddTextboxButton").click();
|
||||||
@@ -29,10 +27,10 @@ describe("Textbox", () => {
|
|||||||
cy.get(".widget-text").should("exist");
|
cy.get(".widget-text").should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes textbox by X button", function() {
|
it("removes textbox by X button", function () {
|
||||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
editDashboard();
|
editDashboard();
|
||||||
|
|
||||||
@@ -45,32 +43,30 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes textbox by menu", function() {
|
it("removes textbox by menu", function () {
|
||||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId).within(() => {
|
cy.getByTestId(elTestId).within(() => {
|
||||||
cy.getByTestId("WidgetDropdownButton").click();
|
cy.getByTestId("WidgetDropdownButton").click();
|
||||||
});
|
});
|
||||||
cy.getByTestId("WidgetDropdownButtonMenu")
|
cy.getByTestId("WidgetDropdownButtonMenu").contains("Remove from Dashboard").click();
|
||||||
.contains("Remove from Dashboard")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
confirmDeletionInModal();
|
confirmDeletionInModal();
|
||||||
cy.getByTestId(elTestId).should("not.exist");
|
cy.getByTestId(elTestId).should("not.exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows opening menu after removal", function() {
|
it("allows opening menu after removal", function () {
|
||||||
let elTestId1;
|
let elTestId1;
|
||||||
cy.addTextbox(this.dashboardId, "txb 1")
|
cy.addTextbox(this.dashboardId, "txb 1")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
elTestId1 = elTestId;
|
elTestId1 = elTestId;
|
||||||
return cy.addTextbox(this.dashboardId, "txb 2").then(getWidgetTestId);
|
return cy.addTextbox(this.dashboardId, "txb 2").then(getWidgetTestId);
|
||||||
})
|
})
|
||||||
.then(elTestId2 => {
|
.then((elTestId2) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
editDashboard();
|
editDashboard();
|
||||||
|
|
||||||
@@ -97,10 +93,10 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("edits textbox", function() {
|
it("edits textbox", function () {
|
||||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId)
|
cy.getByTestId(elTestId)
|
||||||
.as("textboxEl")
|
.as("textboxEl")
|
||||||
@@ -108,17 +104,13 @@ describe("Textbox", () => {
|
|||||||
cy.getByTestId("WidgetDropdownButton").click();
|
cy.getByTestId("WidgetDropdownButton").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("WidgetDropdownButtonMenu")
|
cy.getByTestId("WidgetDropdownButtonMenu").contains("Edit").click();
|
||||||
.contains("Edit")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const newContent = "[edited]";
|
const newContent = "[edited]";
|
||||||
cy.getByTestId("TextboxDialog")
|
cy.getByTestId("TextboxDialog")
|
||||||
.should("exist")
|
.should("exist")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("textarea")
|
cy.get("textarea").clear().type(newContent);
|
||||||
.clear()
|
|
||||||
.type(newContent);
|
|
||||||
cy.contains("button", "Save").click();
|
cy.contains("button", "Save").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,7 +118,7 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders textbox according to position configuration", function() {
|
it("renders textbox according to position configuration", function () {
|
||||||
const id = this.dashboardId;
|
const id = this.dashboardId;
|
||||||
const txb1Pos = { col: 0, row: 0, sizeX: 3, sizeY: 2 };
|
const txb1Pos = { col: 0, row: 0, sizeX: 3, sizeY: 2 };
|
||||||
const txb2Pos = { col: 1, row: 1, sizeX: 3, sizeY: 4 };
|
const txb2Pos = { col: 1, row: 1, sizeX: 3, sizeY: 4 };
|
||||||
@@ -135,15 +127,15 @@ describe("Textbox", () => {
|
|||||||
cy.addTextbox(id, "x", { position: txb1Pos })
|
cy.addTextbox(id, "x", { position: txb1Pos })
|
||||||
.then(() => cy.addTextbox(id, "x", { position: txb2Pos }))
|
.then(() => cy.addTextbox(id, "x", { position: txb2Pos }))
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
return cy.getByTestId(elTestId);
|
return cy.getByTestId(elTestId);
|
||||||
})
|
})
|
||||||
.should($el => {
|
.should(($el) => {
|
||||||
const { top, left } = $el.offset();
|
const { top, left } = $el.offset();
|
||||||
expect(top).to.be.oneOf([162, 162.015625]);
|
expect(top).to.be.oneOf([162, 162.015625]);
|
||||||
expect(left).to.eq(282);
|
expect(left).to.eq(188);
|
||||||
expect($el.width()).to.eq(545);
|
expect($el.width()).to.eq(265);
|
||||||
expect($el.height()).to.eq(185);
|
expect($el.height()).to.eq(185);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ describe("Embedded Queries", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("is unavailable when public urls feature is disabled", () => {
|
it("is unavailable when public urls feature is disabled", () => {
|
||||||
cy.createQuery({ query: "select name from users order by name" }).then(query => {
|
cy.createQuery({ query: "select name from users order by name" }).then((query) => {
|
||||||
cy.visit(`/queries/${query.id}/source`);
|
cy.visit(`/queries/${query.id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
||||||
cy.clickThrough(`
|
cy.clickThrough(`
|
||||||
@@ -15,7 +16,7 @@ describe("Embedded Queries", () => {
|
|||||||
`);
|
`);
|
||||||
cy.getByTestId("EmbedIframe")
|
cy.getByTestId("EmbedIframe")
|
||||||
.invoke("text")
|
.invoke("text")
|
||||||
.then(embedUrl => {
|
.then((embedUrl) => {
|
||||||
// disable the feature
|
// disable the feature
|
||||||
cy.updateOrgSettings({ disable_public_urls: true });
|
cy.updateOrgSettings({ disable_public_urls: true });
|
||||||
|
|
||||||
@@ -23,9 +24,7 @@ describe("Embedded Queries", () => {
|
|||||||
cy.visit(`/queries/${query.id}/source`);
|
cy.visit(`/queries/${query.id}/source`);
|
||||||
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
||||||
cy.getByTestId("QueryPageHeaderMoreButton").click();
|
cy.getByTestId("QueryPageHeaderMoreButton").click();
|
||||||
cy.get(".ant-dropdown-menu-item")
|
cy.get(".ant-dropdown-menu-item").should("exist").should("not.contain", "Show API Key");
|
||||||
.should("exist")
|
|
||||||
.should("not.contain", "Show API Key");
|
|
||||||
cy.getByTestId("QueryControlDropdownButton").click();
|
cy.getByTestId("QueryControlDropdownButton").click();
|
||||||
cy.get(".ant-dropdown-menu-item").should("exist");
|
cy.get(".ant-dropdown-menu-item").should("exist");
|
||||||
cy.getByTestId("ShowEmbedDialogButton").should("not.exist");
|
cy.getByTestId("ShowEmbedDialogButton").should("not.exist");
|
||||||
@@ -42,8 +41,9 @@ describe("Embedded Queries", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("can be shared without parameters", () => {
|
it("can be shared without parameters", () => {
|
||||||
cy.createQuery({ query: "select name from users order by name" }).then(query => {
|
cy.createQuery({ query: "select name from users order by name" }).then((query) => {
|
||||||
cy.visit(`/queries/${query.id}/source`);
|
cy.visit(`/queries/${query.id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
cy.getByTestId("QueryPageVisualizationTabs", { timeout: 10000 }).should("exist");
|
||||||
cy.clickThrough(`
|
cy.clickThrough(`
|
||||||
@@ -52,7 +52,7 @@ describe("Embedded Queries", () => {
|
|||||||
`);
|
`);
|
||||||
cy.getByTestId("EmbedIframe")
|
cy.getByTestId("EmbedIframe")
|
||||||
.invoke("text")
|
.invoke("text")
|
||||||
.then(embedUrl => {
|
.then((embedUrl) => {
|
||||||
cy.logout();
|
cy.logout();
|
||||||
cy.visit(embedUrl);
|
cy.visit(embedUrl);
|
||||||
cy.getByTestId("VisualizationEmbed", { timeout: 10000 }).should("exist");
|
cy.getByTestId("VisualizationEmbed", { timeout: 10000 }).should("exist");
|
||||||
@@ -90,7 +90,7 @@ describe("Embedded Queries", () => {
|
|||||||
|
|
||||||
cy.getByTestId("EmbedIframe")
|
cy.getByTestId("EmbedIframe")
|
||||||
.invoke("text")
|
.invoke("text")
|
||||||
.then(embedUrl => {
|
.then((embedUrl) => {
|
||||||
cy.logout();
|
cy.logout();
|
||||||
cy.visit(embedUrl);
|
cy.visit(embedUrl);
|
||||||
cy.getByTestId("VisualizationEmbed", { timeout: 10000 }).should("exist");
|
cy.getByTestId("VisualizationEmbed", { timeout: 10000 }).should("exist");
|
||||||
|
|||||||
@@ -2,16 +2,14 @@ import { dragParam } from "../../support/parameters";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
function openAndSearchAntdDropdown(testId, paramOption) {
|
function openAndSearchAntdDropdown(testId, paramOption) {
|
||||||
cy.getByTestId(testId)
|
cy.getByTestId(testId).find(".ant-select-selection-search-input").type(paramOption, { force: true });
|
||||||
.find(".ant-select-selection-search-input")
|
|
||||||
.type(paramOption, { force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Parameter", () => {
|
describe("Parameter", () => {
|
||||||
const expectDirtyStateChange = edit => {
|
const expectDirtyStateChange = (edit) => {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter")
|
||||||
.find(".parameter-input")
|
.find(".parameter-input")
|
||||||
.should($el => {
|
.should(($el) => {
|
||||||
assert.isUndefined($el.data("dirty"));
|
assert.isUndefined($el.data("dirty"));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,7 +17,7 @@ describe("Parameter", () => {
|
|||||||
|
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter")
|
||||||
.find(".parameter-input")
|
.find(".parameter-input")
|
||||||
.should($el => {
|
.should(($el) => {
|
||||||
assert.isTrue($el.data("dirty"));
|
assert.isTrue($el.data("dirty"));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -42,9 +40,7 @@ describe("Parameter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updates the results after clicking Apply", () => {
|
it("updates the results after clicking Apply", () => {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("Redash");
|
||||||
.find("input")
|
|
||||||
.type("Redash");
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
@@ -53,13 +49,66 @@ describe("Parameter", () => {
|
|||||||
|
|
||||||
it("sets dirty state when edited", () => {
|
it("sets dirty state when edited", () => {
|
||||||
expectDirtyStateChange(() => {
|
expectDirtyStateChange(() => {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("Redash");
|
||||||
.find("input")
|
|
||||||
.type("Redash");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Text Pattern Parameter", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const queryData = {
|
||||||
|
name: "Text Pattern Parameter",
|
||||||
|
query: "SELECT '{{test-parameter}}' AS parameter",
|
||||||
|
options: {
|
||||||
|
parameters: [{ name: "test-parameter", title: "Test Parameter", type: "text-pattern", regex: "a.*a" }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
cy.createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}/source`));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the results after clicking Apply", () => {
|
||||||
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("{selectall}arta");
|
||||||
|
|
||||||
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
|
cy.getByTestId("TableVisualization").should("contain", "arta");
|
||||||
|
|
||||||
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("{selectall}arounda");
|
||||||
|
|
||||||
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
|
cy.getByTestId("TableVisualization").should("contain", "arounda");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws error message with invalid query request", () => {
|
||||||
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("{selectall}arta");
|
||||||
|
|
||||||
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("{selectall}abcab");
|
||||||
|
|
||||||
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
|
cy.getByTestId("QueryExecutionStatus").should("exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets dirty state when edited", () => {
|
||||||
|
expectDirtyStateChange(() => {
|
||||||
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("{selectall}arta");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't let user save invalid regex", () => {
|
||||||
|
cy.get(".fa-cog").click();
|
||||||
|
cy.getByTestId("RegexPatternInput").type("{selectall}[");
|
||||||
|
cy.contains("Invalid Regex Pattern").should("exist");
|
||||||
|
cy.getByTestId("SaveParameterSettings").click();
|
||||||
|
cy.get(".fa-cog").click();
|
||||||
|
cy.getByTestId("RegexPatternInput").should("not.equal", "[");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Number Parameter", () => {
|
describe("Number Parameter", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const queryData = {
|
const queryData = {
|
||||||
@@ -74,17 +123,13 @@ describe("Parameter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updates the results after clicking Apply", () => {
|
it("updates the results after clicking Apply", () => {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("{selectall}42");
|
||||||
.find("input")
|
|
||||||
.type("{selectall}42");
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
cy.getByTestId("TableVisualization").should("contain", 42);
|
cy.getByTestId("TableVisualization").should("contain", 42);
|
||||||
|
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("{selectall}31415");
|
||||||
.find("input")
|
|
||||||
.type("{selectall}31415");
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
@@ -93,9 +138,7 @@ describe("Parameter", () => {
|
|||||||
|
|
||||||
it("sets dirty state when edited", () => {
|
it("sets dirty state when edited", () => {
|
||||||
expectDirtyStateChange(() => {
|
expectDirtyStateChange(() => {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").type("{selectall}42");
|
||||||
.find("input")
|
|
||||||
.type("{selectall}42");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -119,10 +162,7 @@ describe("Parameter", () => {
|
|||||||
openAndSearchAntdDropdown("ParameterName-test-parameter", "value2"); // asserts option filter prop
|
openAndSearchAntdDropdown("ParameterName-test-parameter", "value2"); // asserts option filter prop
|
||||||
|
|
||||||
// only the filtered option should be on the DOM
|
// only the filtered option should be on the DOM
|
||||||
cy.get(".ant-select-item-option")
|
cy.get(".ant-select-item-option").should("have.length", 1).and("contain", "value2").click();
|
||||||
.should("have.length", 1)
|
|
||||||
.and("contain", "value2")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
// ensure that query is being executed
|
// ensure that query is being executed
|
||||||
@@ -140,12 +180,10 @@ describe("Parameter", () => {
|
|||||||
SaveParameterSettings
|
SaveParameterSettings
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find(".ant-select-selection-search").click();
|
||||||
.find(".ant-select-selection-search")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// select all unselected options
|
// select all unselected options
|
||||||
cy.get(".ant-select-item-option").each($option => {
|
cy.get(".ant-select-item-option").each(($option) => {
|
||||||
if (!$option.hasClass("ant-select-item-option-selected")) {
|
if (!$option.hasClass("ant-select-item-option-selected")) {
|
||||||
cy.wrap($option).click();
|
cy.wrap($option).click();
|
||||||
}
|
}
|
||||||
@@ -160,9 +198,7 @@ describe("Parameter", () => {
|
|||||||
|
|
||||||
it("sets dirty state when edited", () => {
|
it("sets dirty state when edited", () => {
|
||||||
expectDirtyStateChange(() => {
|
expectDirtyStateChange(() => {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find(".ant-select").click();
|
||||||
.find(".ant-select")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.contains(".ant-select-item-option", "value2").click();
|
cy.contains(".ant-select-item-option", "value2").click();
|
||||||
});
|
});
|
||||||
@@ -176,7 +212,7 @@ describe("Parameter", () => {
|
|||||||
name: "Dropdown Query",
|
name: "Dropdown Query",
|
||||||
query: "",
|
query: "",
|
||||||
};
|
};
|
||||||
cy.createQuery(dropdownQueryData, true).then(dropdownQuery => {
|
cy.createQuery(dropdownQueryData, true).then((dropdownQuery) => {
|
||||||
const queryData = {
|
const queryData = {
|
||||||
name: "Query Based Dropdown Parameter",
|
name: "Query Based Dropdown Parameter",
|
||||||
query: "SELECT '{{test-parameter}}' AS parameter",
|
query: "SELECT '{{test-parameter}}' AS parameter",
|
||||||
@@ -208,7 +244,7 @@ describe("Parameter", () => {
|
|||||||
SELECT 'value2' AS name, 2 AS value UNION ALL
|
SELECT 'value2' AS name, 2 AS value UNION ALL
|
||||||
SELECT 'value3' AS name, 3 AS value`,
|
SELECT 'value3' AS name, 3 AS value`,
|
||||||
};
|
};
|
||||||
cy.createQuery(dropdownQueryData, true).then(dropdownQuery => {
|
cy.createQuery(dropdownQueryData, true).then((dropdownQuery) => {
|
||||||
const queryData = {
|
const queryData = {
|
||||||
name: "Query Based Dropdown Parameter",
|
name: "Query Based Dropdown Parameter",
|
||||||
query: "SELECT '{{test-parameter}}' AS parameter",
|
query: "SELECT '{{test-parameter}}' AS parameter",
|
||||||
@@ -234,10 +270,7 @@ describe("Parameter", () => {
|
|||||||
openAndSearchAntdDropdown("ParameterName-test-parameter", "value2"); // asserts option filter prop
|
openAndSearchAntdDropdown("ParameterName-test-parameter", "value2"); // asserts option filter prop
|
||||||
|
|
||||||
// only the filtered option should be on the DOM
|
// only the filtered option should be on the DOM
|
||||||
cy.get(".ant-select-item-option")
|
cy.get(".ant-select-item-option").should("have.length", 1).and("contain", "value2").click();
|
||||||
.should("have.length", 1)
|
|
||||||
.and("contain", "value2")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
// ensure that query is being executed
|
// ensure that query is being executed
|
||||||
@@ -255,12 +288,10 @@ describe("Parameter", () => {
|
|||||||
SaveParameterSettings
|
SaveParameterSettings
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find(".ant-select").click();
|
||||||
.find(".ant-select")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// make sure all options are unselected and select all
|
// make sure all options are unselected and select all
|
||||||
cy.get(".ant-select-item-option").each($option => {
|
cy.get(".ant-select-item-option").each(($option) => {
|
||||||
expect($option).not.to.have.class("ant-select-dropdown-menu-item-selected");
|
expect($option).not.to.have.class("ant-select-dropdown-menu-item-selected");
|
||||||
cy.wrap($option).click();
|
cy.wrap($option).click();
|
||||||
});
|
});
|
||||||
@@ -274,14 +305,10 @@ describe("Parameter", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectCalendarDate = date => {
|
const selectCalendarDate = (date) => {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").click();
|
||||||
.find("input")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".ant-picker-panel")
|
cy.get(".ant-picker-panel").contains(".ant-picker-cell-inner", date).click();
|
||||||
.contains(".ant-picker-cell-inner", date)
|
|
||||||
.click();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("Date Parameter", () => {
|
describe("Date Parameter", () => {
|
||||||
@@ -303,10 +330,10 @@ describe("Parameter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.clock().then(clock => clock.restore());
|
cy.clock().then((clock) => clock.restore());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the results after selecting a date", function() {
|
it("updates the results after selecting a date", function () {
|
||||||
selectCalendarDate("15");
|
selectCalendarDate("15");
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
@@ -314,12 +341,10 @@ describe("Parameter", () => {
|
|||||||
cy.getByTestId("TableVisualization").should("contain", dayjs(this.now).format("15/MM/YY"));
|
cy.getByTestId("TableVisualization").should("contain", dayjs(this.now).format("15/MM/YY"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows picking a dynamic date", function() {
|
it("allows picking a dynamic date", function () {
|
||||||
cy.getByTestId("DynamicButton").click();
|
cy.getByTestId("DynamicButton").click();
|
||||||
|
|
||||||
cy.getByTestId("DynamicButtonMenu")
|
cy.getByTestId("DynamicButtonMenu").contains("Today/Now").click();
|
||||||
.contains("Today/Now")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
@@ -350,14 +375,11 @@ describe("Parameter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.clock().then(clock => clock.restore());
|
cy.clock().then((clock) => clock.restore());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the results after selecting a date and clicking in ok", function() {
|
it("updates the results after selecting a date and clicking in ok", function () {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").as("Input").click();
|
||||||
.find("input")
|
|
||||||
.as("Input")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
selectCalendarDate("15");
|
selectCalendarDate("15");
|
||||||
|
|
||||||
@@ -368,27 +390,20 @@ describe("Parameter", () => {
|
|||||||
cy.getByTestId("TableVisualization").should("contain", dayjs(this.now).format("YYYY-MM-15 HH:mm"));
|
cy.getByTestId("TableVisualization").should("contain", dayjs(this.now).format("YYYY-MM-15 HH:mm"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows the current datetime after clicking in Now", function() {
|
it("shows the current datetime after clicking in Now", function () {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").as("Input").click();
|
||||||
.find("input")
|
|
||||||
.as("Input")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".ant-picker-panel")
|
cy.get(".ant-picker-panel").contains("Now").click();
|
||||||
.contains("Now")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
cy.getByTestId("TableVisualization").should("contain", dayjs(this.now).format("YYYY-MM-DD HH:mm"));
|
cy.getByTestId("TableVisualization").should("contain", dayjs(this.now).format("YYYY-MM-DD HH:mm"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows picking a dynamic date", function() {
|
it("allows picking a dynamic date", function () {
|
||||||
cy.getByTestId("DynamicButton").click();
|
cy.getByTestId("DynamicButton").click();
|
||||||
|
|
||||||
cy.getByTestId("DynamicButtonMenu")
|
cy.getByTestId("DynamicButtonMenu").contains("Today/Now").click();
|
||||||
.contains("Today/Now")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
@@ -397,31 +412,20 @@ describe("Parameter", () => {
|
|||||||
|
|
||||||
it("sets dirty state when edited", () => {
|
it("sets dirty state when edited", () => {
|
||||||
expectDirtyStateChange(() => {
|
expectDirtyStateChange(() => {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").click();
|
||||||
.find("input")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".ant-picker-panel")
|
cy.get(".ant-picker-panel").contains("Now").click();
|
||||||
.contains("Now")
|
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Date Range Parameter", () => {
|
describe("Date Range Parameter", () => {
|
||||||
const selectCalendarDateRange = (startDate, endDate) => {
|
const selectCalendarDateRange = (startDate, endDate) => {
|
||||||
cy.getByTestId("ParameterName-test-parameter")
|
cy.getByTestId("ParameterName-test-parameter").find("input").first().click();
|
||||||
.find("input")
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".ant-picker-panel")
|
cy.get(".ant-picker-panel").contains(".ant-picker-cell-inner", startDate).click();
|
||||||
.contains(".ant-picker-cell-inner", startDate)
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".ant-picker-panel")
|
cy.get(".ant-picker-panel").contains(".ant-picker-cell-inner", endDate).click();
|
||||||
.contains(".ant-picker-cell-inner", endDate)
|
|
||||||
.click();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -442,10 +446,10 @@ describe("Parameter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.clock().then(clock => clock.restore());
|
cy.clock().then((clock) => clock.restore());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the results after selecting a date range", function() {
|
it("updates the results after selecting a date range", function () {
|
||||||
selectCalendarDateRange("15", "20");
|
selectCalendarDateRange("15", "20");
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
@@ -457,12 +461,10 @@ describe("Parameter", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows picking a dynamic date range", function() {
|
it("allows picking a dynamic date range", function () {
|
||||||
cy.getByTestId("DynamicButton").click();
|
cy.getByTestId("DynamicButton").click();
|
||||||
|
|
||||||
cy.getByTestId("DynamicButtonMenu")
|
cy.getByTestId("DynamicButtonMenu").contains("Last month").click();
|
||||||
.contains("Last month")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").click();
|
cy.getByTestId("ParameterApplyButton").click();
|
||||||
|
|
||||||
@@ -479,15 +481,10 @@ describe("Parameter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Apply Changes", () => {
|
describe("Apply Changes", () => {
|
||||||
const expectAppliedChanges = apply => {
|
const expectAppliedChanges = (apply) => {
|
||||||
cy.getByTestId("ParameterName-test-parameter-1")
|
cy.getByTestId("ParameterName-test-parameter-1").find("input").as("Input").type("Redash");
|
||||||
.find("input")
|
|
||||||
.as("Input")
|
|
||||||
.type("Redash");
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterName-test-parameter-2")
|
cy.getByTestId("ParameterName-test-parameter-2").find("input").type("Redash");
|
||||||
.find("input")
|
|
||||||
.type("Redash");
|
|
||||||
|
|
||||||
cy.location("search").should("not.contain", "Redash");
|
cy.location("search").should("not.contain", "Redash");
|
||||||
|
|
||||||
@@ -523,10 +520,7 @@ describe("Parameter", () => {
|
|||||||
it("shows and hides according to parameter dirty state", () => {
|
it("shows and hides according to parameter dirty state", () => {
|
||||||
cy.getByTestId("ParameterApplyButton").should("not.be", "visible");
|
cy.getByTestId("ParameterApplyButton").should("not.be", "visible");
|
||||||
|
|
||||||
cy.getByTestId("ParameterName-test-parameter-1")
|
cy.getByTestId("ParameterName-test-parameter-1").find("input").as("Param").type("Redash");
|
||||||
.find("input")
|
|
||||||
.as("Param")
|
|
||||||
.type("Redash");
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton").should("be.visible");
|
cy.getByTestId("ParameterApplyButton").should("be.visible");
|
||||||
|
|
||||||
@@ -536,21 +530,13 @@ describe("Parameter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updates dirty counter", () => {
|
it("updates dirty counter", () => {
|
||||||
cy.getByTestId("ParameterName-test-parameter-1")
|
cy.getByTestId("ParameterName-test-parameter-1").find("input").type("Redash");
|
||||||
.find("input")
|
|
||||||
.type("Redash");
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton")
|
cy.getByTestId("ParameterApplyButton").find(".ant-badge-count p.current").should("contain", "1");
|
||||||
.find(".ant-badge-count p.current")
|
|
||||||
.should("contain", "1");
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterName-test-parameter-2")
|
cy.getByTestId("ParameterName-test-parameter-2").find("input").type("Redash");
|
||||||
.find("input")
|
|
||||||
.type("Redash");
|
|
||||||
|
|
||||||
cy.getByTestId("ParameterApplyButton")
|
cy.getByTestId("ParameterApplyButton").find(".ant-badge-count p.current").should("contain", "2");
|
||||||
.find(".ant-badge-count p.current")
|
|
||||||
.should("contain", "2");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies changes from "Apply Changes" button', () => {
|
it('applies changes from "Apply Changes" button', () => {
|
||||||
@@ -560,16 +546,13 @@ describe("Parameter", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('applies changes from "alt+enter" keyboard shortcut', () => {
|
it('applies changes from "alt+enter" keyboard shortcut', () => {
|
||||||
expectAppliedChanges(input => {
|
expectAppliedChanges((input) => {
|
||||||
input.type("{alt}{enter}");
|
input.type("{alt}{enter}");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disables "Execute" button', () => {
|
it('disables "Execute" button', () => {
|
||||||
cy.getByTestId("ParameterName-test-parameter-1")
|
cy.getByTestId("ParameterName-test-parameter-1").find("input").as("Input").type("Redash");
|
||||||
.find("input")
|
|
||||||
.as("Input")
|
|
||||||
.type("Redash");
|
|
||||||
cy.getByTestId("ExecuteButton").should("be.disabled");
|
cy.getByTestId("ExecuteButton").should("be.disabled");
|
||||||
|
|
||||||
cy.get("@Input").clear();
|
cy.get("@Input").clear();
|
||||||
@@ -594,15 +577,12 @@ describe("Parameter", () => {
|
|||||||
|
|
||||||
cy.createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}/source`));
|
cy.createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}/source`));
|
||||||
|
|
||||||
cy.get(".parameter-block")
|
cy.get(".parameter-block").first().invoke("width").as("paramWidth");
|
||||||
.first()
|
|
||||||
.invoke("width")
|
|
||||||
.as("paramWidth");
|
|
||||||
|
|
||||||
cy.get("body").type("{alt}D"); // hide schema browser
|
cy.get("body").type("{alt}D"); // hide schema browser
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is possible to rearrange parameters", function() {
|
it("is possible to rearrange parameters", function () {
|
||||||
cy.server();
|
cy.server();
|
||||||
cy.route("POST", "**/api/queries/*").as("QuerySave");
|
cy.route("POST", "**/api/queries/*").as("QuerySave");
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ describe("Box Plot", () => {
|
|||||||
.then(({ id }) => cy.createVisualization(id, "BOXPLOT", "Boxplot (Deprecated)", {}))
|
.then(({ id }) => cy.createVisualization(id, "BOXPLOT", "Boxplot (Deprecated)", {}))
|
||||||
.then(({ id: visualizationId, query_id: queryId }) => {
|
.then(({ id: visualizationId, query_id: queryId }) => {
|
||||||
cy.visit(`queries/${queryId}/source#${visualizationId}`);
|
cy.visit(`queries/${queryId}/source#${visualizationId}`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -61,9 +62,7 @@ describe("Box Plot", () => {
|
|||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("svg").should("exist");
|
||||||
.find("svg")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Box Plot", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Box Plot", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,33 +26,34 @@ const SQL = `
|
|||||||
describe("Chart", () => {
|
describe("Chart", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ name: "Chart Visualization", query: SQL })
|
cy.createQuery({ name: "Chart Visualization", query: SQL }).its("id").as("queryId");
|
||||||
.its("id")
|
|
||||||
.as("queryId");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates Bar charts", function() {
|
it("creates Bar charts", function () {
|
||||||
cy.visit(`queries/${this.queryId}/source`);
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
|
||||||
const getBarChartAssertionFunction = (specificBarChartAssertionFn = () => {}) => () => {
|
const getBarChartAssertionFunction =
|
||||||
// checks for TabbedEditor standard tabs
|
(specificBarChartAssertionFn = () => {}) =>
|
||||||
assertTabbedEditor();
|
() => {
|
||||||
|
// checks for TabbedEditor standard tabs
|
||||||
|
assertTabbedEditor();
|
||||||
|
|
||||||
// standard chart should be bar
|
// standard chart should be bar
|
||||||
cy.getByTestId("Chart.GlobalSeriesType").contains(".ant-select-selection-item", "Bar");
|
cy.getByTestId("Chart.GlobalSeriesType").contains(".ant-select-selection-item", "Bar");
|
||||||
|
|
||||||
// checks the plot canvas exists and is empty
|
// checks the plot canvas exists and is empty
|
||||||
assertPlotPreview("not.exist");
|
assertPlotPreview("not.exist");
|
||||||
|
|
||||||
// creates a chart and checks it is plotted
|
// creates a chart and checks it is plotted
|
||||||
cy.getByTestId("Chart.ColumnMapping.x").selectAntdOption("Chart.ColumnMapping.x.stage");
|
cy.getByTestId("Chart.ColumnMapping.x").selectAntdOption("Chart.ColumnMapping.x.stage");
|
||||||
cy.getByTestId("Chart.ColumnMapping.y").selectAntdOption("Chart.ColumnMapping.y.value1");
|
cy.getByTestId("Chart.ColumnMapping.y").selectAntdOption("Chart.ColumnMapping.y.value1");
|
||||||
cy.getByTestId("Chart.ColumnMapping.y").selectAntdOption("Chart.ColumnMapping.y.value2");
|
cy.getByTestId("Chart.ColumnMapping.y").selectAntdOption("Chart.ColumnMapping.y.value2");
|
||||||
assertPlotPreview("exist");
|
assertPlotPreview("exist");
|
||||||
|
|
||||||
specificBarChartAssertionFn();
|
specificBarChartAssertionFn();
|
||||||
};
|
};
|
||||||
|
|
||||||
const chartTests = [
|
const chartTests = [
|
||||||
{
|
{
|
||||||
@@ -95,8 +96,8 @@ describe("Chart", () => {
|
|||||||
|
|
||||||
const withDashboardWidgetsAssertionFn = (widgetGetters, dashboardUrl) => {
|
const withDashboardWidgetsAssertionFn = (widgetGetters, dashboardUrl) => {
|
||||||
cy.visit(dashboardUrl);
|
cy.visit(dashboardUrl);
|
||||||
widgetGetters.forEach(widgetGetter => {
|
widgetGetters.forEach((widgetGetter) => {
|
||||||
cy.get(`@${widgetGetter}`).then(widget => {
|
cy.get(`@${widgetGetter}`).then((widget) => {
|
||||||
cy.getByTestId(getWidgetTestId(widget)).within(() => {
|
cy.getByTestId(getWidgetTestId(widget)).within(() => {
|
||||||
cy.get("g.points").should("exist");
|
cy.get("g.points").should("exist");
|
||||||
});
|
});
|
||||||
@@ -107,4 +108,36 @@ describe("Chart", () => {
|
|||||||
createDashboardWithCharts("Bar chart visualizations", chartGetters, withDashboardWidgetsAssertionFn);
|
createDashboardWithCharts("Bar chart visualizations", chartGetters, withDashboardWidgetsAssertionFn);
|
||||||
cy.percySnapshot("Visualizations - Charts - Bar");
|
cy.percySnapshot("Visualizations - Charts - Bar");
|
||||||
});
|
});
|
||||||
|
it("colors Bar charts", function () {
|
||||||
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
cy.getByTestId("NewVisualization").click();
|
||||||
|
cy.getByTestId("Chart.ColumnMapping.x").selectAntdOption("Chart.ColumnMapping.x.stage");
|
||||||
|
cy.getByTestId("Chart.ColumnMapping.y").selectAntdOption("Chart.ColumnMapping.y.value1");
|
||||||
|
cy.getByTestId("VisualizationEditor.Tabs.Colors").click();
|
||||||
|
cy.getByTestId("ColorScheme").click();
|
||||||
|
cy.getByTestId("ColorOptionViridis").click();
|
||||||
|
cy.getByTestId("ColorScheme").click();
|
||||||
|
cy.getByTestId("ColorOptionTableau 10").click();
|
||||||
|
cy.getByTestId("ColorScheme").click();
|
||||||
|
cy.getByTestId("ColorOptionD3 Category 10").click();
|
||||||
|
});
|
||||||
|
it("colors Pie charts", function () {
|
||||||
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
cy.getByTestId("NewVisualization").click();
|
||||||
|
cy.getByTestId("Chart.GlobalSeriesType").click();
|
||||||
|
cy.getByTestId("Chart.ChartType.pie").click();
|
||||||
|
cy.getByTestId("Chart.ColumnMapping.x").selectAntdOption("Chart.ColumnMapping.x.stage");
|
||||||
|
cy.getByTestId("Chart.ColumnMapping.y").selectAntdOption("Chart.ColumnMapping.y.value1");
|
||||||
|
cy.getByTestId("VisualizationEditor.Tabs.Colors").click();
|
||||||
|
cy.getByTestId("ColorScheme").click();
|
||||||
|
cy.getByTestId("ColorOptionViridis").click();
|
||||||
|
cy.getByTestId("ColorScheme").click();
|
||||||
|
cy.getByTestId("ColorOptionTableau 10").click();
|
||||||
|
cy.getByTestId("ColorScheme").click();
|
||||||
|
cy.getByTestId("ColorOptionD3 Category 10").click();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ describe("Choropleth", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
@@ -76,9 +77,7 @@ describe("Choropleth", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".map-visualization-container.leaflet-container").should("exist");
|
||||||
.find(".map-visualization-container.leaflet-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Choropleth", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Choropleth", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe("Cohort", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
@@ -51,9 +52,7 @@ describe("Cohort", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
|
||||||
.should("exist");
|
|
||||||
cy.percySnapshot("Visualizations - Cohort (simple)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Cohort (simple)", { widths: [viewportWidth] });
|
||||||
|
|
||||||
cy.clickThrough(`
|
cy.clickThrough(`
|
||||||
@@ -64,9 +63,7 @@ describe("Cohort", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
|
||||||
.should("exist");
|
|
||||||
cy.percySnapshot("Visualizations - Cohort (diagonal)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Cohort (diagonal)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ describe("Counter", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
@@ -24,9 +25,7 @@ describe("Counter", () => {
|
|||||||
Counter.General.ValueColumn.a
|
Counter.General.ValueColumn.a
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -43,9 +42,7 @@ describe("Counter", () => {
|
|||||||
"Counter.General.Label": "Custom Label",
|
"Counter.General.Label": "Custom Label",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -65,9 +62,7 @@ describe("Counter", () => {
|
|||||||
"Counter.General.TargetValueRowNumber": "2",
|
"Counter.General.TargetValueRowNumber": "2",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -83,9 +78,7 @@ describe("Counter", () => {
|
|||||||
Counter.General.TargetValueColumn.b
|
Counter.General.TargetValueColumn.b
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -106,9 +99,7 @@ describe("Counter", () => {
|
|||||||
"Counter.General.TargetValueRowNumber": "2",
|
"Counter.General.TargetValueRowNumber": "2",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -123,9 +114,7 @@ describe("Counter", () => {
|
|||||||
Counter.General.CountRows
|
Counter.General.CountRows
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -151,9 +140,7 @@ describe("Counter", () => {
|
|||||||
"Counter.Formatting.StringSuffix": "%",
|
"Counter.Formatting.StringSuffix": "%",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -180,9 +167,7 @@ describe("Counter", () => {
|
|||||||
"Counter.Formatting.StringSuffix": "%",
|
"Counter.Formatting.StringSuffix": "%",
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".counter-visualization-container").should("exist");
|
||||||
.find(".counter-visualization-container")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
// wait a bit before taking snapshot
|
// wait a bit before taking snapshot
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|||||||
@@ -5,34 +5,25 @@ describe("Edit visualization dialog", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery().then(({ id }) => {
|
cy.createQuery().then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens New Visualization dialog", () => {
|
it("opens New Visualization dialog", () => {
|
||||||
cy.getByTestId("NewVisualization")
|
cy.getByTestId("NewVisualization").should("exist").click();
|
||||||
.should("exist")
|
|
||||||
.click();
|
|
||||||
cy.getByTestId("EditVisualizationDialog").should("exist");
|
cy.getByTestId("EditVisualizationDialog").should("exist");
|
||||||
// Default visualization should be selected
|
// Default visualization should be selected
|
||||||
cy.getByTestId("VisualizationType")
|
cy.getByTestId("VisualizationType").should("exist").should("contain", "Chart");
|
||||||
.should("exist")
|
cy.getByTestId("VisualizationName").should("exist").should("have.value", "Chart");
|
||||||
.should("contain", "Chart");
|
|
||||||
cy.getByTestId("VisualizationName")
|
|
||||||
.should("exist")
|
|
||||||
.should("have.value", "Chart");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens Edit Visualization dialog", () => {
|
it("opens Edit Visualization dialog", () => {
|
||||||
cy.getByTestId("EditVisualization").click();
|
cy.getByTestId("EditVisualization").click();
|
||||||
cy.getByTestId("EditVisualizationDialog").should("exist");
|
cy.getByTestId("EditVisualizationDialog").should("exist");
|
||||||
// Default `Table` visualization should be selected
|
// Default `Table` visualization should be selected
|
||||||
cy.getByTestId("VisualizationType")
|
cy.getByTestId("VisualizationType").should("exist").should("contain", "Table");
|
||||||
.should("exist")
|
cy.getByTestId("VisualizationName").should("exist").should("have.value", "Table");
|
||||||
.should("contain", "Table");
|
|
||||||
cy.getByTestId("VisualizationName")
|
|
||||||
.should("exist")
|
|
||||||
.should("have.value", "Table");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates visualization with custom name", () => {
|
it("creates visualization with custom name", () => {
|
||||||
@@ -44,15 +35,9 @@ describe("Edit visualization dialog", () => {
|
|||||||
VisualizationType.TABLE
|
VisualizationType.TABLE
|
||||||
`);
|
`);
|
||||||
|
|
||||||
cy.getByTestId("VisualizationName")
|
cy.getByTestId("VisualizationName").clear().type(visualizationName);
|
||||||
.clear()
|
|
||||||
.type(visualizationName);
|
|
||||||
|
|
||||||
cy.getByTestId("EditVisualizationDialog")
|
cy.getByTestId("EditVisualizationDialog").contains("button", "Save").click();
|
||||||
.contains("button", "Save")
|
cy.getByTestId("QueryPageVisualizationTabs").contains("span", visualizationName).should("exist");
|
||||||
.click();
|
|
||||||
cy.getByTestId("QueryPageVisualizationTabs")
|
|
||||||
.contains("span", visualizationName)
|
|
||||||
.should("exist");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ describe("Funnel", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -59,9 +60,7 @@ describe("Funnel", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
|
||||||
.should("exist");
|
|
||||||
cy.percySnapshot("Visualizations - Funnel (basic)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Funnel (basic)", { widths: [viewportWidth] });
|
||||||
|
|
||||||
cy.clickThrough(`
|
cy.clickThrough(`
|
||||||
@@ -81,9 +80,7 @@ describe("Funnel", () => {
|
|||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
|
||||||
.should("exist");
|
|
||||||
cy.percySnapshot("Visualizations - Funnel (extra options)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Funnel (extra options)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe("Map (Markers)", () => {
|
|||||||
.then(({ id }) => cy.createVisualization(id, "MAP", "Map (Markers)", { mapTileUrl }))
|
.then(({ id }) => cy.createVisualization(id, "MAP", "Map (Markers)", { mapTileUrl }))
|
||||||
.then(({ id: visualizationId, query_id: queryId }) => {
|
.then(({ id: visualizationId, query_id: queryId }) => {
|
||||||
cy.visit(`queries/${queryId}/source#${visualizationId}`);
|
cy.visit(`queries/${queryId}/source#${visualizationId}`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -51,9 +52,7 @@ describe("Map (Markers)", () => {
|
|||||||
cy.fillInputs({ "ColorPicker.CustomColor": "blue{enter}" });
|
cy.fillInputs({ "ColorPicker.CustomColor": "blue{enter}" });
|
||||||
cy.getByTestId("ColorPicker.CustomColor").should("not.be.visible");
|
cy.getByTestId("ColorPicker.CustomColor").should("not.be.visible");
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".leaflet-control-zoom-in").click();
|
||||||
.find(".leaflet-control-zoom-in")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
@@ -85,9 +84,7 @@ describe("Map (Markers)", () => {
|
|||||||
cy.fillInputs({ "ColorPicker.CustomColor": "maroon{enter}" });
|
cy.fillInputs({ "ColorPicker.CustomColor": "maroon{enter}" });
|
||||||
cy.getByTestId("ColorPicker.CustomColor").should("not.be.visible");
|
cy.getByTestId("ColorPicker.CustomColor").should("not.be.visible");
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find(".leaflet-control-zoom-in").click();
|
||||||
.find(".leaflet-control-zoom-in")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ const SQL = `
|
|||||||
function createPivotThroughUI(visualizationName, options = {}) {
|
function createPivotThroughUI(visualizationName, options = {}) {
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.PIVOT");
|
cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.PIVOT");
|
||||||
cy.getByTestId("VisualizationName")
|
cy.getByTestId("VisualizationName").clear().type(visualizationName);
|
||||||
.clear()
|
|
||||||
.type(visualizationName);
|
|
||||||
if (options.hideControls) {
|
if (options.hideControls) {
|
||||||
cy.getByTestId("PivotEditor.HideControls").click();
|
cy.getByTestId("PivotEditor.HideControls").click();
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview")
|
||||||
@@ -29,36 +27,30 @@ function createPivotThroughUI(visualizationName, options = {}) {
|
|||||||
.find(".pvtAxisContainer, .pvtRenderer, .pvtVals")
|
.find(".pvtAxisContainer, .pvtRenderer, .pvtVals")
|
||||||
.should("be.not.visible");
|
.should("be.not.visible");
|
||||||
}
|
}
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("table").should("exist");
|
||||||
.find("table")
|
cy.getByTestId("EditVisualizationDialog").contains("button", "Save").click();
|
||||||
.should("exist");
|
|
||||||
cy.getByTestId("EditVisualizationDialog")
|
|
||||||
.contains("button", "Save")
|
|
||||||
.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Pivot", () => {
|
describe("Pivot", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ name: "Pivot Visualization", query: SQL })
|
cy.createQuery({ name: "Pivot Visualization", query: SQL }).its("id").as("queryId");
|
||||||
.its("id")
|
|
||||||
.as("queryId");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates Pivot with controls", function() {
|
it("creates Pivot with controls", function () {
|
||||||
cy.visit(`queries/${this.queryId}/source`);
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
|
||||||
const visualizationName = "Pivot";
|
const visualizationName = "Pivot";
|
||||||
createPivotThroughUI(visualizationName);
|
createPivotThroughUI(visualizationName);
|
||||||
|
|
||||||
cy.getByTestId("QueryPageVisualizationTabs")
|
cy.getByTestId("QueryPageVisualizationTabs").contains("span", visualizationName).should("exist");
|
||||||
.contains("span", visualizationName)
|
|
||||||
.should("exist");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates Pivot without controls", function() {
|
it("creates Pivot without controls", function () {
|
||||||
cy.visit(`queries/${this.queryId}/source`);
|
cy.visit(`queries/${this.queryId}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
|
||||||
const visualizationName = "Pivot";
|
const visualizationName = "Pivot";
|
||||||
@@ -76,7 +68,7 @@ describe("Pivot", () => {
|
|||||||
.should("be.not.visible");
|
.should("be.not.visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates the visualization when results change", function() {
|
it("updates the visualization when results change", function () {
|
||||||
const options = {
|
const options = {
|
||||||
aggregatorName: "Count",
|
aggregatorName: "Count",
|
||||||
data: [], // force it to have a data object, although it shouldn't
|
data: [], // force it to have a data object, although it shouldn't
|
||||||
@@ -86,8 +78,9 @@ describe("Pivot", () => {
|
|||||||
vals: ["value"],
|
vals: ["value"],
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.createVisualization(this.queryId, "PIVOT", "Pivot", options).then(visualization => {
|
cy.createVisualization(this.queryId, "PIVOT", "Pivot", options).then((visualization) => {
|
||||||
cy.visit(`queries/${this.queryId}/source#${visualization.id}`);
|
cy.visit(`queries/${this.queryId}/source#${visualization.id}`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
|
|
||||||
// assert number of rows is 11
|
// assert number of rows is 11
|
||||||
@@ -104,16 +97,14 @@ describe("Pivot", () => {
|
|||||||
cy.wait(200);
|
cy.wait(200);
|
||||||
|
|
||||||
cy.getByTestId("SaveButton").click();
|
cy.getByTestId("SaveButton").click();
|
||||||
cy.getByTestId("ExecuteButton")
|
cy.getByTestId("ExecuteButton").should("be.enabled").click();
|
||||||
.should("be.enabled")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// assert number of rows is 12
|
// assert number of rows is 12
|
||||||
cy.getByTestId("PivotTableVisualization").contains(".pvtGrandTotal", "12");
|
cy.getByTestId("PivotTableVisualization").contains(".pvtGrandTotal", "12");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("takes a snapshot with different configured Pivots", function() {
|
it("takes a snapshot with different configured Pivots", function () {
|
||||||
const options = {
|
const options = {
|
||||||
aggregatorName: "Sum",
|
aggregatorName: "Sum",
|
||||||
controls: { enabled: true },
|
controls: { enabled: true },
|
||||||
@@ -142,19 +133,20 @@ describe("Pivot", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
cy.createDashboard("Pivot Visualization")
|
cy.createDashboard("Pivot Visualization")
|
||||||
.then(dashboard => {
|
.then((dashboard) => {
|
||||||
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
||||||
return cy.all(
|
return cy.all(
|
||||||
pivotTables.map(pivot => () =>
|
pivotTables.map(
|
||||||
cy
|
(pivot) => () =>
|
||||||
.createVisualization(this.queryId, "PIVOT", pivot.name, pivot.options)
|
cy
|
||||||
.then(visualization => cy.addWidget(dashboard.id, visualization.id, { position: pivot.position }))
|
.createVisualization(this.queryId, "PIVOT", pivot.name, pivot.options)
|
||||||
|
.then((visualization) => cy.addWidget(dashboard.id, visualization.id, { position: pivot.position }))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.then(widgets => {
|
.then((widgets) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
widgets.forEach(widget => {
|
widgets.forEach((widget) => {
|
||||||
cy.getByTestId(getWidgetTestId(widget)).within(() =>
|
cy.getByTestId(getWidgetTestId(widget)).within(() =>
|
||||||
cy.getByTestId("PivotTableVisualization").should("exist")
|
cy.getByTestId("PivotTableVisualization").should("exist")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ describe("Sankey and Sunburst", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
cy.getByTestId("NewVisualization").click();
|
cy.getByTestId("NewVisualization").click();
|
||||||
cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.SUNBURST_SEQUENCE");
|
cy.getByTestId("VisualizationType").selectAntdOption("VisualizationType.SUNBURST_SEQUENCE");
|
||||||
@@ -34,37 +35,21 @@ describe("Sankey and Sunburst", () => {
|
|||||||
it("creates Sunburst", () => {
|
it("creates Sunburst", () => {
|
||||||
const visualizationName = "Sunburst";
|
const visualizationName = "Sunburst";
|
||||||
|
|
||||||
cy.getByTestId("VisualizationName")
|
cy.getByTestId("VisualizationName").clear().type(visualizationName);
|
||||||
.clear()
|
cy.getByTestId("VisualizationPreview").find("svg").should("exist");
|
||||||
.type(visualizationName);
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
|
||||||
.find("svg")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.getByTestId("EditVisualizationDialog")
|
cy.getByTestId("EditVisualizationDialog").contains("button", "Save").click();
|
||||||
.contains("button", "Save")
|
cy.getByTestId("QueryPageVisualizationTabs").contains("span", visualizationName).should("exist");
|
||||||
.click();
|
|
||||||
cy.getByTestId("QueryPageVisualizationTabs")
|
|
||||||
.contains("span", visualizationName)
|
|
||||||
.should("exist");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates Sankey", () => {
|
it("creates Sankey", () => {
|
||||||
const visualizationName = "Sankey";
|
const visualizationName = "Sankey";
|
||||||
|
|
||||||
cy.getByTestId("VisualizationName")
|
cy.getByTestId("VisualizationName").clear().type(visualizationName);
|
||||||
.clear()
|
cy.getByTestId("VisualizationPreview").find("svg").should("exist");
|
||||||
.type(visualizationName);
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
|
||||||
.find("svg")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.getByTestId("EditVisualizationDialog")
|
cy.getByTestId("EditVisualizationDialog").contains("button", "Save").click();
|
||||||
.contains("button", "Save")
|
cy.getByTestId("QueryPageVisualizationTabs").contains("span", visualizationName).should("exist");
|
||||||
.click();
|
|
||||||
cy.getByTestId("QueryPageVisualizationTabs")
|
|
||||||
.contains("span", visualizationName)
|
|
||||||
.should("exist");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,21 +77,22 @@ describe("Sankey and Sunburst", () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
it("takes a snapshot with Sunburst (1 - 5 stages)", function() {
|
it("takes a snapshot with Sunburst (1 - 5 stages)", function () {
|
||||||
cy.createDashboard("Sunburst Visualization").then(dashboard => {
|
cy.createDashboard("Sunburst Visualization").then((dashboard) => {
|
||||||
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
||||||
return cy
|
return cy
|
||||||
.all(
|
.all(
|
||||||
STAGES_WIDGETS.map(sunburst => () =>
|
STAGES_WIDGETS.map(
|
||||||
cy
|
(sunburst) => () =>
|
||||||
.createQuery({ name: `Sunburst with ${sunburst.name}`, query: sunburst.query })
|
cy
|
||||||
.then(queryData => cy.createVisualization(queryData.id, "SUNBURST_SEQUENCE", "Sunburst", {}))
|
.createQuery({ name: `Sunburst with ${sunburst.name}`, query: sunburst.query })
|
||||||
.then(visualization => cy.addWidget(dashboard.id, visualization.id, { position: sunburst.position }))
|
.then((queryData) => cy.createVisualization(queryData.id, "SUNBURST_SEQUENCE", "Sunburst", {}))
|
||||||
|
.then((visualization) => cy.addWidget(dashboard.id, visualization.id, { position: sunburst.position }))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.then(widgets => {
|
.then((widgets) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
widgets.forEach(widget => {
|
widgets.forEach((widget) => {
|
||||||
cy.getByTestId(getWidgetTestId(widget)).within(() => cy.get("svg").should("exist"));
|
cy.getByTestId(getWidgetTestId(widget)).within(() => cy.get("svg").should("exist"));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,21 +103,22 @@ describe("Sankey and Sunburst", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("takes a snapshot with Sankey (1 - 5 stages)", function() {
|
it("takes a snapshot with Sankey (1 - 5 stages)", function () {
|
||||||
cy.createDashboard("Sankey Visualization").then(dashboard => {
|
cy.createDashboard("Sankey Visualization").then((dashboard) => {
|
||||||
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
this.dashboardUrl = `/dashboards/${dashboard.id}`;
|
||||||
return cy
|
return cy
|
||||||
.all(
|
.all(
|
||||||
STAGES_WIDGETS.map(sankey => () =>
|
STAGES_WIDGETS.map(
|
||||||
cy
|
(sankey) => () =>
|
||||||
.createQuery({ name: `Sankey with ${sankey.name}`, query: sankey.query })
|
cy
|
||||||
.then(queryData => cy.createVisualization(queryData.id, "SANKEY", "Sankey", {}))
|
.createQuery({ name: `Sankey with ${sankey.name}`, query: sankey.query })
|
||||||
.then(visualization => cy.addWidget(dashboard.id, visualization.id, { position: sankey.position }))
|
.then((queryData) => cy.createVisualization(queryData.id, "SANKEY", "Sankey", {}))
|
||||||
|
.then((visualization) => cy.addWidget(dashboard.id, visualization.id, { position: sankey.position }))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.then(widgets => {
|
.then((widgets) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
widgets.forEach(widget => {
|
widgets.forEach((widget) => {
|
||||||
cy.getByTestId(getWidgetTestId(widget)).within(() => cy.get("svg").should("exist"));
|
cy.getByTestId(getWidgetTestId(widget)).within(() => cy.get("svg").should("exist"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ function prepareVisualization(query, type, name, options) {
|
|||||||
cy.get("body").type("{alt}D");
|
cy.get("body").type("{alt}D");
|
||||||
|
|
||||||
// do some pre-checks here to ensure that visualization was created and is visible
|
// do some pre-checks here to ensure that visualization was created and is visible
|
||||||
cy.getByTestId("TableVisualization")
|
cy.getByTestId("TableVisualization").should("exist").find("table").should("exist");
|
||||||
.should("exist")
|
|
||||||
.find("table")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
return cy.then(() => ({ queryId, visualizationId }));
|
return cy.then(() => ({ queryId, visualizationId }));
|
||||||
});
|
});
|
||||||
@@ -53,7 +50,7 @@ describe("Table", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Sorting data", () => {
|
describe("Sorting data", () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
const { query, config } = MultiColumnSort;
|
const { query, config } = MultiColumnSort;
|
||||||
prepareVisualization(query, "TABLE", "Sort data", config).then(({ queryId, visualizationId }) => {
|
prepareVisualization(query, "TABLE", "Sort data", config).then(({ queryId, visualizationId }) => {
|
||||||
this.queryId = queryId;
|
this.queryId = queryId;
|
||||||
@@ -61,39 +58,22 @@ describe("Table", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sorts data by a single column", function() {
|
it("sorts data by a single column", function () {
|
||||||
cy.getByTestId("TableVisualization")
|
cy.getByTestId("TableVisualization").find("table th").contains("c").should("exist").click();
|
||||||
.find("table th")
|
|
||||||
.contains("c")
|
|
||||||
.should("exist")
|
|
||||||
.click();
|
|
||||||
cy.percySnapshot("Visualizations - Table (Single-column sort)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Table (Single-column sort)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sorts data by a multiple columns", function() {
|
it("sorts data by a multiple columns", function () {
|
||||||
cy.getByTestId("TableVisualization")
|
cy.getByTestId("TableVisualization").find("table th").contains("a").should("exist").click();
|
||||||
.find("table th")
|
|
||||||
.contains("a")
|
|
||||||
.should("exist")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get("body").type("{shift}", { release: false });
|
cy.get("body").type("{shift}", { release: false });
|
||||||
cy.getByTestId("TableVisualization")
|
cy.getByTestId("TableVisualization").find("table th").contains("b").should("exist").click();
|
||||||
.find("table th")
|
|
||||||
.contains("b")
|
|
||||||
.should("exist")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Table (Multi-column sort)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Table (Multi-column sort)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sorts data in reverse order", function() {
|
it("sorts data in reverse order", function () {
|
||||||
cy.getByTestId("TableVisualization")
|
cy.getByTestId("TableVisualization").find("table th").contains("c").should("exist").click().click();
|
||||||
.find("table th")
|
|
||||||
.contains("c")
|
|
||||||
.should("exist")
|
|
||||||
.click()
|
|
||||||
.click();
|
|
||||||
cy.percySnapshot("Visualizations - Table (Single-column reverse sort)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Table (Single-column reverse sort)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -101,10 +81,7 @@ describe("Table", () => {
|
|||||||
it("searches in multiple columns", () => {
|
it("searches in multiple columns", () => {
|
||||||
const { query, config } = SearchInData;
|
const { query, config } = SearchInData;
|
||||||
prepareVisualization(query, "TABLE", "Search", config).then(({ visualizationId }) => {
|
prepareVisualization(query, "TABLE", "Search", config).then(({ visualizationId }) => {
|
||||||
cy.getByTestId("TableVisualization")
|
cy.getByTestId("TableVisualization").find("table input").should("exist").type("test");
|
||||||
.find("table input")
|
|
||||||
.should("exist")
|
|
||||||
.type("test");
|
|
||||||
cy.percySnapshot("Visualizations - Table (Search in data)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Table (Search in data)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ describe("Word Cloud", () => {
|
|||||||
cy.login();
|
cy.login();
|
||||||
cy.createQuery({ query: SQL }).then(({ id }) => {
|
cy.createQuery({ query: SQL }).then(({ id }) => {
|
||||||
cy.visit(`queries/${id}/source`);
|
cy.visit(`queries/${id}/source`);
|
||||||
|
cy.wait(1500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId("ExecuteButton").click();
|
cy.getByTestId("ExecuteButton").click();
|
||||||
});
|
});
|
||||||
cy.document().then(injectFont);
|
cy.document().then(injectFont);
|
||||||
@@ -80,9 +81,7 @@ describe("Word Cloud", () => {
|
|||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("svg text").should("have.length", 11);
|
||||||
.find("svg text")
|
|
||||||
.should("have.length", 11);
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Word Cloud (Automatic word frequencies)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Word Cloud (Automatic word frequencies)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
@@ -99,9 +98,7 @@ describe("Word Cloud", () => {
|
|||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("svg text").should("have.length", 5);
|
||||||
.find("svg text")
|
|
||||||
.should("have.length", 5);
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Word Cloud (Frequencies from another column)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Word Cloud (Frequencies from another column)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
@@ -125,9 +122,7 @@ describe("Word Cloud", () => {
|
|||||||
// Wait for proper initialization of visualization
|
// Wait for proper initialization of visualization
|
||||||
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
cy.wait(500); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||||
|
|
||||||
cy.getByTestId("VisualizationPreview")
|
cy.getByTestId("VisualizationPreview").find("svg text").should("have.length", 2);
|
||||||
.find("svg text")
|
|
||||||
.should("have.length", 2);
|
|
||||||
|
|
||||||
cy.percySnapshot("Visualizations - Word Cloud (With filters)", { widths: [viewportWidth] });
|
cy.percySnapshot("Visualizations - Word Cloud (With filters)", { widths: [viewportWidth] });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
const { extend, get, merge, find } = Cypress._;
|
const { extend, get, merge, find } = Cypress._;
|
||||||
|
|
||||||
const post = options =>
|
const post = (options) =>
|
||||||
cy
|
cy
|
||||||
.getCookie("csrf_token")
|
.getCookie("csrf_token")
|
||||||
.then(csrf => cy.request({ ...options, method: "POST", headers: { "X-CSRF-TOKEN": csrf.value } }));
|
.then((csrf) => cy.request({ ...options, method: "POST", headers: { "X-CSRF-TOKEN": csrf.value } }));
|
||||||
|
|
||||||
Cypress.Commands.add("createDashboard", name => {
|
Cypress.Commands.add("createDashboard", (name) => {
|
||||||
return post({ url: "api/dashboards", body: { name } }).then(({ body }) => body);
|
return post({ url: "api/dashboards", body: { name } }).then(({ body }) => body);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ Cypress.Commands.add("createQuery", (data, shouldPublish = true) => {
|
|||||||
// eslint-disable-next-line cypress/no-assigning-return-values
|
// eslint-disable-next-line cypress/no-assigning-return-values
|
||||||
let request = post({ url: "/api/queries", body: merged }).then(({ body }) => body);
|
let request = post({ url: "/api/queries", body: merged }).then(({ body }) => body);
|
||||||
if (shouldPublish) {
|
if (shouldPublish) {
|
||||||
request = request.then(query =>
|
request = request.then((query) =>
|
||||||
post({ url: `/api/queries/${query.id}`, body: { is_draft: false } }).then(() => query)
|
post({ url: `/api/queries/${query.id}`, body: { is_draft: false } }).then(() => query)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,7 @@ Cypress.Commands.add("addWidget", (dashboardId, visualizationId, options = {}) =
|
|||||||
Cypress.Commands.add("createAlert", (queryId, options = {}, name) => {
|
Cypress.Commands.add("createAlert", (queryId, options = {}, name) => {
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
column: "?column?",
|
column: "?column?",
|
||||||
|
selector: "first",
|
||||||
op: "greater than",
|
op: "greater than",
|
||||||
rearm: 0,
|
rearm: 0,
|
||||||
value: 1,
|
value: 1,
|
||||||
@@ -109,7 +110,7 @@ Cypress.Commands.add("createUser", ({ name, email, password }) => {
|
|||||||
url: "api/users?no_invite=yes",
|
url: "api/users?no_invite=yes",
|
||||||
body: { name, email },
|
body: { name, email },
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
}).then(xhr => {
|
}).then((xhr) => {
|
||||||
const { status, body } = xhr;
|
const { status, body } = xhr;
|
||||||
if (status < 200 || status > 400) {
|
if (status < 200 || status > 400) {
|
||||||
throw new Error(xhr);
|
throw new Error(xhr);
|
||||||
@@ -146,7 +147,7 @@ Cypress.Commands.add("getDestinations", () => {
|
|||||||
Cypress.Commands.add("addDestinationSubscription", (alertId, destinationName) => {
|
Cypress.Commands.add("addDestinationSubscription", (alertId, destinationName) => {
|
||||||
return cy
|
return cy
|
||||||
.getDestinations()
|
.getDestinations()
|
||||||
.then(destinations => {
|
.then((destinations) => {
|
||||||
const destination = find(destinations, { name: destinationName });
|
const destination = find(destinations, { name: destinationName });
|
||||||
if (!destination) {
|
if (!destination) {
|
||||||
throw new Error("Destination not found");
|
throw new Error("Destination not found");
|
||||||
@@ -166,6 +167,6 @@ Cypress.Commands.add("addDestinationSubscription", (alertId, destinationName) =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add("updateOrgSettings", settings => {
|
Cypress.Commands.add("updateOrgSettings", (settings) => {
|
||||||
return post({ url: "api/settings/organization", body: settings }).then(({ body }) => body);
|
return post({ url: "api/settings/organization", body: settings }).then(({ body }) => body);
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user