Compare commits

...

56 Commits

Author SHA1 Message Date
Elad Ossadon
521ca6afa4 8 2020-12-16 08:30:47 -08:00
Elad Ossadon
1ba94d30a1 7 2020-12-16 08:25:37 -08:00
Elad Ossadon
e4d2c82338 6 2020-12-16 08:25:36 -08:00
Elad Ossadon
95621a93bc 5 2020-12-16 08:25:06 -08:00
Elad Ossadon
2f1ed63bd5 4 2020-12-16 08:25:05 -08:00
Elad Ossadon
f23f1d1924 3 2020-12-16 08:24:36 -08:00
Elad Ossadon
c426379bef [ts-migrate][client] Run TS Migrate
Co-authored-by: ts-migrate <>
2020-12-16 08:24:26 -08:00
Elad Ossadon
501ca0bef8 [ts-migrate][client] Rename files from JS/JSX to TS/TSX
Co-authored-by: ts-migrate <>
2020-12-16 08:23:08 -08:00
Elad Ossadon
4c385f85f1 2 2020-12-16 08:22:53 -08:00
Elad Ossadon
698d87ed48 ts-migrate pkg 2020-12-15 20:59:50 -08:00
Elad Ossadon
c290864ccd Convert viz-lib to TypeScript (#5310)
Co-authored-by: ts-migrate <>
2020-12-15 18:21:37 -08:00
Rafael Wendel
b70e95a323 added eslint no-console (#5305)
* added eslint no-console

* Update client/.eslintrc.js to allow warnings

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
2020-12-14 10:09:43 -03:00
Elad Ossadon
18ee5343aa Sync date format from settings with clientConfig (#5299) 2020-12-10 11:16:31 -08:00
Elad Ossadon
fdf636a393 Fix disabled hot reload flow (#5306) 2020-12-07 16:02:52 -08:00
Rafael Wendel
88c13868a3 removed leftover console.log (#5303) 2020-12-07 17:21:40 -03:00
Elad Ossadon
aab11dc79b Add React Fast Refresh + Hot Module Reloading (#5291) 2020-12-07 11:46:46 -08:00
Elad Ossadon
00c77cf36e Redesign desktop nav bar (#5294) 2020-12-06 12:09:19 -08:00
Rafael Wendel
6e2631dec2 Changed 'Delete Alert' into 'Delete' for consistency (#5287) 2020-11-30 18:48:35 -03:00
Rafael Wendel
4b88959341 Fix QuerySourceDropdown value type (#5284) 2020-11-24 11:42:20 -03:00
Rafael Wendel
fa2b57a209 Remove unwanted props from Select component (#5277)
* Explicitly selected props so as to avoid errors from non-wanted props

* Simplified approach

* Ran prettier 😬

* Fixed minor issues
2020-11-22 13:07:56 -03:00
Jiajie Zhong
132fed64b3 Correct cleanup_query_results comment (#5276)
Correct comment from QUERY_RESULTS_MAX_AGE
to QUERY_RESULTS_CLEANUP_MAX_AGE
2020-11-20 23:11:13 +02:00
Gabriel Dutra
fa7ecca485 Frontend updates from internal fork (#5259)
* DynamicComponent for QuerySourceAlerts

* General Settings updates

* Dynamic Date[Range] updates

* EmptyState updates

* Query and SchemaBrowser updates

* Adjust page headers and add disablePublish

* Policy updates

* Separate Home FavoritesList component

* Update FormatQuery

* Autolimit frontend fixes

* Misc updates

* Keep registering of QuerySourceDropdown

* Undo changes in DynamicComponent

* Change sql-formatter package.json syntax

* Allow opening help trigger in new tab

* Don't run npm commands as root in Dockerfile

* Cypress: Remove extra execute query
2020-11-10 14:59:15 +02:00
deecay
8f484706b1 Enable Boxplot to be horizontal (#5262) 2020-11-08 23:17:08 +02:00
Josh Bohde
e2e8714155 Enable graceful shutdown of rq workers (#5214)
* Enable graceful shutdown of rq workers

* Use `exec` in the `worker` command of the entrypoint to propagate
  the `TERM` signal
* Allow rq processes managed by supervisor to exit without restart on
  expected status codes
* Allow supervisorctl to contact the running supervisor
* Add a `shutdown_worker` command that will send `TERM` to all running
  worker processes and then sleep. This allows orchestration systems
  to initiate a graceful shutdown before sending `SIGTERM` to
  supervisord

* Use Heroku worker as the BaseWorker

This implements a graceful shutdown on SIGTERM, which simplifies
external shutdown procedures.

* Fix imports based upon review

* Remove supervisorctl config
2020-11-05 11:49:45 +02:00
Jerry
c6bf8a1c55 bugfix: fix #5254 (#5255)
Co-authored-by: Jerry <jerry.yuan@webweye.com>
2020-11-04 20:56:41 +02:00
Rafael Wendel
12f71925c2 Multiselect dropdown slowness (fix) (#5221)
* created util to estimate reasonable width for dropdown

* removed unused import

* improved calculation of item percentile

* added getItemOfPercentileLength to relevant spots

* added getItemOfPercentileLength to relevant spots

* Added missing import

* created custom select element

* added check for property path

* removed uses of percentile util

* gave up on getting element reference

* finished testing Select component

* removed unused imports

* removed older uses of Option component

* added canvas calculation

* removed minWidth from Select

* improved calculation

* added fallbacks

* added estimated offset

* removed leftovers 😅

* replaced to percentiles to max value

* switched to memo and renamed component

* proper useMemo syntax

* Update client/app/components/Select.tsx

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>

* created custom restrictive types

* added quick const

* fixed style

* fixed generics

* added pos absolute to fix percy

* removed custom select from ParameterMappingInput

* applied prettier

* Revert "added pos absolute to fix percy"

This reverts commit 4daf1d4bef.

* Pin Percy version to 0.24.3

* Update client/app/components/ParameterMappingInput.jsx

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>

* renamed Select.jsx to SelectWithVirtualScroll

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
2020-11-03 21:50:39 +02:00
Omer Lachish
cae088f35b extend the refresh_queries timeout from 3 minutes to 10 minutes (#5253) 2020-11-02 22:36:57 +02:00
Rafael Wendel
a3c79f26b9 Fix for the typo button in ParameterMappingInput (#5244) 2020-10-29 17:24:13 -03:00
Jonathan Hult
c7c92a3192 Fix annotation bug causing queries not to run - ORA-00933 (#5179) 2020-10-28 10:03:26 +02:00
Rafael Wendel
55cf17aa47 added required to Form.Item and Input for better UI (#5231)
* added required to Form.Item and Input for better UI

* removed required from input

* Revert "removed required from input"

This reverts commit b56cd76fa1.

* Redo "removed required from input"

* removed typo

Co-authored-by: rafawendel2010@gmail.com <rafawendel>
2020-10-28 09:37:16 +02:00
Levko Kravets
8dd76a00c5 Fix dashboard background grid (#5238) 2020-10-26 21:46:38 +02:00
Christopher Grant
e242ac2b10 Static SAML configuration and assertion encryption (#5175)
* Change front-end and data model for SAML2 auth - static configuration

* Add changes to use inline metadata.

* add switch for static and dynamic SAML configurations

* Fixed config of backend static/dynamic to match UI

* add ability to encrypt/decrypt SAML assertions with pem and crt files. Upgraded to pysaml2 6.1.0 to mitigate signature mismatch during decryption

* remove print debug statement

* Use utility to find xmlsec binary for encryption, formatting saml_auth module

* format SAML Javascript, revert want_signed_response to pre-PR value

* pysaml2's entityid should point to the sp, not the idp

* add logging for entityid for validation

* use mustache_render instead of string formatting. put all static logic into static branch

* move mustache template for inline saml metadata to the global level

* Incorporate SAML type with Enabled setting

* Update client/app/pages/settings/components/AuthSettings/SAMLSettings.jsx

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>

Co-authored-by: Chad Chen <chad.chen@databricks.com>
Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
2020-10-25 12:06:45 -03:00
Gabriel Dutra
66463aedd4 Fix Home EmptyState help link (#5217) 2020-10-16 11:53:21 -03:00
Rafael Wendel
8a6524c1ba Add horizontal bar chart (#5154)
* added bar chart boilerplate

* added x/y manipulation

* replaced x/y management to inner series preparer

* added tests

* moved axis inversion to all charts series

* removed line and area

* inverted labels ui

* removed normalizer check, simplified inverted axes check

* finished working hbar

* minor review

* added conditional title to YAxis

* generalized horizontal chart for line charts, resetted state on globalSeriesType change

* fixed updates

* fixed updates to layout

* fixed minor issues

* removed right Y axis when axes inverted

* ran prettier

* fixed updater function conflict and misuse of getOptions

* renamed inverted to swapped

* created mappingtypes for swapped columns

* removed unused import

* minor polishing

* improved series behaviour in h-bar

* minor fix

* added basic filter to ChartTypeSelect

* final setup of filtered chart types

* Update viz-lib/src/components/visualizations/editor/createTabbedEditor.jsx

* added proptypes and renamed ChartTypeSelect props

* Add missing import

* fixed import, moved result array to global scope

* merged import

* clearer naming in ChartTypeSelect

* better lodash map syntax

* fixed global modification

* moved result inside useMemo

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
Co-authored-by: Levko Kravets <levko.ne@gmail.com>
2020-10-15 21:34:38 +03:00
Gabriel Dutra
9097feb100 Frontend updates from internal fork (#5209) 2020-10-15 14:25:22 -03:00
Gabriel Dutra
db4e97fa6f Remove build args from Cypress start script (#5203) 2020-10-09 12:23:14 -03:00
Levko Kravets
0d4615a482 Extra actions on Queries and Dashboards pages (#5201)
* Extra actions for Query View and Query Source pages

* Convert Queries List page to functional component

* Convert Dashboards List page to functional component

* Extra actions for Query List page

* Extra actions for Dashboard List page

* Extra actions for Dashboard page

* Pass some extra data to Dashboard.HeaderExtra component

* CR1
2020-10-09 12:12:56 +03:00
Alexander Rusanov
ff008a076b Updated Cypress to v5.3 and fixed e2e tests (#5199)
* Upgraded Cypress to v5.3 and fixed e2e tests

* Updated cypress image

* Fixed failing tests

* Updated NODE_VERSION in netlify

* Update client/cypress/integration/visualizations/choropleth_spec.js

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>

* fixed test in choropleth

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
2020-10-06 16:06:47 -03:00
Gabriel Dutra
8d548ecbac Share Embed Spec: Make sure query is executed (#5191) 2020-10-04 16:01:30 +03:00
Gabriel Dutra
2992c382d1 ScheduleDialog: Filter empty interval groups (#5196) 2020-10-03 05:54:05 +03:00
Gabriel Dutra
f4dcb2918a Move Cypress to dev dependencies (#3991)
* Test Cypress on package list

* Skip Puppeteer Chromium as well

* Put back missing npm install on netlify.toml

* Netlify: move env vars to build.environment

* Remove cypress:install script

* Update Cypress dockerfile

* Copy package-lock.json to Cypress dockerfile
2020-09-29 09:51:28 +03:00
Gabriel Dutra
c821cab4cb Generate Code Coverage report for Cypress (#5137) 2020-09-28 21:43:04 -03:00
Levko Kravets
4fb77867b0 Align Y axes at zero (#5053)
* Align Y axes as zero

* Fix typo (with @deecay)

* Add alignYAxesAtZero function

* Avoid 0 division

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
2020-09-28 13:12:40 +03:00
Levko Kravets
a473611cb0 Some Choropleth improvements/refactoring (#5186)
* Directly map query results column to GeoJSON property

* Use cache for geoJson requests

* Don't handle bounds changes while loading geoJson data

* Choropleth: fix map "jumping" on load; don't save bounds if user didn't edit them; refine code a bit

* Improve cache

* Optimize Japan Perfectures map (remove irrelevant GeoJson properties)

* Improve getOptions for Choropleth; remove unused code

* Fix test

* Add US states map

* Convert USA map to Albers projection

* Allow to specify user-friendly field names for maps
2020-09-24 14:39:09 +03:00
Levko Kravets
210008c714 Ask user to log in when session expires (#5178)
* Ask user to log in when session expires

* Update implementation

* Update implementation

* Minor fix

* Update modal

* Do not intercept calls to api/session as Auth.requireSession() relies on it

* Refine code; adjust popup size and position
2020-09-23 16:30:08 +03:00
Omer Lachish
aa5d4f5f4e add 'cancelled' meta directive to all cancelled jobs (#5187) 2020-09-23 12:54:48 +03:00
Omer Lachish
6b811c5245 Refresh CSRF tokens (#5177)
* expire CSRF tokens after 6 hours

* use axios' built-in cookie to header copy mechanism

* add axios-auth-refresh

* retry CSRF-related 400 errors by refreshing the cookie

* export the auth refresh interceptor to support ejecting it if neccessary

* reject the original request if it's unrelated to CSRF
2020-09-21 23:21:14 +03:00
Levko Kravets
83726da48a Keep additional URL params when forking a query (#5184) 2020-09-21 12:54:55 +03:00
Levko Kravets
72dc157bbe Allow to clear selected tags on list pages (#5142)
* Convert TagsList to functional component

* Convert TagsList to typescript

* Allow to unselect all tags

* Add title to Tags block and explicit "clear filter" button

* Some tweaks
2020-09-17 14:01:15 +03:00
Lingkai Kong
1b8ff8e810 Add default limit (1000) to SQL queries (#5088)
* add default limit 1000

* Add frontend changes and connect to backend

* Fix query hash because of default limit

* fix CircleCI test

* adjust for comment
2020-09-14 14:18:31 +03:00
Omer Lachish
31ddd0fb79 prevent assigning queries to view_only data sources (#5152) 2020-09-10 15:43:25 +03:00
Levko Kravets
5cabf7a724 Keep selected filters when switching visualizations (#5146)
* getredash/redash#4944 Query pages: keep selected filters when switching visualizations

* Pass current filters to expanded widget modal
2020-09-10 13:42:53 +03:00
max-voronov
59b135ace7 Move CardsList to typescript (#5136)
* Refactor CardsList - pass a suffix for list item

Adding :id to an item to be used as a key suffix is redundant and the same
can be accomplished by using :index from the map function.

* Move CardsList to typescript

* Convert CardsList component to functional component

* CR1

* CR2
2020-09-05 20:08:01 +03:00
Gabriel Dutra
32b41e4112 Misc frontend changes from internal fork (#5143) 2020-09-04 08:00:30 -03:00
Gabriel Dutra
2e31b91054 Antd v4: Fix CreateUserDialog (#5139)
* Antd v4: Update CreateUserDialog

* Add Cypress test for user creation
2020-09-04 07:57:43 -03:00
Gabriel Dutra
205915e6db Add toggle to disable public URLs (#5140)
* Add toggle to disable public URLs

* Add Cypress tests
2020-09-01 08:49:30 -03:00
639 changed files with 26107 additions and 9769 deletions

View File

@@ -1,12 +1,12 @@
FROM cypress/browsers:chrome67
FROM cypress/browsers:node14.0.0-chrome84
ENV APP /usr/src/app
WORKDIR $APP
COPY package.json $APP/package.json
RUN npm run cypress:install > /dev/null
COPY package.json package-lock.json $APP/
COPY viz-lib $APP/viz-lib
RUN npm ci > /dev/null
COPY client/cypress $APP/client/cypress
COPY cypress.json $APP/cypress.json
COPY . $APP
RUN ./node_modules/.bin/cypress verify

View File

@@ -57,6 +57,9 @@ jobs:
- store_artifacts:
path: coverage.xml
frontend-lint:
environment:
CYPRESS_INSTALL_BINARY: 0
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
docker:
- image: circleci/node:12
steps:
@@ -67,6 +70,9 @@ jobs:
- store_test_results:
path: /tmp/test-results
frontend-unit-tests:
environment:
CYPRESS_INSTALL_BINARY: 0
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
docker:
- image: circleci/node:12
steps:
@@ -90,11 +96,20 @@ jobs:
PERCY_TOKEN_ENCODED: ZGRiY2ZmZDQ0OTdjMzM5ZWE0ZGQzNTZiOWNkMDRjOTk4Zjg0ZjMxMWRmMDZiM2RjOTYxNDZhOGExMjI4ZDE3MA==
CYPRESS_PROJECT_ID_ENCODED: OTI0Y2th
CYPRESS_RECORD_KEY_ENCODED: YzA1OTIxMTUtYTA1Yy00NzQ2LWEyMDMtZmZjMDgwZGI2ODgx
CYPRESS_INSTALL_BINARY: 0
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
docker:
- image: circleci/node:12
steps:
- setup_remote_docker
- checkout
- run:
name: Enable Code Coverage report for master branch
command: |
if [ "$CIRCLE_BRANCH" = "master" ]; then
echo 'export CODE_COVERAGE=true' >> $BASH_ENV
source $BASH_ENV
fi
- run:
name: Install npm dependencies
command: |
@@ -113,6 +128,13 @@ jobs:
command: |
docker-compose logs
when: on_fail
- run:
name: Copy Code Coverage results
command: |
docker cp cypress:/usr/src/app/coverage ./coverage || true
when: always
- store_artifacts:
path: coverage
build-docker-image: *build-docker-image-job
build-preview-docker-image: *build-docker-image-job
workflows:

View File

@@ -1,7 +1,20 @@
version: '2.2'
version: "2.2"
x-redash-service: &redash-service
build:
context: ../
args:
skip_dev_deps: "true"
skip_ds_deps: "true"
code_coverage: ${CODE_COVERAGE}
x-redash-environment: &redash-environment
REDASH_LOG_LEVEL: "INFO"
REDASH_REDIS_URL: "redis://redis:6379/0"
REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
REDASH_RATELIMIT_ENABLED: "false"
REDASH_ENFORCE_CSRF: "true"
services:
server:
build: ../
<<: *redash-service
command: server
depends_on:
- postgres
@@ -9,30 +22,25 @@ services:
ports:
- "5000:5000"
environment:
<<: *redash-environment
PYTHONUNBUFFERED: 0
REDASH_LOG_LEVEL: "INFO"
REDASH_REDIS_URL: "redis://redis:6379/0"
REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
REDASH_RATELIMIT_ENABLED: "false"
REDASH_ENFORCE_CSRF: "true"
scheduler:
build: ../
<<: *redash-service
command: scheduler
depends_on:
- server
environment:
REDASH_REDIS_URL: "redis://redis:6379/0"
<<: *redash-environment
worker:
build: ../
<<: *redash-service
command: worker
depends_on:
- server
environment:
<<: *redash-environment
PYTHONUNBUFFERED: 0
REDASH_LOG_LEVEL: "INFO"
REDASH_REDIS_URL: "redis://redis:6379/0"
REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
cypress:
ipc: host
build:
context: ../
dockerfile: .circleci/Dockerfile.cypress
@@ -42,6 +50,7 @@ services:
- scheduler
environment:
CYPRESS_baseUrl: "http://server:5000"
CYPRESS_coverage: ${CODE_COVERAGE}
PERCY_TOKEN: ${PERCY_TOKEN}
PERCY_BRANCH: ${CIRCLE_BRANCH}
PERCY_COMMIT: ${CIRCLE_SHA1}

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@ venv/
.coveralls.yml
.idea
*.pyc
.nyc_output
coverage
.coverage
coverage.xml
client/dist

View File

@@ -3,13 +3,24 @@ FROM node:12 as frontend-builder
# Controls whether to build the frontend assets
ARG skip_frontend_build
ENV CYPRESS_INSTALL_BINARY=0
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
RUN useradd -m -d /frontend redash
USER redash
WORKDIR /frontend
COPY package.json package-lock.json /frontend/
COPY viz-lib /frontend/viz-lib
COPY --chown=redash package.json package-lock.json /frontend/
COPY --chown=redash viz-lib /frontend/viz-lib
# Controls whether to instrument code for coverage information
ARG code_coverage
ENV BABEL_ENV=${code_coverage:+test}
RUN if [ "x$skip_frontend_build" = "x" ] ; then npm ci --unsafe-perm; fi
COPY client /frontend/client
COPY webpack.config.js /frontend/
COPY --chown=redash client /frontend/client
COPY --chown=redash webpack.config.js /frontend/
RUN if [ "x$skip_frontend_build" = "x" ] ; then npm run build; else mkdir -p /frontend/client/dist && touch /frontend/client/dist/multi_org.html && touch /frontend/client/dist/index.html; fi
FROM python:3.7-slim

View File

@@ -35,7 +35,7 @@ backend-unit-tests: up test_db
docker-compose run --rm --name tests server tests
frontend-unit-tests: bundle
npm ci
CYPRESS_INSTALL_BINARY=0 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 npm ci
npm run bundle
npm test

View File

@@ -18,8 +18,8 @@ worker() {
export WORKERS_COUNT=${WORKERS_COUNT:-2}
export QUEUES=${QUEUES:-}
supervisord -c worker.conf
exec supervisord -c worker.conf
}
dev_worker() {

View File

@@ -20,5 +20,10 @@
"globals": ["Error"]
}
]
]
],
"env": {
"test": {
"plugins": ["istanbul"]
}
}
}

View File

@@ -20,6 +20,22 @@ module.exports = {
// allow debugger during development
"no-debugger": process.env.NODE_ENV === "production" ? 2 : 0,
"jsx-a11y/anchor-is-valid": "off",
"no-console": ["warn", { allow: ["warn", "error"] }],
"no-restricted-imports": [
"error",
{
paths: [
{
name: "antd",
message: "Please use 'import XXX from antd/lib/XXX' import instead.",
},
{
name: "antd/lib",
message: "Please use 'import XXX from antd/lib/XXX' import instead.",
},
],
},
],
},
overrides: [
{
@@ -34,6 +50,8 @@ module.exports = {
// Do not complain about useless contructors in declaration files
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": "error",
// Many API fields and generated types use camelcase
"@typescript-eslint/camelcase": "off","@typescript-eslint/no-empty-function": "off",
},
},
],

View File

@@ -1,4 +0,0 @@
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() });

View File

@@ -0,0 +1,5 @@
import { configure } from "enzyme";
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'enzy... Remove this comment to see the full error message
import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -141,6 +141,7 @@ a.label-tag {
display: flex;
flex-direction: column;
flex-grow: 1;
position: relative;
}
.query-fullscreen {

View File

@@ -3,7 +3,7 @@ import AceEditor from "react-ace";
import "./AceEditorInput.less";
function AceEditorInput(props, ref) {
function AceEditorInput(props: any, ref: any) {
return (
<div className="ace-editor-input" data-test={props["data-test"]}>
<AceEditor

View File

@@ -1,12 +1,17 @@
@backgroundColor: #001529;
@dividerColor: rgba(255, 255, 255, 0.5);
@textColor: rgba(255, 255, 255, 0.75);
@brandColor: #ff7964; // Redash logo color
@activeItemColor: @brandColor;
@iconSize: 26px;
.desktop-navbar {
background: @backgroundColor;
display: flex;
flex-direction: column;
height: 100%;
width: 80px;
overflow: hidden;
&-spacer {
flex: 1 1 auto;
@@ -21,12 +26,6 @@
height: 40px;
transition: all 270ms;
}
&.ant-menu-inline-collapsed {
img {
height: 20px;
}
}
}
.help-trigger {
@@ -34,26 +33,19 @@
}
.ant-menu {
&:not(.ant-menu-inline-collapsed) {
width: 170px;
}
&.ant-menu-inline-collapsed > .ant-menu-submenu-title span img + span,
&.ant-menu-inline-collapsed > .ant-menu-item i + span {
display: inline-block;
max-width: 0;
opacity: 0;
}
.ant-menu-item-divider {
background: @dividerColor;
}
.ant-menu-item,
.ant-menu-submenu {
font-weight: 500;
color: @textColor;
&.navbar-active-item {
box-shadow: inset 3px 0 0 @activeItemColor;
.anticon {
color: @activeItemColor;
}
}
&.ant-menu-submenu-open,
&.ant-menu-submenu-active,
&:hover,
@@ -61,6 +53,16 @@
color: #fff;
}
.anticon {
font-size: @iconSize;
margin: 0;
}
.desktop-navbar-label {
margin-top: 4px;
font-size: 11px;
}
a,
span,
.anticon {
@@ -71,21 +73,33 @@
.ant-menu-submenu-arrow {
display: none;
}
}
.ant-btn.desktop-navbar-collapse-button {
background-color: @backgroundColor;
border: 0;
border-radius: 0;
color: @textColor;
&:hover,
&:active {
color: #fff;
.ant-menu-item,
.ant-menu-submenu {
padding: 0;
height: 60px;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
}
&:after {
animation: 0s !important;
.ant-menu-submenu-title {
width: 100%;
padding: 0;
}
a,
&.ant-menu-vertical > .ant-menu-submenu > .ant-menu-submenu-title,
.ant-menu-submenu-title {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: normal;
height: auto;
background: none;
color: inherit;
}
}
@@ -99,37 +113,8 @@
.profile__image_thumb {
margin: 0;
vertical-align: middle;
}
.profile__image_thumb + span {
flex: 1 1 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-left: 10px;
vertical-align: middle;
display: inline-block;
// styles from Antd
opacity: 1;
transition: opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
margin-left 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
}
}
&.ant-menu-inline-collapsed {
.ant-menu-submenu-title {
padding-left: 16px !important;
padding-right: 16px !important;
}
.desktop-navbar-profile-menu-title {
.profile__image_thumb + span {
opacity: 0;
max-width: 0;
margin-left: 0;
}
width: @iconSize;
height: @iconSize;
}
}
}

View File

@@ -1,12 +1,13 @@
import { first } from "lodash";
import React, { useState } from "react";
import Button from "antd/lib/button";
import React, { useMemo } from "react";
import { first, includes } from "lodash";
import Menu from "antd/lib/menu";
import Link from "@/components/Link";
import HelpTrigger from "@/components/HelpTrigger";
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
import { useCurrentRoute } from "@/components/ApplicationArea/Router";
import { Auth, currentUser } from "@/services/auth";
import settingsMenu from "@/services/settingsMenu";
// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module '@/assets/images/redash_icon_sm... Remove this comment to see the full error message
import logoUrl from "@/assets/images/redash_icon_small.png";
import DesktopOutlinedIcon from "@ant-design/icons/DesktopOutlined";
@@ -15,37 +16,66 @@ import AlertOutlinedIcon from "@ant-design/icons/AlertOutlined";
import PlusOutlinedIcon from "@ant-design/icons/PlusOutlined";
import QuestionCircleOutlinedIcon from "@ant-design/icons/QuestionCircleOutlined";
import SettingOutlinedIcon from "@ant-design/icons/SettingOutlined";
import MenuUnfoldOutlinedIcon from "@ant-design/icons/MenuUnfoldOutlined";
import MenuFoldOutlinedIcon from "@ant-design/icons/MenuFoldOutlined";
import VersionInfo from "./VersionInfo";
import "./DesktopNavbar.less";
function NavbarSection({ inlineCollapsed, children, ...props }) {
function NavbarSection({
children,
...props
}: any) {
return (
<Menu
selectable={false}
mode={inlineCollapsed ? "inline" : "vertical"}
inlineCollapsed={inlineCollapsed}
theme="dark"
{...props}>
<Menu selectable={false} mode="vertical" theme="dark" {...props}>
{children}
</Menu>
);
}
export default function DesktopNavbar() {
const [collapsed, setCollapsed] = useState(true);
function useNavbarActiveState() {
const currentRoute = useCurrentRoute();
return useMemo(
() => ({
dashboards: includes(
["Dashboards.List", "Dashboards.Favorites", "Dashboards.ViewOrEdit", "Dashboards.LegacyViewOrEdit"],
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
currentRoute.id
),
queries: includes(
[
"Queries.List",
"Queries.Favorites",
"Queries.Archived",
"Queries.My",
"Queries.View",
"Queries.New",
"Queries.Edit",
],
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
currentRoute.id
),
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
dataSources: includes(["DataSources.List"], currentRoute.id),
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
alerts: includes(["Alerts.List", "Alerts.New", "Alerts.View", "Alerts.Edit"], currentRoute.id),
}),
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
[currentRoute.id]
);
}
export default function DesktopNavbar() {
const firstSettingsTab = first(settingsMenu.getAvailableItems());
const activeState = useNavbarActiveState();
const canCreateQuery = currentUser.hasPermission("create_query");
const canCreateDashboard = currentUser.hasPermission("create_dashboard");
const canCreateAlert = currentUser.hasPermission("list_alerts");
return (
<div className="desktop-navbar">
<NavbarSection inlineCollapsed={collapsed} className="desktop-navbar-logo">
<NavbarSection className="desktop-navbar-logo">
<div>
<Link href="./">
<img src={logoUrl} alt="Redash" />
@@ -53,45 +83,46 @@ export default function DesktopNavbar() {
</div>
</NavbarSection>
<NavbarSection inlineCollapsed={collapsed}>
<NavbarSection>
{currentUser.hasPermission("list_dashboards") && (
<Menu.Item key="dashboards">
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<Menu.Item key="dashboards" className={activeState.dashboards ? "navbar-active-item" : null}>
<Link href="dashboards">
<DesktopOutlinedIcon />
<span>Dashboards</span>
<span className="desktop-navbar-label">Dashboards</span>
</Link>
</Menu.Item>
)}
{currentUser.hasPermission("view_query") && (
<Menu.Item key="queries">
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<Menu.Item key="queries" className={activeState.queries ? "navbar-active-item" : null}>
<Link href="queries">
<CodeOutlinedIcon />
<span>Queries</span>
<span className="desktop-navbar-label">Queries</span>
</Link>
</Menu.Item>
)}
{currentUser.hasPermission("list_alerts") && (
<Menu.Item key="alerts">
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<Menu.Item key="alerts" className={activeState.alerts ? "navbar-active-item" : null}>
<Link href="alerts">
<AlertOutlinedIcon />
<span>Alerts</span>
<span className="desktop-navbar-label">Alerts</span>
</Link>
</Menu.Item>
)}
</NavbarSection>
<NavbarSection inlineCollapsed={collapsed} className="desktop-navbar-spacer">
{(canCreateQuery || canCreateDashboard || canCreateAlert) && <Menu.Divider />}
<NavbarSection className="desktop-navbar-spacer">
{(canCreateQuery || canCreateDashboard || canCreateAlert) && (
<Menu.SubMenu
key="create"
popupClassName="desktop-navbar-submenu"
data-test="CreateButton"
title={
<React.Fragment>
<span data-test="CreateButton">
<PlusOutlinedIcon />
<span>Create</span>
</span>
<PlusOutlinedIcon />
<span className="desktop-navbar-label">Create</span>
</React.Fragment>
}>
{canCreateQuery && (
@@ -103,6 +134,7 @@ export default function DesktopNavbar() {
)}
{canCreateDashboard && (
<Menu.Item key="new-dashboard">
{/* @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 0. */}
<a data-test="CreateDashboardMenuItem" onMouseUp={() => CreateDashboardDialog.showModal()}>
New Dashboard
</a>
@@ -119,32 +151,34 @@ export default function DesktopNavbar() {
)}
</NavbarSection>
<NavbarSection inlineCollapsed={collapsed}>
<NavbarSection>
<Menu.Item key="help">
{/* @ts-expect-error ts-migrate(2746) FIXME: This JSX tag's 'children' prop expects a single ch... Remove this comment to see the full error message */}
<HelpTrigger showTooltip={false} type="HOME">
<QuestionCircleOutlinedIcon />
<span>Help</span>
<span className="desktop-navbar-label">Help</span>
</HelpTrigger>
</Menu.Item>
{firstSettingsTab && (
<Menu.Item key="settings">
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<Menu.Item key="settings" className={activeState.dataSources ? "navbar-active-item" : null}>
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'path' does not exist on type 'number | (... Remove this comment to see the full error message */}
<Link href={firstSettingsTab.path} data-test="SettingsLink">
<SettingOutlinedIcon />
<span>Settings</span>
<span className="desktop-navbar-label">Settings</span>
</Link>
</Menu.Item>
)}
<Menu.Divider />
</NavbarSection>
<NavbarSection inlineCollapsed={collapsed} className="desktop-navbar-profile-menu">
<NavbarSection className="desktop-navbar-profile-menu">
<Menu.SubMenu
key="profile"
popupClassName="desktop-navbar-submenu"
title={
<span data-test="ProfileDropdown" className="desktop-navbar-profile-menu-title">
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'profile_image_url' does not exist on typ... Remove this comment to see the full error message */}
<img className="profile__image_thumb" src={currentUser.profile_image_url} alt={currentUser.name} />
<span>{currentUser.name}</span>
</span>
}>
<Menu.Item key="profile">
@@ -167,10 +201,6 @@ export default function DesktopNavbar() {
</Menu.Item>
</Menu.SubMenu>
</NavbarSection>
<Button onClick={() => setCollapsed(!collapsed)} className="desktop-navbar-collapse-button">
{collapsed ? <MenuUnfoldOutlinedIcon /> : <MenuFoldOutlinedIcon />}
</Button>
</div>
);
}

View File

@@ -1,6 +1,5 @@
import { first } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import MenuOutlinedIcon from "@ant-design/icons/MenuOutlined";
import Dropdown from "antd/lib/dropdown";
@@ -8,11 +7,18 @@ import Menu from "antd/lib/menu";
import Link from "@/components/Link";
import { Auth, currentUser } from "@/services/auth";
import settingsMenu from "@/services/settingsMenu";
// @ts-expect-error ts-migrate(2307) FIXME: Cannot find module '@/assets/images/redash_icon_sm... Remove this comment to see the full error message
import logoUrl from "@/assets/images/redash_icon_small.png";
import "./MobileNavbar.less";
export default function MobileNavbar({ getPopupContainer }) {
type OwnProps = {
getPopupContainer?: (...args: any[]) => any;
};
type Props = OwnProps & typeof MobileNavbar.defaultProps;
export default function MobileNavbar({ getPopupContainer }: Props) {
const firstSettingsTab = first(settingsMenu.getAvailableItems());
return (
@@ -50,6 +56,7 @@ export default function MobileNavbar({ getPopupContainer }) {
<Menu.Divider />
{firstSettingsTab && (
<Menu.Item key="settings">
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'path' does not exist on type 'number | (... Remove this comment to see the full error message */}
<Link href={firstSettingsTab.path}>Settings</Link>
</Menu.Item>
)}
@@ -79,10 +86,6 @@ export default function MobileNavbar({ getPopupContainer }) {
);
}
MobileNavbar.propTypes = {
getPopupContainer: PropTypes.func,
};
MobileNavbar.defaultProps = {
getPopupContainer: null,
};

View File

@@ -1,15 +1,19 @@
import React from "react";
import Link from "@/components/Link";
import { clientConfig, currentUser } from "@/services/auth";
// @ts-expect-error ts-migrate(7042) FIXME: Module '@/version.json' was resolved to '/Users/el... Remove this comment to see the full error message
import frontendVersion from "@/version.json";
export default function VersionInfo() {
return (
<React.Fragment>
<div>
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'version' does not exist on type '{}'. */}
Version: {clientConfig.version}
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'version' does not exist on type '{}'. */}
{frontendVersion !== clientConfig.version && ` (${frontendVersion.substring(0, 8)})`}
</div>
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'newVersionAvailable' does not exist on t... Remove this comment to see the full error message */}
{clientConfig.newVersionAvailable && currentUser.hasPermission("super_admin") && (
<div className="m-t-10">
{/* eslint-disable react/jsx-no-target-blank */}

View File

@@ -1,39 +0,0 @@
import React, { useRef, useCallback } from "react";
import PropTypes from "prop-types";
import DynamicComponent from "@/components/DynamicComponent";
import DesktopNavbar from "./DesktopNavbar";
import MobileNavbar from "./MobileNavbar";
import "./index.less";
export default function ApplicationLayout({ children }) {
const mobileNavbarContainerRef = useRef();
const getMobileNavbarPopupContainer = useCallback(() => mobileNavbarContainerRef.current, []);
return (
<React.Fragment>
<div className="application-layout-side-menu">
<DynamicComponent name="ApplicationDesktopNavbar">
<DesktopNavbar />
</DynamicComponent>
</div>
<div className="application-layout-content">
<nav className="application-layout-top-menu" ref={mobileNavbarContainerRef}>
<DynamicComponent name="ApplicationMobileNavbar" getPopupContainer={getMobileNavbarPopupContainer}>
<MobileNavbar getPopupContainer={getMobileNavbarPopupContainer} />
</DynamicComponent>
</nav>
{children}
</div>
</React.Fragment>
);
}
ApplicationLayout.propTypes = {
children: PropTypes.node,
};
ApplicationLayout.defaultProps = {
children: null,
};

View File

@@ -0,0 +1,46 @@
import React, { useRef, useCallback } from "react";
import DynamicComponent from "@/components/DynamicComponent";
import DesktopNavbar from "./DesktopNavbar";
import MobileNavbar from "./MobileNavbar";
import "./index.less";
type OwnProps = {
children?: React.ReactNode;
};
type Props = OwnProps & typeof ApplicationLayout.defaultProps;
export default function ApplicationLayout({ children }: Props) {
const mobileNavbarContainerRef = useRef();
const getMobileNavbarPopupContainer = useCallback(() => mobileNavbarContainerRef.current, []);
return (
<React.Fragment>
{/* @ts-expect-error ts-migrate(2746) FIXME: This JSX tag's 'children' prop expects a single ch... Remove this comment to see the full error message */}
<DynamicComponent name="ApplicationWrapper">
<div className="application-layout-side-menu">
<DynamicComponent name="ApplicationDesktopNavbar">
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<DesktopNavbar />
</DynamicComponent>
</div>
<div className="application-layout-content">
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'MutableRefObject<undefined>' is not assignab... Remove this comment to see the full error message */}
<nav className="application-layout-top-menu" ref={mobileNavbarContainerRef}>
<DynamicComponent name="ApplicationMobileNavbar" getPopupContainer={getMobileNavbarPopupContainer}>
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<MobileNavbar getPopupContainer={getMobileNavbarPopupContainer} />
</DynamicComponent>
</nav>
{children}
</div>
</DynamicComponent>
</React.Fragment>
);
}
ApplicationLayout.defaultProps = {
children: null,
};

View File

@@ -1,57 +0,0 @@
import { isObject, get } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import "./ErrorMessage.less";
function getErrorMessageByStatus(status, defaultMessage) {
switch (status) {
case 404:
return "It seems like the page you're looking for cannot be found.";
case 401:
case 403:
return "It seems like you dont have permission to see this page.";
default:
return defaultMessage;
}
}
function getErrorMessage(error) {
const message = "It seems like we encountered an error. Try refreshing this page or contact your administrator.";
if (isObject(error)) {
// HTTP errors
if (error.isAxiosError && isObject(error.response)) {
return getErrorMessageByStatus(error.response.status, get(error, "response.data.message", message));
}
// Router errors
if (error.status) {
return getErrorMessageByStatus(error.status, message);
}
}
return message;
}
export default function ErrorMessage({ error }) {
if (!error) {
return null;
}
console.error(error);
return (
<div className="error-message-container" data-test="ErrorMessage">
<div className="error-state bg-white tiled">
<div className="error-state__icon">
<i className="zmdi zmdi-alert-circle-o" />
</div>
<div className="error-state__details">
<h4>{getErrorMessage(error)}</h4>
</div>
</div>
</div>
);
}
ErrorMessage.propTypes = {
error: PropTypes.object.isRequired,
};

View File

@@ -10,7 +10,9 @@ const ErrorMessages = {
function mockAxiosError(status = 500, response = {}) {
const error = new Error(`Failed with code ${status}.`);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isAxiosError' does not exist on type 'Er... Remove this comment to see the full error message
error.isAxiosError = true;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'response' does not exist on type 'Error'... Remove this comment to see the full error message
error.response = { status, ...response };
return error;
}
@@ -22,7 +24,7 @@ describe("Error Message", () => {
spyError.mockReset();
});
function expectErrorMessageToBe(error, errorMessage) {
function expectErrorMessageToBe(error: any, errorMessage: any) {
const component = mount(<ErrorMessage error={error} />);
expect(component.find(".error-state__details h4").text()).toBe(errorMessage);

View File

@@ -0,0 +1,72 @@
import { get, isObject } from "lodash";
import React from "react";
import "./ErrorMessage.less";
import DynamicComponent from "@/components/DynamicComponent";
import { ErrorMessageDetails } from "@/components/ApplicationArea/ErrorMessageDetails";
function getErrorMessageByStatus(status: any, defaultMessage: any) {
switch (status) {
case 404:
return "It seems like the page you're looking for cannot be found.";
case 401:
case 403:
return "It seems like you dont have permission to see this page.";
default:
return defaultMessage;
}
}
function getErrorMessage(error: any) {
const message = "It seems like we encountered an error. Try refreshing this page or contact your administrator.";
if (isObject(error)) {
// HTTP errors
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isAxiosError' does not exist on type 'ob... Remove this comment to see the full error message
if (error.isAxiosError && isObject(error.response)) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'response' does not exist on type 'object... Remove this comment to see the full error message
return getErrorMessageByStatus(error.response.status, get(error, "response.data.message", message));
}
// Router errors
// @ts-expect-error ts-migrate(2339) FIXME: Property 'status' does not exist on type 'object'.
if (error.status) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'status' does not exist on type 'object'.
return getErrorMessageByStatus(error.status, message);
}
}
return message;
}
type Props = {
error: any;
message?: string;
};
export default function ErrorMessage({ error, message }: Props) {
if (!error) {
return null;
}
console.error(error);
const errorDetailsProps = {
error,
message: message || getErrorMessage(error),
};
return (
<div className="error-message-container" data-test="ErrorMessage" role="alert">
<div className="error-state bg-white tiled">
<div className="error-state__icon">
<i className="zmdi zmdi-alert-circle-o" />
</div>
<div className="error-state__details">
<DynamicComponent
name="ErrorMessageDetails"
fallback={<ErrorMessageDetails {...errorDetailsProps} />}
{...errorDetailsProps}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import React from "react";
type Props = {
error: any; // TODO: PropTypes.instanceOf(Error)
message: string;
};
export function ErrorMessageDetails(props: Props) {
return <h4>{props.message}</h4>;
}

View File

@@ -1,6 +1,5 @@
import { isFunction, startsWith, trimStart, trimEnd } from "lodash";
import React, { useState, useEffect, useRef, useContext } from "react";
import PropTypes from "prop-types";
import UniversalRouter from "universal-router";
import ErrorBoundary from "@redash/viz/lib/components/ErrorBoundary";
import location from "@/services/location";
@@ -20,7 +19,7 @@ export function useCurrentRoute() {
return useContext(CurrentRouteContext);
}
export function stripBase(href) {
export function stripBase(href: any) {
// Resolve provided link and '' (root) relative to document's base.
// If provided href is not related to current document (does not
// start with resolved root) - return false. Otherwise
@@ -36,7 +35,20 @@ export function stripBase(href) {
return false;
}
export default function Router({ routes, onRouteChange }) {
type OwnProps = {
routes?: {
path: string;
render?: (...args: any[]) => any;
resolve?: {
[key: string]: any;
};
}[];
onRouteChange?: (...args: any[]) => any;
};
type Props = OwnProps & typeof Router.defaultProps;
export default function Router({ routes, onRouteChange }: Props) {
const [currentRoute, setCurrentRoute] = useState(null);
const currentPathRef = useRef(null);
@@ -47,15 +59,17 @@ export default function Router({ routes, onRouteChange }) {
const router = new UniversalRouter(routes, {
resolveRoute({ route }, routeParams) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'render' does not exist on type 'Route<Co... Remove this comment to see the full error message
if (isFunction(route.render)) {
return { ...route, routeParams };
}
},
});
function resolve(action) {
function resolve(action: any) {
if (!isAbandoned) {
if (errorHandlerRef.current) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
errorHandlerRef.current.reset();
}
@@ -70,6 +84,7 @@ export default function Router({ routes, onRouteChange }) {
if (pathname === currentPathRef.current) {
return;
}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'null'.
currentPathRef.current = pathname;
// Don't reload controller if URL was replaced
@@ -87,7 +102,8 @@ export default function Router({ routes, onRouteChange }) {
.catch(error => {
if (!isAbandoned && currentPathRef.current === pathname) {
setCurrentRoute({
render: currentRoute => <ErrorMessage {...currentRoute.routeParams} />,
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ render: (currentRoute: any) =>... Remove this comment to see the full error message
render: (currentRoute: any) => <ErrorMessage {...currentRoute.routeParams} />,
routeParams: { error },
});
}
@@ -97,7 +113,7 @@ export default function Router({ routes, onRouteChange }) {
resolve("PUSH");
const unlisten = location.listen((unused, action) => resolve(action));
const unlisten = location.listen((unused: any, action: any) => resolve(action));
return () => {
isAbandoned = true;
@@ -116,29 +132,15 @@ export default function Router({ routes, onRouteChange }) {
return (
<CurrentRouteContext.Provider value={currentRoute}>
<ErrorBoundary ref={errorHandlerRef} renderError={error => <ErrorMessage error={error} />}>
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<ErrorBoundary ref={errorHandlerRef} renderError={(error: any) => <ErrorMessage error={error} />}>
{/* @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. */}
{currentRoute.render(currentRoute)}
</ErrorBoundary>
</CurrentRouteContext.Provider>
);
}
Router.propTypes = {
routes: PropTypes.arrayOf(
PropTypes.shape({
path: PropTypes.string.isRequired,
render: PropTypes.func, // (routeParams: PropTypes.object; currentRoute; location) => PropTypes.node
// Additional props to be injected into route component.
// Object keys are props names. Object values will become prop values:
// - if value is a function - it will be called without arguments, and result will be used; otherwise value will be used;
// - after previous step, if value is a promise - router will wait for it to resolve; resolved value then will be used;
// otherwise value will be used directly.
resolve: PropTypes.objectOf(PropTypes.any),
})
),
onRouteChange: PropTypes.func,
};
Router.defaultProps = {
routes: [],
onRouteChange: () => {},

View File

@@ -1,7 +1,7 @@
import { isString } from "lodash";
import navigateTo from "./navigateTo";
export default function handleNavigationIntent(event) {
export default function handleNavigationIntent(event: any) {
let element = event.target;
while (element) {
if (element.tagName === "A") {
@@ -9,7 +9,7 @@ export default function handleNavigationIntent(event) {
}
element = element.parentNode;
}
if (!element || !element.hasAttribute("href") || element.hasAttribute("download")) {
if (!element || !element.hasAttribute("href") || element.hasAttribute("download") || element.dataset.skipRouter) {
return;
}

View File

@@ -9,13 +9,15 @@ export default function ApplicationArea() {
const [unhandledError, setUnhandledError] = useState(null);
useEffect(() => {
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
if (currentRoute && currentRoute.title) {
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
document.title = currentRoute.title;
}
}, [currentRoute]);
useEffect(() => {
function globalErrorHandler(event) {
function globalErrorHandler(event: any) {
event.preventDefault();
setUnhandledError(event.error);
}
@@ -33,5 +35,6 @@ export default function ApplicationArea() {
return <ErrorMessage error={unhandledError} />;
}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'RouteItem[]' is not assignable to type '{ pa... Remove this comment to see the full error message
return <Router routes={routes.items} onRouteChange={setCurrentRoute} />;
}

View File

@@ -4,7 +4,7 @@ import { stripBase } from "./Router";
// When `replace` is set to `true` - it will just replace current URL
// without reloading current page (router will skip this location change)
export default function navigateTo(href, replace = false) {
export default function navigateTo(href: any, replace = false) {
// Allow calling chain to roll up, and then navigate
setTimeout(() => {
const isExternal = stripBase(href) === false;

View File

@@ -1,7 +1,13 @@
import React, { useEffect, useState, useContext } from "react";
import PropTypes from "prop-types";
import { ErrorBoundaryContext } from "@redash/viz/lib/components/ErrorBoundary";
import { Auth } from "@/services/auth";
import { Auth, clientConfig } from "@/services/auth";
type OwnProps = {
apiKey: string;
renderChildren?: (...args: any[]) => any;
};
type Props = OwnProps & typeof ApiKeySessionWrapper.defaultProps;
// This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object
// that contains:
@@ -10,7 +16,8 @@ import { Auth } from "@/services/auth";
// - `onError` field which is a `handleError` method of nearest error boundary
// - `apiKey` field
function ApiKeySessionWrapper({ apiKey, currentRoute, renderChildren }) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'currentRoute' does not exist on type 'Pr... Remove this comment to see the full error message
function ApiKeySessionWrapper({ apiKey, currentRoute, renderChildren }: Props) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const { handleError } = useContext(ErrorBoundaryContext);
@@ -33,7 +40,8 @@ function ApiKeySessionWrapper({ apiKey, currentRoute, renderChildren }) {
};
}, [apiKey]);
if (!isAuthenticated) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'disablePublicUrls' does not exist on typ... Remove this comment to see the full error message
if (!isAuthenticated || clientConfig.disablePublicUrls) {
return null;
}
@@ -44,20 +52,18 @@ function ApiKeySessionWrapper({ apiKey, currentRoute, renderChildren }) {
);
}
ApiKeySessionWrapper.propTypes = {
apiKey: PropTypes.string.isRequired,
renderChildren: PropTypes.func,
};
ApiKeySessionWrapper.defaultProps = {
renderChildren: () => null,
};
export default function routeWithApiKeySession({ render, getApiKey, ...rest }) {
export default function routeWithApiKeySession({
render,
getApiKey,
...rest
}: any) {
return {
...rest,
render: currentRoute => (
<ApiKeySessionWrapper apiKey={getApiKey(currentRoute)} currentRoute={currentRoute} renderChildren={render} />
),
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ apiKey: any; currentRoute: any; renderChil... Remove this comment to see the full error message
render: (currentRoute: any) => <ApiKeySessionWrapper apiKey={getApiKey(currentRoute)} currentRoute={currentRoute} renderChildren={render} />,
};
}

View File

@@ -1,82 +0,0 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import ErrorBoundary, { ErrorBoundaryContext } from "@redash/viz/lib/components/ErrorBoundary";
import { Auth } from "@/services/auth";
import { policy } from "@/services/policy";
import organizationStatus from "@/services/organizationStatus";
import ApplicationLayout from "./ApplicationLayout";
import ErrorMessage from "./ErrorMessage";
// This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object
// that contains:
// - `currentRoute.routeParams`
// - `pageTitle` field which is equal to `currentRoute.title`
// - `onError` field which is a `handleError` method of nearest error boundary
function UserSessionWrapper({ bodyClass, currentRoute, renderChildren }) {
const [isAuthenticated, setIsAuthenticated] = useState(!!Auth.isAuthenticated());
useEffect(() => {
let isCancelled = false;
Promise.all([Auth.requireSession(), organizationStatus.refresh(), policy.refresh()])
.then(() => {
if (!isCancelled) {
setIsAuthenticated(!!Auth.isAuthenticated());
}
})
.catch(() => {
if (!isCancelled) {
setIsAuthenticated(false);
}
});
return () => {
isCancelled = true;
};
}, []);
useEffect(() => {
if (bodyClass) {
document.body.classList.toggle(bodyClass, true);
return () => {
document.body.classList.toggle(bodyClass, false);
};
}
}, [bodyClass]);
if (!isAuthenticated) {
return null;
}
return (
<ApplicationLayout>
<React.Fragment key={currentRoute.key}>
<ErrorBoundary renderError={error => <ErrorMessage error={error} />}>
<ErrorBoundaryContext.Consumer>
{({ handleError }) =>
renderChildren({ ...currentRoute.routeParams, pageTitle: currentRoute.title, onError: handleError })
}
</ErrorBoundaryContext.Consumer>
</ErrorBoundary>
</React.Fragment>
</ApplicationLayout>
);
}
UserSessionWrapper.propTypes = {
bodyClass: PropTypes.string,
renderChildren: PropTypes.func,
};
UserSessionWrapper.defaultProps = {
bodyClass: null,
renderChildren: () => null,
};
export default function routeWithUserSession({ render, bodyClass, ...rest }) {
return {
...rest,
render: currentRoute => (
<UserSessionWrapper bodyClass={bodyClass} currentRoute={currentRoute} renderChildren={render} />
),
};
}

View File

@@ -0,0 +1,109 @@
import React, { useEffect, useState } from "react";
import ErrorBoundary, { ErrorBoundaryContext } from "@redash/viz/lib/components/ErrorBoundary";
import { Auth } from "@/services/auth";
import { policy } from "@/services/policy";
import { CurrentRoute } from "@/services/routes";
import organizationStatus from "@/services/organizationStatus";
import DynamicComponent from "@/components/DynamicComponent";
import ApplicationLayout from "./ApplicationLayout";
import ErrorMessage from "./ErrorMessage";
export type UserSessionWrapperRenderChildrenProps<P> = {
pageTitle?: string;
onError: (error: Error) => void;
} & P;
export interface UserSessionWrapperProps<P> {
render: (props: UserSessionWrapperRenderChildrenProps<P>) => React.ReactNode;
currentRoute: CurrentRoute<P>;
bodyClass?: string;
}
// This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object
// that contains:
// - `currentRoute.routeParams`
// - `pageTitle` field which is equal to `currentRoute.title`
// - `onError` field which is a `handleError` method of nearest error boundary
export function UserSessionWrapper<P>({ bodyClass, currentRoute, render }: UserSessionWrapperProps<P>) {
const [isAuthenticated, setIsAuthenticated] = useState(!!Auth.isAuthenticated());
useEffect(() => {
let isCancelled = false;
Promise.all([Auth.requireSession(), organizationStatus.refresh(), policy.refresh()])
.then(() => {
if (!isCancelled) {
setIsAuthenticated(!!Auth.isAuthenticated());
}
})
.catch(() => {
if (!isCancelled) {
setIsAuthenticated(false);
}
});
return () => {
isCancelled = true;
};
}, []);
useEffect(() => {
if (bodyClass) {
document.body.classList.toggle(bodyClass, true);
return () => {
document.body.classList.toggle(bodyClass, false);
};
}
}, [bodyClass]);
if (!isAuthenticated) {
return null;
}
return (
<ApplicationLayout>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'null | u... Remove this comment to see the full error message */}
<React.Fragment key={currentRoute.key}>
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<ErrorBoundary renderError={(error: Error) => <ErrorMessage error={error} />}>
<ErrorBoundaryContext.Consumer>
{({ handleError } /* : { handleError: UserSessionWrapperRenderChildrenProps<P>["onError"] } FIXME bring back type */) =>
render({ ...currentRoute.routeParams, pageTitle: currentRoute.title, onError: handleError })
}
</ErrorBoundaryContext.Consumer>
</ErrorBoundary>
</React.Fragment>
</ApplicationLayout>
);
}
export type RouteWithUserSessionOptions<P> = {
render: (props: UserSessionWrapperRenderChildrenProps<P>) => React.ReactNode;
bodyClass?: string;
title: string;
path: string;
};
export const UserSessionWrapperDynamicComponentName = "UserSessionWrapper";
export default function routeWithUserSession<P extends {} = {}>({
render: originalRender,
bodyClass,
...rest
}: RouteWithUserSessionOptions<P>) {
return {
...rest,
render: (currentRoute: CurrentRoute<P>) => {
const props = {
render: originalRender,
bodyClass,
currentRoute,
};
return (
<DynamicComponent
{...props}
name={UserSessionWrapperDynamicComponentName}
fallback={<UserSessionWrapper {...props} />}
/>
);
},
};
}

View File

@@ -13,16 +13,18 @@ const Text = Typography.Text;
function BeaconConsent() {
const [hide, setHide] = useState(false);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'showBeaconConsentMessage' does not exist... Remove this comment to see the full error message
if (!clientConfig.showBeaconConsentMessage || hide) {
return null;
}
const hideConsentCard = () => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'showBeaconConsentMessage' does not exist... Remove this comment to see the full error message
clientConfig.showBeaconConsentMessage = false;
setHide(true);
};
const confirmConsent = confirm => {
const confirmConsent = (confirm: any) => {
let message = "🙏 Thank you.";
if (!confirm) {
@@ -39,11 +41,13 @@ function BeaconConsent() {
return (
<DynamicComponent name="BeaconConsent">
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<div className="m-t-10 tiled">
<Card
title={
<>
Would you be ok with sharing anonymous usage data with the Redash team?{" "}
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'never'. */}
<HelpTrigger type="USAGE_DATA_SHARING" />
</>
}

View File

@@ -1,7 +1,15 @@
import React from "react";
import PropTypes from "prop-types";
function BigMessage({ message, icon, children, className }) {
type OwnProps = {
message?: string;
icon: string;
children?: React.ReactNode;
className?: string;
};
type Props = OwnProps & typeof BigMessage.defaultProps;
function BigMessage({ message, icon, children, className }: Props) {
return (
<div className={"p-15 text-center " + className}>
<h3 className="m-t-0 m-b-0">
@@ -14,13 +22,6 @@ function BigMessage({ message, icon, children, className }) {
);
}
BigMessage.propTypes = {
message: PropTypes.string,
icon: PropTypes.string.isRequired,
children: PropTypes.node,
className: PropTypes.string,
};
BigMessage.defaultProps = {
message: "",
children: null,

View File

@@ -1,24 +1,30 @@
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Tooltip from "antd/lib/tooltip";
import CopyOutlinedIcon from "@ant-design/icons/CopyOutlined";
import "./CodeBlock.less";
export default class CodeBlock extends React.Component {
static propTypes = {
copyable: PropTypes.bool,
children: PropTypes.node,
};
type OwnProps = {
copyable?: boolean;
};
type State = any;
type Props = OwnProps & typeof CodeBlock.defaultProps;
export default class CodeBlock extends React.Component<Props, State> {
static defaultProps = {
copyable: false,
children: null,
};
copyFeatureEnabled: any;
ref: any;
resetCopyState: any;
state = { copied: null };
constructor(props) {
constructor(props: Props) {
super(props);
this.ref = React.createRef();
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported("copy");
@@ -33,6 +39,7 @@ export default class CodeBlock extends React.Component {
copy = () => {
// select text
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
window.getSelection().selectAllChildren(this.ref.current);
// copy
@@ -49,6 +56,7 @@ export default class CodeBlock extends React.Component {
}
// reset selection
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
window.getSelection().removeAllRanges();
// reset tooltip

View File

@@ -1,12 +1,20 @@
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import AntCollapse from "antd/lib/collapse";
export default function Collapse({ collapsed, children, className, ...props }) {
type OwnProps = {
collapsed?: boolean;
children?: React.ReactNode;
className?: string;
};
type Props = OwnProps & typeof Collapse.defaultProps;
export default function Collapse({ collapsed, children, className, ...props }: Props) {
return (
<AntCollapse
{...props}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string | null' is not assignable to type 'st... Remove this comment to see the full error message
activeKey={collapsed ? null : "content"}
className={cx(className, "ant-collapse-headerless")}>
<AntCollapse.Panel key="content" header="">
@@ -16,12 +24,6 @@ export default function Collapse({ collapsed, children, className, ...props }) {
);
}
Collapse.propTypes = {
collapsed: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
};
Collapse.defaultProps = {
collapsed: true,
children: null,

View File

@@ -1,11 +1,11 @@
import React from "react";
import PropTypes from "prop-types";
import { isEmpty, toUpper, includes, get } from "lodash";
import Button from "antd/lib/button";
import List from "antd/lib/list";
import Modal from "antd/lib/modal";
import Input from "antd/lib/input";
import Steps from "antd/lib/steps";
// @ts-expect-error ts-migrate(6133) FIXME: 'DialogPropType' is declared but its value is neve... Remove this comment to see the full error message
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import Link from "@/components/Link";
import { PreviewCard } from "@/components/PreviewCard";
@@ -23,15 +23,21 @@ const StepEnum = {
DONE: 2,
};
class CreateSourceDialog extends React.Component {
static propTypes = {
dialog: DialogPropType.isRequired,
types: PropTypes.arrayOf(PropTypes.object),
sourceType: PropTypes.string.isRequired,
imageFolder: PropTypes.string.isRequired,
helpTriggerPrefix: PropTypes.string,
onCreate: PropTypes.func.isRequired,
};
type OwnProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'DialogPropType' refers to a value, but is being u... Remove this comment to see the full error message
dialog: DialogPropType;
types?: any[];
sourceType: string;
imageFolder: string;
helpTriggerPrefix?: string;
onCreate: (...args: any[]) => any;
};
type State = any;
type Props = OwnProps & typeof CreateSourceDialog.defaultProps;
class CreateSourceDialog extends React.Component<Props, State> {
static defaultProps = {
types: [],
@@ -45,7 +51,7 @@ class CreateSourceDialog extends React.Component {
currentStep: StepEnum.SELECT_TYPE,
};
selectType = selectedType => {
selectType = (selectedType: any) => {
this.setState({ selectedType, currentStep: StepEnum.CONFIGURE_IT });
};
@@ -55,17 +61,19 @@ class CreateSourceDialog extends React.Component {
}
};
createSource = (values, successCallback, errorCallback) => {
createSource = (values: any, successCallback: any, errorCallback: any) => {
const { selectedType, savingSource } = this.state;
if (!savingSource) {
this.setState({ savingSource: true, currentStep: StepEnum.DONE });
this.props
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onCreate' does not exist on type 'never'... Remove this comment to see the full error message
.onCreate(selectedType, values)
.then(data => {
.then((data: any) => {
successCallback("Saved.");
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dialog' does not exist on type 'never'.
this.props.dialog.close({ success: true, data });
})
.catch(error => {
.catch((error: any) => {
this.setState({ savingSource: false, currentStep: StepEnum.CONFIGURE_IT });
errorCallback(get(error, "response.data.message", "Failed saving."));
});
@@ -75,8 +83,9 @@ class CreateSourceDialog extends React.Component {
renderTypeSelector() {
const { types } = this.props;
const { searchText } = this.state;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'filter' does not exist on type 'never'.
const filteredTypes = types.filter(
type => isEmpty(searchText) || includes(type.name.toLowerCase(), searchText.toLowerCase())
(type: any) => isEmpty(searchText) || includes(type.name.toLowerCase(), searchText.toLowerCase())
);
return (
<div className="m-t-10">
@@ -100,22 +109,30 @@ class CreateSourceDialog extends React.Component {
renderForm() {
const { imageFolder, helpTriggerPrefix } = this.props;
const { selectedType } = this.state;
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'null' is not assignable to param... Remove this comment to see the full error message
const fields = helper.getFields(selectedType);
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
const helpTriggerType = `${helpTriggerPrefix}${toUpper(selectedType.type)}`;
return (
<div>
<div className="d-flex justify-content-center align-items-center">
{/* @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. */}
<img className="p-5" src={`${imageFolder}/${selectedType.type}.png`} alt={selectedType.name} width="48" />
{/* @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. */}
<h4 className="m-0">{selectedType.name}</h4>
</div>
<div className="text-right">
{/* @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message */}
{HELP_TRIGGER_TYPES[helpTriggerType] && (
// @ts-expect-error ts-migrate(2746) FIXME: This JSX tag's 'children' prop expects a single ch... Remove this comment to see the full error message
<HelpTrigger className="f-13" type={helpTriggerType}>
Setup Instructions <i className="fa fa-question-circle" />
</HelpTrigger>
)}
</div>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'never'. */}
<DynamicForm id="sourceForm" fields={fields} onSubmit={this.createSource} feedbackIcons hideSubmitButton />
{/* @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. */}
{selectedType.type === "databricks" && (
<small>
By using the Databricks Data Source you agree to the Databricks JDBC/ODBC{" "}
@@ -129,7 +146,7 @@ class CreateSourceDialog extends React.Component {
);
}
renderItem(item) {
renderItem(item: any) {
const { imageFolder } = this.props;
return (
<List.Item className="p-l-10 p-r-10 clickable" onClick={() => this.selectType(item)}>
@@ -139,6 +156,7 @@ class CreateSourceDialog extends React.Component {
roundedImage={false}
data-test="PreviewItem"
data-test-type={item.type}>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'null | u... Remove this comment to see the full error message */}
<i className="fa fa-angle-double-right" />
</PreviewCard>
</List.Item>
@@ -150,11 +168,13 @@ class CreateSourceDialog extends React.Component {
const { dialog, sourceType } = this.props;
return (
<Modal
// @ts-expect-error ts-migrate(2339) FIXME: Property 'props' does not exist on type 'never'.
{...dialog.props}
title={`Create a New ${sourceType}`}
footer={
currentStep === StepEnum.SELECT_TYPE
? [
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dismiss' does not exist on type 'never'.
<Button key="cancel" onClick={() => dialog.dismiss()} data-test="CreateSourceCancelButton">
Cancel
</Button>,

View File

@@ -1,43 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
import { Moment } from "@/components/proptypes";
const DateInput = React.forwardRef(({ defaultValue, value, onSelect, className, ...props }, ref) => {
const format = clientConfig.dateFormat || "YYYY-MM-DD";
const additionalAttributes = {};
if (defaultValue && defaultValue.isValid()) {
additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (value && value.isValid())) {
additionalAttributes.value = value;
}
return (
<DatePicker
ref={ref}
className={className}
{...additionalAttributes}
format={format}
placeholder="Select Date"
onChange={onSelect}
{...props}
/>
);
});
DateInput.propTypes = {
defaultValue: Moment,
value: Moment,
onSelect: PropTypes.func,
className: PropTypes.string,
};
DateInput.defaultProps = {
defaultValue: null,
value: undefined,
onSelect: () => {},
className: "",
};
export default DateInput;

View File

@@ -0,0 +1,48 @@
import React from "react";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
// @ts-expect-error ts-migrate(6133) FIXME: 'Moment' is declared but its value is never read.
import { Moment } from "@/components/proptypes";
type Props = {
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
defaultValue?: Moment;
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
value?: Moment;
onSelect?: (...args: any[]) => any;
className?: string;
};
const DateInput = React.forwardRef<any, Props>(({ defaultValue, value, onSelect, className, ...props }, ref) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dateFormat' does not exist on type '{}'.
const format = clientConfig.dateFormat || "YYYY-MM-DD";
const additionalAttributes = {};
if (defaultValue && defaultValue.isValid()) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'defaultValue' does not exist on type '{}... Remove this comment to see the full error message
additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (value && value.isValid())) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type '{}'.
additionalAttributes.value = value;
}
return (
<DatePicker
ref={ref}
className={className}
{...additionalAttributes}
format={format}
placeholder="Select Date"
onChange={onSelect}
{...props}
/>
);
});
DateInput.defaultProps = {
defaultValue: null,
value: undefined,
onSelect: () => {},
className: "",
};
export default DateInput;

View File

@@ -1,45 +0,0 @@
import { isArray } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
import { Moment } from "@/components/proptypes";
const { RangePicker } = DatePicker;
const DateRangeInput = React.forwardRef(({ defaultValue, value, onSelect, className, ...props }, ref) => {
const format = clientConfig.dateFormat || "YYYY-MM-DD";
const additionalAttributes = {};
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (isArray(value) && value[0].isValid() && value[1].isValid())) {
additionalAttributes.value = value;
}
return (
<RangePicker
ref={ref}
className={className}
{...additionalAttributes}
format={format}
onChange={onSelect}
{...props}
/>
);
});
DateRangeInput.propTypes = {
defaultValue: PropTypes.arrayOf(Moment),
value: PropTypes.arrayOf(Moment),
onSelect: PropTypes.func,
className: PropTypes.string,
};
DateRangeInput.defaultProps = {
defaultValue: null,
value: undefined,
onSelect: () => {},
className: "",
};
export default DateRangeInput;

View File

@@ -0,0 +1,51 @@
import { isArray } from "lodash";
import React from "react";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
// @ts-expect-error ts-migrate(6133) FIXME: 'Moment' is declared but its value is never read.
import { Moment } from "@/components/proptypes";
const { RangePicker } = DatePicker;
type Props = {
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
defaultValue?: Moment[];
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
value?: Moment[];
onSelect?: (...args: any[]) => any;
className?: string;
};
const DateRangeInput = React.forwardRef<any, Props>(({ defaultValue, value, onSelect, className, ...props }, ref) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dateFormat' does not exist on type '{}'.
const format = clientConfig.dateFormat || "YYYY-MM-DD";
const additionalAttributes = {};
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'defaultValue' does not exist on type '{}... Remove this comment to see the full error message
additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (isArray(value) && value[0].isValid() && value[1].isValid())) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type '{}'.
additionalAttributes.value = value;
}
return (
<RangePicker
ref={ref}
className={className}
{...additionalAttributes}
format={format}
onChange={onSelect}
{...props}
/>
);
});
DateRangeInput.defaultProps = {
// @ts-expect-error ts-migrate(2322) FIXME: Type 'null' is not assignable to type 'any[] | und... Remove this comment to see the full error message
defaultValue: null,
value: undefined,
onSelect: () => {},
className: "",
};
export default DateRangeInput;

View File

@@ -1,46 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
import { Moment } from "@/components/proptypes";
const DateTimeInput = React.forwardRef(({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
const additionalAttributes = {};
if (defaultValue && defaultValue.isValid()) {
additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (value && value.isValid())) {
additionalAttributes.value = value;
}
return (
<DatePicker
ref={ref}
className={className}
showTime
{...additionalAttributes}
format={format}
placeholder="Select Date and Time"
onChange={onSelect}
{...props}
/>
);
});
DateTimeInput.propTypes = {
defaultValue: Moment,
value: Moment,
withSeconds: PropTypes.bool,
onSelect: PropTypes.func,
className: PropTypes.string,
};
DateTimeInput.defaultProps = {
defaultValue: null,
value: undefined,
withSeconds: false,
onSelect: () => {},
className: "",
};
export default DateTimeInput;

View File

@@ -0,0 +1,51 @@
import React from "react";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
// @ts-expect-error ts-migrate(6133) FIXME: 'Moment' is declared but its value is never read.
import { Moment } from "@/components/proptypes";
type Props = {
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
defaultValue?: Moment;
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
value?: Moment;
withSeconds?: boolean;
onSelect?: (...args: any[]) => any;
className?: string;
};
const DateTimeInput = React.forwardRef<any, Props>(({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dateFormat' does not exist on type '{}'.
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
const additionalAttributes = {};
if (defaultValue && defaultValue.isValid()) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'defaultValue' does not exist on type '{}... Remove this comment to see the full error message
additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (value && value.isValid())) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type '{}'.
additionalAttributes.value = value;
}
return (
<DatePicker
ref={ref}
className={className}
showTime
{...additionalAttributes}
format={format}
placeholder="Select Date and Time"
onChange={onSelect}
{...props}
/>
);
});
DateTimeInput.defaultProps = {
defaultValue: null,
value: undefined,
withSeconds: false,
onSelect: () => {},
className: "",
};
export default DateTimeInput;

View File

@@ -1,20 +1,33 @@
import { isArray } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
// @ts-expect-error ts-migrate(6133) FIXME: 'Moment' is declared but its value is never read.
import { Moment } from "@/components/proptypes";
const { RangePicker } = DatePicker;
const DateTimeRangeInput = React.forwardRef(
type Props = {
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
defaultValue?: Moment[];
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
value?: Moment[];
withSeconds?: boolean;
onSelect?: (...args: any[]) => any;
className?: string;
};
const DateTimeRangeInput = React.forwardRef<any, Props>(
({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dateFormat' does not exist on type '{}'.
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
const additionalAttributes = {};
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'defaultValue' does not exist on type '{}... Remove this comment to see the full error message
additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (isArray(value) && value[0].isValid() && value[1].isValid())) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type '{}'.
additionalAttributes.value = value;
}
return (
@@ -31,15 +44,8 @@ const DateTimeRangeInput = React.forwardRef(
}
);
DateTimeRangeInput.propTypes = {
defaultValue: PropTypes.arrayOf(Moment),
value: PropTypes.arrayOf(Moment),
withSeconds: PropTypes.bool,
onSelect: PropTypes.func,
className: PropTypes.string,
};
DateTimeRangeInput.defaultProps = {
// @ts-expect-error ts-migrate(2322) FIXME: Type 'null' is not assignable to type 'any[] | und... Remove this comment to see the full error message
defaultValue: null,
value: undefined,
withSeconds: false,

View File

@@ -22,8 +22,8 @@ export function wrap<ROk = void, P = {}, RCancel = void>(
props?: P
) => {
update: (props: P) => void;
onClose: (handler: (result: ROk) => Promise<void>) => void;
onDismiss: (handler: (result: RCancel) => Promise<void>) => void;
onClose: (handler: (result: ROk) => Promise<void> | void) => void;
onDismiss: (handler: (result: RCancel) => Promise<void> | void) => void;
close: (result: ROk) => void;
dismiss: (result: RCancel) => void;
};

View File

@@ -3,6 +3,17 @@ import React from "react";
import PropTypes from "prop-types";
import ReactDOM from "react-dom";
type DialogPropType = {
props: {
visible?: boolean;
onOk?: (...args: any[]) => any;
onCancel?: (...args: any[]) => any;
afterClose?: (...args: any[]) => any;
};
close: (...args: any[]) => any;
dismiss: (...args: any[]) => any;
};
/**
Wrapper for dialogs based on Ant's <Modal> component.
@@ -75,7 +86,7 @@ import ReactDOM from "react-dom";
);
}
4. wrap your component and export it:
4. wrap your component and it:
export default wrapDialog(YourComponent).
@@ -96,18 +107,20 @@ import ReactDOM from "react-dom";
}
*/
export const DialogPropType = PropTypes.shape({
props: PropTypes.shape({
visible: PropTypes.bool,
onOk: PropTypes.func,
onCancel: PropTypes.func,
afterClose: PropTypes.func,
}).isRequired,
close: PropTypes.func.isRequired,
dismiss: PropTypes.func.isRequired,
// @ts-expect-error ts-migrate(2322) FIXME: Type 'Requireable<InferProps<{ props: Validator<In... Remove this comment to see the full error message
export const DialogPropType: PropTypes.Requireable<DialogPropType> = PropTypes.shape({
props: PropTypes.shape({
visible: PropTypes.bool,
onOk: PropTypes.func,
onCancel: PropTypes.func,
afterClose: PropTypes.func,
}).isRequired,
close: PropTypes.func.isRequired,
dismiss: PropTypes.func.isRequired,
});
function openDialog(DialogComponent, props) {
function openDialog(DialogComponent: any, props: any) {
const dialog = {
props: {
visible: true,
@@ -121,7 +134,7 @@ function openDialog(DialogComponent, props) {
dismiss: () => {},
};
let pendingCloseTask = null;
let pendingCloseTask: any = null;
const handlers = {
onClose: () => {},
@@ -143,7 +156,7 @@ function openDialog(DialogComponent, props) {
}, 10);
}
function processDialogClose(result, setAdditionalDialogProps) {
function processDialogClose(result: any, setAdditionalDialogProps: any) {
dialog.props.okButtonProps = { disabled: true };
dialog.props.cancelButtonProps = { disabled: true };
setAdditionalDialogProps();
@@ -160,9 +173,11 @@ function openDialog(DialogComponent, props) {
});
}
function closeDialog(result) {
function closeDialog(result: any) {
if (!pendingCloseTask) {
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
pendingCloseTask = processDialogClose(handlers.onClose(result), () => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'loading' does not exist on type '{}'.
dialog.props.okButtonProps.loading = true;
}).finally(() => {
pendingCloseTask = null;
@@ -171,9 +186,11 @@ function openDialog(DialogComponent, props) {
return pendingCloseTask;
}
function dismissDialog(result) {
function dismissDialog(result: any) {
if (!pendingCloseTask) {
// @ts-expect-error ts-migrate(2554) FIXME: Expected 0 arguments, but got 1.
pendingCloseTask = processDialogClose(handlers.onDismiss(result), () => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'loading' does not exist on type '{}'.
dialog.props.cancelButtonProps.loading = true;
}).finally(() => {
pendingCloseTask = null;
@@ -182,26 +199,30 @@ function openDialog(DialogComponent, props) {
return pendingCloseTask;
}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(result: any) => any' is not assignable to t... Remove this comment to see the full error message
dialog.props.onOk = closeDialog;
// @ts-expect-error ts-migrate(2322) FIXME: Type '(result: any) => any' is not assignable to t... Remove this comment to see the full error message
dialog.props.onCancel = dismissDialog;
dialog.props.afterClose = destroyDialog;
// @ts-expect-error ts-migrate(2322) FIXME: Type '(result: any) => any' is not assignable to t... Remove this comment to see the full error message
dialog.close = closeDialog;
// @ts-expect-error ts-migrate(2322) FIXME: Type '(result: any) => any' is not assignable to t... Remove this comment to see the full error message
dialog.dismiss = dismissDialog;
const result = {
close: closeDialog,
dismiss: dismissDialog,
update: newProps => {
update: (newProps: any) => {
props = { ...props, ...newProps };
render();
},
onClose: handler => {
onClose: (handler: any) => {
if (isFunction(handler)) {
handlers.onClose = handler;
}
return result;
},
onDismiss: handler => {
onDismiss: (handler: any) => {
if (isFunction(handler)) {
handlers.onDismiss = handler;
}
@@ -214,14 +235,9 @@ function openDialog(DialogComponent, props) {
return result;
}
export function wrap(DialogComponent) {
export function wrap(DialogComponent: any) {
return {
Component: DialogComponent,
showModal: props => openDialog(DialogComponent, props),
showModal: (props: any) => openDialog(DialogComponent, props),
};
}
export default {
DialogPropType,
wrap,
};

View File

@@ -1,31 +1,35 @@
import { isFunction, isString } from "lodash";
import { isFunction, isString, isUndefined } from "lodash";
import React from "react";
import PropTypes from "prop-types";
const componentsRegistry = new Map();
const activeInstances = new Set();
export function registerComponent(name, component) {
export function registerComponent(name: any, component: any) {
if (isString(name) && name !== "") {
componentsRegistry.set(name, isFunction(component) ? component : null);
// Refresh active DynamicComponent instances which use this component
activeInstances.forEach(dynamicComponent => {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
if (dynamicComponent.props.name === name) {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
dynamicComponent.forceUpdate();
}
});
}
}
export function unregisterComponent(name) {
export function unregisterComponent(name: any) {
registerComponent(name, null);
}
export default class DynamicComponent extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
children: PropTypes.node,
};
type OwnProps = {
name: string;
fallback?: React.ReactNode;
};
type Props = OwnProps & typeof DynamicComponent.defaultProps;
export default class DynamicComponent extends React.Component<Props> {
static defaultProps = {
children: null,
@@ -40,10 +44,11 @@ export default class DynamicComponent extends React.Component {
}
render() {
const { name, children, ...props } = this.props;
const { name, children, fallback, ...props } = this.props;
const RealComponent = componentsRegistry.get(name);
if (!RealComponent) {
return children;
// return fallback if any, otherwise return children
return isUndefined(fallback) ? children : fallback;
}
return <RealComponent {...props}>{children}</RealComponent>;
}

View File

@@ -1,21 +1,25 @@
import { trim } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Input from "antd/lib/input";
export default class EditInPlace extends React.Component {
static propTypes = {
ignoreBlanks: PropTypes.bool,
isEditable: PropTypes.bool,
placeholder: PropTypes.string,
value: PropTypes.string,
onDone: PropTypes.func.isRequired,
onStopEditing: PropTypes.func,
multiline: PropTypes.bool,
editorProps: PropTypes.object,
defaultEditing: PropTypes.bool,
};
type OwnProps = {
ignoreBlanks?: boolean;
isEditable?: boolean;
placeholder?: string;
value?: string;
onDone: (...args: any[]) => any;
onStopEditing?: (...args: any[]) => any;
multiline?: boolean;
editorProps?: any;
defaultEditing?: boolean;
};
type State = any;
type Props = OwnProps & typeof EditInPlace.defaultProps;
export default class EditInPlace extends React.Component<Props, State> {
static defaultProps = {
ignoreBlanks: false,
@@ -28,14 +32,14 @@ export default class EditInPlace extends React.Component {
defaultEditing: false,
};
constructor(props) {
constructor(props: Props) {
super(props);
this.state = {
editing: props.defaultEditing,
};
}
componentDidUpdate(_, prevState) {
componentDidUpdate(_: Props, prevState: State) {
if (!this.state.editing && prevState.editing) {
this.props.onStopEditing();
}
@@ -47,7 +51,7 @@ export default class EditInPlace extends React.Component {
}
};
stopEditing = currentValue => {
stopEditing = (currentValue: any) => {
const newValue = trim(currentValue);
const ignorableBlank = this.props.ignoreBlanks && newValue === "";
if (!ignorableBlank && newValue !== this.props.value) {
@@ -56,7 +60,7 @@ export default class EditInPlace extends React.Component {
this.setState({ editing: false });
};
handleKeyDown = event => {
handleKeyDown = (event: any) => {
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
this.stopEditing(event.target.value);
@@ -86,7 +90,7 @@ export default class EditInPlace extends React.Component {
return (
<InputComponent
defaultValue={value}
onBlur={e => this.stopEditing(e.target.value)}
onBlur={(e: any) => this.stopEditing(e.target.value)}
onKeyDown={this.handleKeyDown}
autoFocus
{...editorProps}
@@ -96,6 +100,7 @@ export default class EditInPlace extends React.Component {
render() {
return (
// @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type 'Reado... Remove this comment to see the full error message
<span className={cx("edit-in-place", { active: this.state.editing }, this.props.className)}>
{this.state.editing ? this.renderEdit() : this.renderNormal()}
</span>

View File

@@ -1,6 +1,5 @@
import { includes, words, capitalize, clone, isNull } from "lodash";
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import Checkbox from "antd/lib/checkbox";
import Modal from "antd/lib/modal";
import Form from "antd/lib/form";
@@ -8,6 +7,7 @@ import Button from "antd/lib/button";
import Select from "antd/lib/select";
import Input from "antd/lib/input";
import Divider from "antd/lib/divider";
// @ts-expect-error ts-migrate(6133) FIXME: 'DialogPropType' is declared but its value is neve... Remove this comment to see the full error message
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import QuerySelector from "@/components/QuerySelector";
import { Query } from "@/services/query";
@@ -15,20 +15,28 @@ import { Query } from "@/services/query";
const { Option } = Select;
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
function getDefaultTitle(text) {
function getDefaultTitle(text: any) {
return capitalize(words(text).join(" ")); // humanize
}
function isTypeDateRange(type) {
function isTypeDateRange(type: any) {
return /-range/.test(type);
}
function joinExampleList(multiValuesOptions) {
function joinExampleList(multiValuesOptions: any) {
const { prefix, suffix } = multiValuesOptions;
return ["value1", "value2", "value3"].map(value => `${prefix}${value}${suffix}`).join(",");
}
function NameInput({ name, type, onChange, existingNames, setValidation }) {
type NameInputProps = {
name: string;
onChange: (...args: any[]) => any;
existingNames: string[];
setValidation: (...args: any[]) => any;
type: string;
};
function NameInput({ name, type, onChange, existingNames, setValidation }: NameInputProps) {
let helpText = "";
let validateStatus = "";
@@ -41,6 +49,7 @@ function NameInput({ name, type, onChange, existingNames, setValidation }) {
validateStatus = "error";
} else {
if (isTypeDateRange(type)) {
// @ts-expect-error ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'string'.
helpText = (
<React.Fragment>
Appears in query as{" "}
@@ -52,21 +61,23 @@ function NameInput({ name, type, onChange, existingNames, setValidation }) {
}
return (
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type '"" | "err... Remove this comment to see the full error message
<Form.Item required label="Keyword" help={helpText} validateStatus={validateStatus} {...formItemProps}>
<Input onChange={e => onChange(e.target.value)} autoFocus />
</Form.Item>
);
}
NameInput.propTypes = {
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
existingNames: PropTypes.arrayOf(PropTypes.string).isRequired,
setValidation: PropTypes.func.isRequired,
type: PropTypes.string.isRequired,
type OwnEditParameterSettingsDialogProps = {
parameter: any;
// @ts-expect-error ts-migrate(2749) FIXME: 'DialogPropType' refers to a value, but is being u... Remove this comment to see the full error message
dialog: DialogPropType;
existingParams?: string[];
};
function EditParameterSettingsDialog(props) {
type EditParameterSettingsDialogProps = OwnEditParameterSettingsDialogProps & typeof EditParameterSettingsDialog.defaultProps;
function EditParameterSettingsDialog(props: EditParameterSettingsDialogProps) {
const [param, setParam] = useState(clone(props.parameter));
const [isNameValid, setIsNameValid] = useState(true);
const [initialQuery, setInitialQuery] = useState();
@@ -77,6 +88,7 @@ function EditParameterSettingsDialog(props) {
useEffect(() => {
const queryId = props.parameter.queryId;
if (queryId) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'get' does not exist on type 'typeof Quer... Remove this comment to see the full error message
Query.get({ id: queryId }).then(setInitialQuery);
}
}, [props.parameter.queryId]);
@@ -140,7 +152,7 @@ function EditParameterSettingsDialog(props) {
type={param.type}
/>
)}
<Form.Item label="Title" {...formItemProps}>
<Form.Item required label="Title" {...formItemProps}>
<Input
value={isNull(param.title) ? getDefaultTitle(param.name) : param.title}
onChange={e => setParam({ ...param, title: e.target.value })}
@@ -157,6 +169,7 @@ function EditParameterSettingsDialog(props) {
</Option>
<Option value="enum">Dropdown List</Option>
<Option value="query">Query Based Dropdown List</Option>
{/* @ts-expect-error ts-migrate(2741) FIXME: Property 'value' is missing in type '{ children: E... Remove this comment to see the full error message */}
<Option disabled key="dv1">
<Divider className="select-option-divider" />
</Option>
@@ -167,6 +180,7 @@ function EditParameterSettingsDialog(props) {
Date and Time
</Option>
<Option value="datetime-with-seconds">Date and Time (with seconds)</Option>
{/* @ts-expect-error ts-migrate(2741) FIXME: Property 'value' is missing in type '{ children: E... Remove this comment to see the full error message */}
<Option disabled key="dv2">
<Divider className="select-option-divider" />
</Option>
@@ -189,8 +203,11 @@ function EditParameterSettingsDialog(props) {
{param.type === "query" && (
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
<QuerySelector
// @ts-expect-error ts-migrate(2322) FIXME: Type 'undefined' is not assignable to type 'never'... Remove this comment to see the full error message
selectedQuery={initialQuery}
onChange={q => setParam({ ...param, queryId: q && q.id })}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(q: any) => void' is not assignable to type ... Remove this comment to see the full error message
onChange={(q: any) => setParam({ ...param, queryId: q && q.id })}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'never'.
type="select"
/>
</Form.Item>
@@ -251,12 +268,6 @@ function EditParameterSettingsDialog(props) {
);
}
EditParameterSettingsDialog.propTypes = {
parameter: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
dialog: DialogPropType.isRequired,
existingParams: PropTypes.arrayOf(PropTypes.string),
};
EditParameterSettingsDialog.defaultProps = {
existingParams: [],
};

View File

@@ -1,8 +1,8 @@
import React from "react";
import PropTypes from "prop-types";
import Dropdown from "antd/lib/dropdown";
import Menu from "antd/lib/menu";
import Button from "antd/lib/button";
import { clientConfig } from "@/services/auth";
import PlusCircleFilledIcon from "@ant-design/icons/PlusCircleFilled";
import ShareAltOutlinedIcon from "@ant-design/icons/ShareAltOutlined";
@@ -12,7 +12,20 @@ import EllipsisOutlinedIcon from "@ant-design/icons/EllipsisOutlined";
import QueryResultsLink from "./QueryResultsLink";
export default function QueryControlDropdown(props) {
type OwnProps = {
query: any;
queryResult?: any;
queryExecuting: boolean;
showEmbedDialog: (...args: any[]) => any;
embed?: boolean;
apiKey?: string;
selectedTab?: string | number;
openAddToDashboardForm: (...args: any[]) => any;
};
type Props = OwnProps & typeof QueryControlDropdown.defaultProps;
export default function QueryControlDropdown(props: Props) {
const menu = (
<Menu>
{!props.query.isNew() && (!props.query.is_draft || !props.query.is_archived) && (
@@ -22,7 +35,8 @@ export default function QueryControlDropdown(props) {
</a>
</Menu.Item>
)}
{!props.query.isNew() && (
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'disablePublicUrls' does not exist on typ... Remove this comment to see the full error message */}
{!clientConfig.disablePublicUrls && !props.query.isNew() && (
<Menu.Item>
<a onClick={() => props.showEmbedDialog(props.query, props.selectedTab)} data-test="ShowEmbedDialogButton">
<ShareAltOutlinedIcon /> Embed Elsewhere
@@ -74,17 +88,6 @@ export default function QueryControlDropdown(props) {
);
}
QueryControlDropdown.propTypes = {
query: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types
queryExecuting: PropTypes.bool.isRequired,
showEmbedDialog: PropTypes.func.isRequired,
embed: PropTypes.bool,
apiKey: PropTypes.string,
selectedTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
openAddToDashboardForm: PropTypes.func.isRequired,
};
QueryControlDropdown.defaultProps = {
queryResult: {},
embed: false,

View File

@@ -1,8 +1,19 @@
import React from "react";
import PropTypes from "prop-types";
import Link from "@/components/Link";
export default function QueryResultsLink(props) {
type OwnProps = {
query: any;
queryResult?: any;
fileType?: string;
disabled: boolean;
embed?: boolean;
apiKey?: string;
children: React.ReactNode[] | React.ReactNode;
};
type Props = OwnProps & typeof QueryResultsLink.defaultProps;
export default function QueryResultsLink(props: Props) {
let href = "";
const { query, queryResult, fileType } = props;
@@ -24,16 +35,6 @@ export default function QueryResultsLink(props) {
);
}
QueryResultsLink.propTypes = {
query: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types
fileType: PropTypes.string,
disabled: PropTypes.bool.isRequired,
embed: PropTypes.bool,
apiKey: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
};
QueryResultsLink.defaultProps = {
queryResult: {},
fileType: "csv",

View File

@@ -1,9 +1,15 @@
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import FormOutlinedIcon from "@ant-design/icons/FormOutlined";
export default function EditVisualizationButton(props) {
type OwnProps = {
openVisualizationEditor: (...args: any[]) => any;
selectedTab?: string | number;
};
type Props = OwnProps & typeof EditVisualizationButton.defaultProps;
export default function EditVisualizationButton(props: Props) {
return (
<Button
data-test="EditVisualization"
@@ -15,11 +21,6 @@ export default function EditVisualizationButton(props) {
);
}
EditVisualizationButton.propTypes = {
openVisualizationEditor: PropTypes.func.isRequired,
selectedTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
EditVisualizationButton.defaultProps = {
selectedTab: "",
};

View File

@@ -1,12 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { clientConfig, currentUser } from "@/services/auth";
import Tooltip from "antd/lib/tooltip";
import Alert from "antd/lib/alert";
import HelpTrigger from "@/components/HelpTrigger";
export default function EmailSettingsWarning({ featureName, className, mode, adminOnly }) {
type OwnProps = {
featureName: string;
className?: string;
mode?: "alert" | "icon";
adminOnly?: boolean;
};
type Props = OwnProps & typeof EmailSettingsWarning.defaultProps;
export default function EmailSettingsWarning({ featureName, className, mode, adminOnly }: Props) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'mailSettingsMissing' does not exist on t... Remove this comment to see the full error message
if (!clientConfig.mailSettingsMissing) {
return null;
}
@@ -18,6 +27,7 @@ export default function EmailSettingsWarning({ featureName, className, mode, adm
const message = (
<span>
Your mail server isn&apos;t configured correctly, and is needed for {featureName} to work.{" "}
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'never'. */}
<HelpTrigger type="MAIL_CONFIG" className="f-inherit" />
</span>
);
@@ -33,13 +43,6 @@ export default function EmailSettingsWarning({ featureName, className, mode, adm
return <Alert message={message} type="error" className={className} />;
}
EmailSettingsWarning.propTypes = {
featureName: PropTypes.string.isRequired,
className: PropTypes.string,
mode: PropTypes.oneOf(["alert", "icon"]),
adminOnly: PropTypes.bool,
};
EmailSettingsWarning.defaultProps = {
className: null,
mode: "alert",

View File

@@ -1,19 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
export default class FavoritesControl extends React.Component {
static propTypes = {
item: PropTypes.shape({
is_favorite: PropTypes.bool.isRequired,
}).isRequired,
onChange: PropTypes.func,
};
type OwnProps = {
item: {
is_favorite: boolean;
};
onChange?: (...args: any[]) => any;
};
type Props = OwnProps & typeof FavoritesControl.defaultProps;
export default class FavoritesControl extends React.Component<Props> {
static defaultProps = {
onChange: () => {},
};
toggleItem(event, item, callback) {
toggleItem(event: any, item: any, callback: any) {
const action = item.is_favorite ? item.unfavorite.bind(item) : item.favorite.bind(item);
const savedIsFavorite = item.is_favorite;

View File

@@ -8,18 +8,28 @@ import { formatColumnValue } from "@/lib/utils";
const ALL_VALUES = "###Redash::Filters::SelectAll###";
const NONE_VALUES = "###Redash::Filters::Clear###";
export const FilterType = PropTypes.shape({
name: PropTypes.string.isRequired,
friendlyName: PropTypes.string.isRequired,
multiple: PropTypes.bool,
current: PropTypes.oneOfType([PropTypes.any, PropTypes.arrayOf(PropTypes.any)]),
values: PropTypes.arrayOf(PropTypes.any).isRequired,
type FilterType = {
name: string;
friendlyName: string;
multiple?: boolean;
current?: any | any[];
values: any[];
};
// @ts-expect-error ts-migrate(2322) FIXME: Type 'Requireable<InferProps<{ name: Validator<str... Remove this comment to see the full error message
const FilterType: PropTypes.Requireable<FilterType> = PropTypes.shape({
name: PropTypes.string.isRequired,
friendlyName: PropTypes.string.isRequired,
multiple: PropTypes.bool,
current: PropTypes.oneOfType([PropTypes.any, PropTypes.arrayOf(PropTypes.any)]),
values: PropTypes.arrayOf(PropTypes.any).isRequired,
});
export { FilterType };
export const FiltersType = PropTypes.arrayOf(FilterType);
function createFilterChangeHandler(filters, onChange) {
return (filter, values) => {
function createFilterChangeHandler(filters: any, onChange: any) {
return (filter: any, values: any) => {
if (isArray(values)) {
values = map(values, value => filter.values[toNumber(value.key)] || value.key);
} else {
@@ -38,7 +48,7 @@ function createFilterChangeHandler(filters, onChange) {
};
}
export function filterData(rows, filters = []) {
export function filterData(rows: any, filters = []) {
if (!isArray(rows)) {
return [];
}
@@ -49,7 +59,9 @@ export function filterData(rows, filters = []) {
// "every" field's value should match "some" of corresponding filter's values
result = result.filter(row =>
every(filters, filter => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'name' does not exist on type 'never'.
const rowValue = row[filter.name];
// @ts-expect-error ts-migrate(2339) FIXME: Property 'current' does not exist on type 'never'.
const filterValues = isArray(filter.current) ? filter.current : [filter.current];
return some(filterValues, filterValue => {
if (moment.isMoment(rowValue)) {
@@ -66,11 +78,20 @@ export function filterData(rows, filters = []) {
return result;
}
function Filters({ filters, onChange }) {
type OwnProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'FiltersType' refers to a value, but is being used... Remove this comment to see the full error message
filters: FiltersType;
onChange?: (...args: any[]) => any;
};
type Props = OwnProps & typeof Filters.defaultProps;
function Filters({ filters, onChange }: Props) {
if (filters.length === 0) {
return null;
}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(filter: any, values: any) => void' is not a... Remove this comment to see the full error message
onChange = createFilterChangeHandler(filters, onChange);
return (
@@ -79,6 +100,7 @@ function Filters({ filters, onChange }) {
<div className="row">
{map(filters, filter => {
const options = map(filter.values, (value, index) => (
// @ts-expect-error ts-migrate(2741) FIXME: Property 'value' is missing in type '{ children: a... Remove this comment to see the full error message
<Select.Option key={index}>{formatColumnValue(value, get(filter, "column.type"))}</Select.Option>
));
@@ -90,6 +112,7 @@ function Filters({ filters, onChange }) {
<label>{filter.friendlyName}</label>
{options.length === 0 && <Select className="w-100" disabled value="No values" />}
{options.length > 0 && (
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<Select
labelInValue
className="w-100"
@@ -111,10 +134,12 @@ function Filters({ filters, onChange }) {
onChange={values => onChange(filter, values)}>
{!filter.multiple && options}
{filter.multiple && [
// @ts-expect-error ts-migrate(2741) FIXME: Property 'value' is missing in type '{ children: (... Remove this comment to see the full error message
<Select.Option key={NONE_VALUES} data-test="ClearOption">
<i className="fa fa-square-o m-r-5" />
Clear
</Select.Option>,
// @ts-expect-error ts-migrate(2741) FIXME: Property 'value' is missing in type '{ children: (... Remove this comment to see the full error message
<Select.Option key={ALL_VALUES} data-test="SelectAllOption">
<i className="fa fa-check-square-o m-r-5" />
Select All
@@ -134,11 +159,6 @@ function Filters({ filters, onChange }) {
);
}
Filters.propTypes = {
filters: FiltersType.isRequired,
onChange: PropTypes.func, // (name, value) => void
};
Filters.defaultProps = {
onChange: () => {},
};

View File

@@ -1,219 +0,0 @@
import { startsWith, get } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Tooltip from "antd/lib/tooltip";
import Drawer from "antd/lib/drawer";
import Link from "@/components/Link";
import CloseOutlinedIcon from "@ant-design/icons/CloseOutlined";
import BigMessage from "@/components/BigMessage";
import DynamicComponent from "@/components/DynamicComponent";
import "./HelpTrigger.less";
const DOMAIN = "https://redash.io";
const HELP_PATH = "/help";
const IFRAME_TIMEOUT = 20000;
const IFRAME_URL_UPDATE_MESSAGE = "iframe_url";
export const TYPES = {
HOME: ["", "Help"],
VALUE_SOURCE_OPTIONS: ["/user-guide/querying/query-parameters#Value-Source-Options", "Guide: Value Source Options"],
SHARE_DASHBOARD: ["/user-guide/dashboards/sharing-dashboards", "Guide: Sharing and Embedding Dashboards"],
AUTHENTICATION_OPTIONS: ["/user-guide/users/authentication-options", "Guide: Authentication Options"],
USAGE_DATA_SHARING: ["/open-source/admin-guide/usage-data", "Help: Anonymous Usage Data Sharing"],
DS_ATHENA: ["/data-sources/amazon-athena-setup", "Guide: Help Setting up Amazon Athena"],
DS_BIGQUERY: ["/data-sources/bigquery-setup", "Guide: Help Setting up BigQuery"],
DS_URL: ["/data-sources/querying-urls", "Guide: Help Setting up URL"],
DS_MONGODB: ["/data-sources/mongodb-setup", "Guide: Help Setting up MongoDB"],
DS_GOOGLE_SPREADSHEETS: ["/data-sources/querying-a-google-spreadsheet", "Guide: Help Setting up Google Spreadsheets"],
DS_GOOGLE_ANALYTICS: ["/data-sources/google-analytics-setup", "Guide: Help Setting up Google Analytics"],
DS_AXIBASETSD: ["/data-sources/axibase-time-series-database", "Guide: Help Setting up Axibase Time Series"],
DS_RESULTS: ["/user-guide/querying/query-results-data-source", "Guide: Help Setting up Query Results"],
ALERT_SETUP: ["/user-guide/alerts/setting-up-an-alert", "Guide: Setting Up a New Alert"],
MAIL_CONFIG: ["/open-source/setup/#Mail-Configuration", "Guide: Mail Configuration"],
ALERT_NOTIF_TEMPLATE_GUIDE: ["/user-guide/alerts/custom-alert-notifications", "Guide: Custom Alerts Notifications"],
FAVORITES: ["/user-guide/querying/favorites-tagging/#Favorites", "Guide: Favorites"],
MANAGE_PERMISSIONS: [
"/user-guide/querying/writing-queries#Managing-Query-Permissions",
"Guide: Managing Query Permissions",
],
NUMBER_FORMAT_SPECS: ["/user-guide/visualizations/formatting-numbers", "Formatting Numbers"],
};
export default class HelpTrigger extends React.Component {
static propTypes = {
type: PropTypes.oneOf(Object.keys(TYPES)),
href: PropTypes.string,
title: PropTypes.node,
className: PropTypes.string,
showTooltip: PropTypes.bool,
children: PropTypes.node,
};
static defaultProps = {
type: null,
href: null,
title: null,
className: null,
showTooltip: true,
children: <i className="fa fa-question-circle" />,
};
iframeRef = React.createRef();
iframeLoadingTimeout = null;
state = {
visible: false,
loading: false,
error: false,
currentUrl: null,
};
componentDidMount() {
window.addEventListener("message", this.onPostMessageReceived, false);
}
componentWillUnmount() {
window.removeEventListener("message", this.onPostMessageReceived);
clearTimeout(this.iframeLoadingTimeout);
}
loadIframe = url => {
clearTimeout(this.iframeLoadingTimeout);
this.setState({ loading: true, error: false });
this.iframeRef.current.src = url;
this.iframeLoadingTimeout = setTimeout(() => {
this.setState({ error: url, loading: false });
}, IFRAME_TIMEOUT); // safety
};
onIframeLoaded = () => {
this.setState({ loading: false });
clearTimeout(this.iframeLoadingTimeout);
};
onPostMessageReceived = event => {
if (!startsWith(event.origin, DOMAIN)) {
return;
}
const { type, message: currentUrl } = event.data || {};
if (type !== IFRAME_URL_UPDATE_MESSAGE) {
return;
}
this.setState({ currentUrl });
};
getUrl = () => {
const helpTriggerType = get(TYPES, this.props.type);
return helpTriggerType ? DOMAIN + HELP_PATH + helpTriggerType[0] : this.props.href;
};
openDrawer = () => {
this.setState({ visible: true });
// wait for drawer animation to complete so there's no animation jank
setTimeout(() => this.loadIframe(this.getUrl()), 300);
};
closeDrawer = event => {
if (event) {
event.preventDefault();
}
this.setState({ visible: false });
this.setState({ visible: false, currentUrl: null });
};
render() {
const tooltip = get(TYPES, `${this.props.type}[1]`, this.props.title);
const className = cx("help-trigger", this.props.className);
const url = this.state.currentUrl;
const isAllowedDomain = startsWith(url || this.getUrl(), DOMAIN);
return (
<React.Fragment>
<Tooltip
title={
this.props.showTooltip ? (
<>
{tooltip}
{!isAllowedDomain && <i className="fa fa-external-link" style={{ marginLeft: 5 }} />}
</>
) : null
}>
{isAllowedDomain ? (
<a onClick={this.openDrawer} className={className}>
{this.props.children}
</a>
) : (
<Link href={url || this.getUrl()} className={className} rel="noopener noreferrer" target="_blank">
{this.props.children}
</Link>
)}
</Tooltip>
<Drawer
placement="right"
closable={false}
onClose={this.closeDrawer}
visible={this.state.visible}
className="help-drawer"
destroyOnClose
width={400}>
<div className="drawer-wrapper">
<div className="drawer-menu">
{url && (
<Tooltip title="Open page in a new window" placement="left">
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<Link href={url} target="_blank">
<i className="fa fa-external-link" />
</Link>
</Tooltip>
)}
<Tooltip title="Close" placement="bottom">
<a onClick={this.closeDrawer}>
<CloseOutlinedIcon />
</a>
</Tooltip>
</div>
{/* iframe */}
{!this.state.error && (
<iframe
ref={this.iframeRef}
title="Redash Help"
src="about:blank"
className={cx({ ready: !this.state.loading })}
onLoad={this.onIframeLoaded}
/>
)}
{/* loading indicator */}
{this.state.loading && (
<BigMessage icon="fa-spinner fa-2x fa-pulse" message="Loading..." className="help-message" />
)}
{/* error message */}
{this.state.error && (
<BigMessage icon="fa-exclamation-circle" className="help-message">
Something went wrong.
<br />
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<Link href={this.state.error} target="_blank" rel="noopener">
Click here
</Link>{" "}
to open the page in a new window.
</BigMessage>
)}
</div>
{/* extra content */}
<DynamicComponent name="HelpDrawerExtraContent" onLeave={this.closeDrawer} openPageUrl={this.loadIframe} />
</Drawer>
</React.Fragment>
);
}
}

View File

@@ -0,0 +1,282 @@
import { startsWith, get, some, mapValues } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Tooltip from "antd/lib/tooltip";
import Drawer from "antd/lib/drawer";
import Link from "@/components/Link";
import CloseOutlinedIcon from "@ant-design/icons/CloseOutlined";
import BigMessage from "@/components/BigMessage";
import DynamicComponent, { registerComponent } from "@/components/DynamicComponent";
import "./HelpTrigger.less";
const DOMAIN = "https://redash.io";
const HELP_PATH = "/help";
const IFRAME_TIMEOUT = 20000;
const IFRAME_URL_UPDATE_MESSAGE = "iframe_url";
export const TYPES = mapValues(
{
HOME: ["", "Help"],
VALUE_SOURCE_OPTIONS: ["/user-guide/querying/query-parameters#Value-Source-Options", "Guide: Value Source Options"],
SHARE_DASHBOARD: ["/user-guide/dashboards/sharing-dashboards", "Guide: Sharing and Embedding Dashboards"],
AUTHENTICATION_OPTIONS: ["/user-guide/users/authentication-options", "Guide: Authentication Options"],
USAGE_DATA_SHARING: ["/open-source/admin-guide/usage-data", "Help: Anonymous Usage Data Sharing"],
DS_ATHENA: ["/data-sources/amazon-athena-setup", "Guide: Help Setting up Amazon Athena"],
DS_BIGQUERY: ["/data-sources/bigquery-setup", "Guide: Help Setting up BigQuery"],
DS_URL: ["/data-sources/querying-urls", "Guide: Help Setting up URL"],
DS_MONGODB: ["/data-sources/mongodb-setup", "Guide: Help Setting up MongoDB"],
DS_GOOGLE_SPREADSHEETS: [
"/data-sources/querying-a-google-spreadsheet",
"Guide: Help Setting up Google Spreadsheets",
],
DS_GOOGLE_ANALYTICS: ["/data-sources/google-analytics-setup", "Guide: Help Setting up Google Analytics"],
DS_AXIBASETSD: ["/data-sources/axibase-time-series-database", "Guide: Help Setting up Axibase Time Series"],
DS_RESULTS: ["/user-guide/querying/query-results-data-source", "Guide: Help Setting up Query Results"],
ALERT_SETUP: ["/user-guide/alerts/setting-up-an-alert", "Guide: Setting Up a New Alert"],
MAIL_CONFIG: ["/open-source/setup/#Mail-Configuration", "Guide: Mail Configuration"],
ALERT_NOTIF_TEMPLATE_GUIDE: ["/user-guide/alerts/custom-alert-notifications", "Guide: Custom Alerts Notifications"],
FAVORITES: ["/user-guide/querying/favorites-tagging/#Favorites", "Guide: Favorites"],
MANAGE_PERMISSIONS: [
"/user-guide/querying/writing-queries#Managing-Query-Permissions",
"Guide: Managing Query Permissions",
],
NUMBER_FORMAT_SPECS: ["/user-guide/visualizations/formatting-numbers", "Formatting Numbers"],
GETTING_STARTED: ["/user-guide/getting-started", "Guide: Getting Started"],
DASHBOARDS: ["/user-guide/dashboards", "Guide: Dashboards"],
QUERIES: ["/help/user-guide/querying", "Guide: Queries"],
ALERTS: ["/user-guide/alerts", "Guide: Alerts"],
},
([url, title]) => [DOMAIN + HELP_PATH + url, title]
);
type OwnProps = {
type?: string;
href?: string;
title?: React.ReactNode;
className?: string;
showTooltip?: boolean;
renderAsLink?: boolean;
children?: React.ReactNode;
};
const HelpTriggerPropTypes = {
type: PropTypes.string,
href: PropTypes.string,
title: PropTypes.node,
className: PropTypes.string,
showTooltip: PropTypes.bool,
renderAsLink: PropTypes.bool,
children: PropTypes.node,
};
const HelpTriggerDefaultProps = {
type: null,
href: null,
title: null,
className: null,
showTooltip: true,
renderAsLink: false,
children: <i className="fa fa-question-circle" />,
};
export function helpTriggerWithTypes(types: any, allowedDomains = [], drawerClassName = null) {
return class HelpTrigger extends React.Component {
static propTypes = {
...HelpTriggerPropTypes,
type: PropTypes.oneOf(Object.keys(types)),
};
static defaultProps = HelpTriggerDefaultProps;
iframeRef = React.createRef();
iframeLoadingTimeout = null;
state = {
visible: false,
loading: false,
error: false,
currentUrl: null,
};
componentDidMount() {
window.addEventListener("message", this.onPostMessageReceived, false);
}
componentWillUnmount() {
window.removeEventListener("message", this.onPostMessageReceived);
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
clearTimeout(this.iframeLoadingTimeout);
}
loadIframe = (url: any) => {
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
clearTimeout(this.iframeLoadingTimeout);
this.setState({ loading: true, error: false });
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
this.iframeRef.current.src = url;
// @ts-expect-error ts-migrate(2322) FIXME: Type 'number' is not assignable to type 'null'.
this.iframeLoadingTimeout = setTimeout(() => {
this.setState({ error: url, loading: false });
}, IFRAME_TIMEOUT); // safety
};
onIframeLoaded = () => {
this.setState({ loading: false });
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
clearTimeout(this.iframeLoadingTimeout);
};
onPostMessageReceived = (event: any) => {
if (!some(allowedDomains, domain => startsWith(event.origin, domain))) {
return;
}
const { type, message: currentUrl } = event.data || {};
if (type !== IFRAME_URL_UPDATE_MESSAGE) {
return;
}
this.setState({ currentUrl });
};
getUrl = () => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'Readonly<{... Remove this comment to see the full error message
const helpTriggerType = get(types, this.props.type);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'href' does not exist on type 'Readonly<{... Remove this comment to see the full error message
return helpTriggerType ? helpTriggerType[0] : this.props.href;
};
openDrawer = (e: any) => {
// keep "open in new tab" behavior
if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
this.setState({ visible: true });
// wait for drawer animation to complete so there's no animation jank
setTimeout(() => this.loadIframe(this.getUrl()), 300);
}
};
closeDrawer = (event: any) => {
if (event) {
event.preventDefault();
}
this.setState({ visible: false });
this.setState({ visible: false, currentUrl: null });
};
render() {
const targetUrl = this.getUrl();
if (!targetUrl) {
return null;
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'Readonly<{... Remove this comment to see the full error message
const tooltip = get(types, `${this.props.type}[1]`, this.props.title);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type 'Reado... Remove this comment to see the full error message
const className = cx("help-trigger", this.props.className);
const url = this.state.currentUrl;
const isAllowedDomain = some(allowedDomains, domain => startsWith(url || targetUrl, domain));
// @ts-expect-error ts-migrate(2339) FIXME: Property 'renderAsLink' does not exist on type 'Re... Remove this comment to see the full error message
const shouldRenderAsLink = this.props.renderAsLink || !isAllowedDomain;
return (
<React.Fragment>
<Tooltip
title={
// @ts-expect-error ts-migrate(2339) FIXME: Property 'showTooltip' does not exist on type 'Rea... Remove this comment to see the full error message
this.props.showTooltip ? (
<>
{tooltip}
{shouldRenderAsLink && <i className="fa fa-external-link" style={{ marginLeft: 5 }} />}
</>
) : null
}>
<Link
href={url || this.getUrl()}
className={className}
rel="noopener noreferrer"
target="_blank"
onClick={shouldRenderAsLink ? () => {} : this.openDrawer}>
{this.props.children}
</Link>
</Tooltip>
<Drawer
placement="right"
closable={false}
onClose={this.closeDrawer}
visible={this.state.visible}
className={cx("help-drawer", drawerClassName)}
destroyOnClose
width={400}>
<div className="drawer-wrapper">
<div className="drawer-menu">
{url && (
<Tooltip title="Open page in a new window" placement="left">
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<Link href={url} target="_blank">
<i className="fa fa-external-link" />
</Link>
</Tooltip>
)}
<Tooltip title="Close" placement="bottom">
<a onClick={this.closeDrawer}>
<CloseOutlinedIcon />
</a>
</Tooltip>
</div>
{/* iframe */}
{!this.state.error && (
<iframe
// @ts-expect-error ts-migrate(2322) FIXME: Type 'RefObject<unknown>' is not assignable to typ... Remove this comment to see the full error message
ref={this.iframeRef}
title="Usage Help"
src="about:blank"
className={cx({ ready: !this.state.loading })}
onLoad={this.onIframeLoaded}
/>
)}
{/* loading indicator */}
{this.state.loading && (
<BigMessage icon="fa-spinner fa-2x fa-pulse" message="Loading..." className="help-message" />
)}
{/* error message */}
{this.state.error && (
// @ts-expect-error ts-migrate(2746) FIXME: This JSX tag's 'children' prop expects a single ch... Remove this comment to see the full error message
<BigMessage icon="fa-exclamation-circle" className="help-message">
Something went wrong.
<br />
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<Link href={this.state.error} target="_blank" rel="noopener">
Click here
</Link>{" "}
to open the page in a new window.
</BigMessage>
)}
</div>
{/* extra content */}
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<DynamicComponent name="HelpDrawerExtraContent" onLeave={this.closeDrawer} openPageUrl={this.loadIframe} />
</Drawer>
</React.Fragment>
);
}
};
}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'never'.
registerComponent("HelpTrigger", helpTriggerWithTypes(TYPES, [DOMAIN]));
type Props = OwnProps & typeof HelpTriggerDefaultProps;
export default function HelpTrigger(props: Props) {
return <DynamicComponent {...props} name="HelpTrigger" />;
}
HelpTrigger.defaultProps = HelpTriggerDefaultProps;

View File

@@ -3,8 +3,13 @@ import Input from "antd/lib/input";
import CopyOutlinedIcon from "@ant-design/icons/CopyOutlined";
import Tooltip from "antd/lib/tooltip";
export default class InputWithCopy extends React.Component {
constructor(props) {
type State = any;
export default class InputWithCopy extends React.Component<{}, State> {
copyFeatureSupported: any;
ref: any;
resetCopyState: any;
constructor(props: {}) {
super(props);
this.state = { copied: null };
this.ref = React.createRef();

View File

@@ -1,21 +1,21 @@
import React from "react";
import Button from "antd/lib/button";
function DefaultLinkComponent(props) {
function DefaultLinkComponent(props: any) {
return <a {...props} />; // eslint-disable-line jsx-a11y/anchor-has-content
}
function Link(props) {
function Link(props: any) {
return <Link.Component {...props} />;
}
Link.Component = DefaultLinkComponent;
function DefaultButtonLinkComponent(props) {
return <Button {...props} />;
function DefaultButtonLinkComponent(props: any) {
return <Button role="button" {...props} />;
}
function ButtonLink(props) {
function ButtonLink(props: any) {
return <ButtonLink.Component {...props} />;
}

View File

@@ -1,18 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import BigMessage from "@/components/BigMessage";
import { TagsControl } from "@/components/tags-control/TagsControl";
export default function NoTaggedObjectsFound({ objectType, tags }) {
return (
<BigMessage icon="fa-tags">
No {objectType} found tagged with&nbsp;
<TagsControl className="inline-tags-control" tags={Array.from(tags)} tagSeparator={"+"} />.
</BigMessage>
);
}
NoTaggedObjectsFound.propTypes = {
objectType: PropTypes.string.isRequired,
tags: PropTypes.oneOfType([PropTypes.array, PropTypes.objectOf(Set)]).isRequired,
};

View File

@@ -0,0 +1,22 @@
import React from "react";
import BigMessage from "@/components/BigMessage";
import { TagsControl } from "@/components/tags-control/TagsControl";
type Props = {
objectType: string;
tags: any[] | {
// @ts-expect-error ts-migrate(2314) FIXME: Generic type 'Set<T>' requires 1 type argument(s).
[key: string]: Set;
};
};
export default function NoTaggedObjectsFound({ objectType, tags }: Props) {
return (
// @ts-expect-error ts-migrate(2746) FIXME: This JSX tag's 'children' prop expects a single ch... Remove this comment to see the full error message
<BigMessage icon="fa-tags">
No {objectType} found tagged with&nbsp;
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<TagsControl className="inline-tags-control" tags={Array.from(tags)} tagSeparator={"+"} />.
</BigMessage>
);
}

View File

@@ -1,9 +1,15 @@
import React from "react";
import PropTypes from "prop-types";
import "./index.less";
export default function PageHeader({ title, actions }) {
type OwnProps = {
title?: string;
actions?: React.ReactNode;
};
type Props = OwnProps & typeof PageHeader.defaultProps;
export default function PageHeader({ title, actions }: Props) {
return (
<div className="page-header-wrapper">
<h3>{title}</h3>
@@ -12,11 +18,6 @@ export default function PageHeader({ title, actions }) {
);
}
PageHeader.propTypes = {
title: PropTypes.string,
actions: PropTypes.node,
};
PageHeader.defaultProps = {
title: "",
actions: null,

View File

@@ -1,10 +1,20 @@
import React from "react";
import PropTypes from "prop-types";
import Pagination from "antd/lib/pagination";
const MIN_ITEMS_PER_PAGE = 5;
export default function Paginator({ page, showPageSizeSelect, pageSize, onPageSizeChange, totalCount, onChange }) {
type OwnProps = {
page: number;
showPageSizeSelect?: boolean;
pageSize: number;
totalCount: number;
onPageSizeChange?: (...args: any[]) => any;
onChange?: (...args: any[]) => any;
};
type Props = OwnProps & typeof Paginator.defaultProps;
export default function Paginator({ page, showPageSizeSelect, pageSize, onPageSizeChange, totalCount, onChange }: Props) {
if (totalCount <= (showPageSizeSelect ? MIN_ITEMS_PER_PAGE : pageSize)) {
return null;
}
@@ -23,15 +33,6 @@ export default function Paginator({ page, showPageSizeSelect, pageSize, onPageSi
);
}
Paginator.propTypes = {
page: PropTypes.number.isRequired,
showPageSizeSelect: PropTypes.bool,
pageSize: PropTypes.number.isRequired,
totalCount: PropTypes.number.isRequired,
onPageSizeChange: PropTypes.func,
onChange: PropTypes.func,
};
Paginator.defaultProps = {
showPageSizeSelect: false,
onChange: () => {},

View File

@@ -1,11 +1,15 @@
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Badge from "antd/lib/badge";
import Tooltip from "antd/lib/tooltip";
import KeyboardShortcuts from "@/services/KeyboardShortcuts";
function ParameterApplyButton({ paramCount, onClick }) {
type Props = {
onClick: (...args: any[]) => any;
paramCount: number;
};
function ParameterApplyButton({ paramCount, onClick }: Props) {
// show spinner when count is empty so the fade out is consistent
const icon = !paramCount ? "spinner fa-pulse" : "check";
@@ -24,9 +28,4 @@ function ParameterApplyButton({ paramCount, onClick }) {
);
}
ParameterApplyButton.propTypes = {
onClick: PropTypes.func.isRequired,
paramCount: PropTypes.number.isRequired,
};
export default ParameterApplyButton;

View File

@@ -2,7 +2,6 @@
import { isString, extend, each, has, map, includes, findIndex, find, fromPairs, clone, isEmpty } from "lodash";
import React, { Fragment } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Select from "antd/lib/select";
import Table from "antd/lib/table";
@@ -25,8 +24,6 @@ import CheckOutlinedIcon from "@ant-design/icons/CheckOutlined";
import "./ParameterMappingInput.less";
const { Option } = Select;
export const MappingType = {
DashboardAddNew: "dashboard-add-new",
DashboardMapToExisting: "dashboard-map-to-existing",
@@ -34,7 +31,7 @@ export const MappingType = {
StaticValue: "static-value",
};
export function parameterMappingsToEditableMappings(mappings, parameters, existingParameterNames = []) {
export function parameterMappingsToEditableMappings(mappings: any, parameters: any, existingParameterNames = []) {
return map(mappings, mapping => {
const result = extend({}, mapping);
const alreadyExists = includes(existingParameterNames, mapping.mapTo);
@@ -59,7 +56,7 @@ export function parameterMappingsToEditableMappings(mappings, parameters, existi
});
}
export function editableMappingsToParameterMappings(mappings) {
export function editableMappingsToParameterMappings(mappings: any) {
return fromPairs(
map(
// convert to map
@@ -94,8 +91,8 @@ export function editableMappingsToParameterMappings(mappings) {
);
}
export function synchronizeWidgetTitles(sourceMappings, widgets) {
const affectedWidgets = [];
export function synchronizeWidgetTitles(sourceMappings: any, widgets: any) {
const affectedWidgets: any = [];
each(sourceMappings, sourceMapping => {
if (sourceMapping.type === ParameterMappingType.DashboardLevel) {
@@ -121,13 +118,16 @@ export function synchronizeWidgetTitles(sourceMappings, widgets) {
return affectedWidgets;
}
export class ParameterMappingInput extends React.Component {
static propTypes = {
mapping: PropTypes.object, // eslint-disable-line react/forbid-prop-types
existingParamNames: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func,
inputError: PropTypes.string,
};
type OwnParameterMappingInputProps = {
mapping?: any;
existingParamNames?: string[];
onChange?: (...args: any[]) => any;
inputError?: string;
};
type ParameterMappingInputProps = OwnParameterMappingInputProps & typeof ParameterMappingInput.defaultProps;
export class ParameterMappingInput extends React.Component<ParameterMappingInputProps> {
static defaultProps = {
mapping: {},
@@ -142,7 +142,7 @@ export class ParameterMappingInput extends React.Component {
className: "form-item",
};
updateSourceType = type => {
updateSourceType = (type: any) => {
let {
mapping: { mapTo },
} = this.props;
@@ -157,26 +157,34 @@ export class ParameterMappingInput extends React.Component {
this.updateParamMapping({ type, mapTo });
};
updateParamMapping = update => {
updateParamMapping = (update: any) => {
const { onChange, mapping } = this.props;
const newMapping = extend({}, mapping, update);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type 'never'.
if (newMapping.value !== mapping.value) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'param' does not exist on type 'never'.
newMapping.param = cloneParameter(newMapping.param);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'param' does not exist on type 'never'.
newMapping.param.setValue(newMapping.value);
}
if (has(update, "type")) {
if (update.type === MappingType.StaticValue) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type 'never'.
newMapping.value = newMapping.param.value;
} else {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type 'never'.
newMapping.value = null;
}
}
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
onChange(newMapping);
};
renderMappingTypeSelector() {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'existingParamNames' does not exist on ty... Remove this comment to see the full error message
const noExisting = isEmpty(this.props.existingParamNames);
return (
// @ts-expect-error ts-migrate(2339) FIXME: Property 'mapping' does not exist on type 'never'.
<Radio.Group value={this.props.mapping.type} onChange={e => this.updateSourceType(e.target.value)}>
<Radio className="radio" value={MappingType.DashboardAddNew} data-test="NewDashboardParameterOption">
New dashboard parameter
@@ -208,37 +216,35 @@ export class ParameterMappingInput extends React.Component {
renderDashboardMapToExisting() {
const { mapping, existingParamNames } = this.props;
const options = map(existingParamNames, paramName => ({ label: paramName, value: paramName }));
return (
<Select
value={mapping.mapTo}
onChange={mapTo => this.updateParamMapping({ mapTo })}
dropdownMatchSelectWidth={false}>
{map(existingParamNames, name => (
<Option value={name} key={name}>
{name}
</Option>
))}
</Select>
);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'mapTo' does not exist on type 'never'.
return <Select value={mapping.mapTo} onChange={mapTo => this.updateParamMapping({ mapTo })} options={options} />;
}
renderStaticValue() {
const { mapping } = this.props;
return (
<ParameterValueInput
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
type={mapping.param.type}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
value={mapping.param.normalizedValue}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
enumOptions={mapping.param.enumOptions}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
queryId={mapping.param.queryId}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
parameter={mapping.param}
onSelect={value => this.updateParamMapping({ value })}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(value: any) => void' is not assignable to t... Remove this comment to see the full error message
onSelect={(value: any) => this.updateParamMapping({ value })}
/>
);
}
renderInputBlock() {
const { mapping } = this.props;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'never'.
switch (mapping.type) {
case MappingType.DashboardAddNew:
return ["Key", "Enter a new parameter keyword", this.renderDashboardAddNew()];
@@ -274,14 +280,17 @@ export class ParameterMappingInput extends React.Component {
}
}
class MappingEditor extends React.Component {
static propTypes = {
mapping: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
existingParamNames: PropTypes.arrayOf(PropTypes.string).isRequired,
onChange: PropTypes.func.isRequired,
};
type MappingEditorProps = {
mapping: any;
existingParamNames: string[];
onChange: (...args: any[]) => any;
};
constructor(props) {
type MappingEditorState = any;
class MappingEditor extends React.Component<MappingEditorProps, MappingEditorState> {
constructor(props: MappingEditorProps) {
super(props);
this.state = {
visible: false,
@@ -290,12 +299,12 @@ class MappingEditor extends React.Component {
};
}
onVisibleChange = visible => {
onVisibleChange = (visible: any) => {
if (visible) this.show();
else this.hide();
};
onChange = mapping => {
onChange = (mapping: any) => {
let inputError = null;
if (mapping.type === MappingType.DashboardAddNew) {
@@ -331,8 +340,10 @@ class MappingEditor extends React.Component {
return (
<div className="parameter-mapping-editor" data-test="EditParamMappingPopover">
<header>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'never'. */}
Edit Source and Value <HelpTrigger type="VALUE_SOURCE_OPTIONS" />
</header>
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<ParameterMappingInput
mapping={mapping}
existingParamNames={this.props.existingParamNames}
@@ -358,7 +369,7 @@ class MappingEditor extends React.Component {
content={this.renderContent()}
visible={visible}
onVisibleChange={this.onVisibleChange}>
<Button size="small" type="dashed" data-test={`EditParamMappingButon-${mapping.param.name}`}>
<Button size="small" type="dashed" data-test={`EditParamMappingButton-${mapping.param.name}`}>
<EditOutlinedIcon />
</Button>
</Popover>
@@ -366,12 +377,17 @@ class MappingEditor extends React.Component {
}
}
class TitleEditor extends React.Component {
static propTypes = {
existingParams: PropTypes.arrayOf(PropTypes.object),
mapping: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onChange: PropTypes.func.isRequired,
};
type OwnTitleEditorProps = {
existingParams?: any[];
mapping: any;
onChange: (...args: any[]) => any;
};
type TitleEditorState = any;
type TitleEditorProps = OwnTitleEditorProps & typeof TitleEditor.defaultProps;
class TitleEditor extends React.Component<TitleEditorProps, TitleEditorState> {
static defaultProps = {
existingParams: [],
@@ -382,14 +398,14 @@ class TitleEditor extends React.Component {
title: "", // will be set on editing
};
onPopupVisibleChange = showPopup => {
onPopupVisibleChange = (showPopup: any) => {
this.setState({
showPopup,
title: showPopup ? this.getMappingTitle() : "",
});
};
onEditingTitleChange = event => {
onEditingTitleChange = (event: any) => {
this.setState({ title: event.target.value });
};
@@ -484,12 +500,15 @@ class TitleEditor extends React.Component {
}
}
export class ParameterMappingListInput extends React.Component {
static propTypes = {
mappings: PropTypes.arrayOf(PropTypes.object),
existingParams: PropTypes.arrayOf(PropTypes.object),
onChange: PropTypes.func,
};
type OwnParameterMappingListInputProps = {
mappings?: any[];
existingParams?: any[];
onChange?: (...args: any[]) => any;
};
type ParameterMappingListInputProps = OwnParameterMappingListInputProps & typeof ParameterMappingListInput.defaultProps;
export class ParameterMappingListInput extends React.Component<ParameterMappingListInputProps> {
static defaultProps = {
mappings: [],
@@ -497,7 +516,8 @@ export class ParameterMappingListInput extends React.Component {
onChange: () => {},
};
static getStringValue(value) {
// @ts-expect-error ts-migrate(7023) FIXME: 'getStringValue' implicitly has return type 'any' ... Remove this comment to see the full error message
static getStringValue(value: any) {
// null
if (!value) {
return "";
@@ -517,7 +537,7 @@ export class ParameterMappingListInput extends React.Component {
return value.toString();
}
static getDefaultValue(mapping, existingParams) {
static getDefaultValue(mapping: any, existingParams: any) {
const { type, mapTo, name } = mapping;
let { param } = mapping;
@@ -544,7 +564,10 @@ export class ParameterMappingListInput extends React.Component {
return this.getStringValue(value);
}
static getSourceTypeLabel({ type, mapTo }) {
static getSourceTypeLabel({
type,
mapTo
}: any) {
switch (type) {
case MappingType.DashboardAddNew:
case MappingType.DashboardMapToExisting:
@@ -562,13 +585,15 @@ export class ParameterMappingListInput extends React.Component {
}
}
updateParamMapping(oldMapping, newMapping) {
updateParamMapping(oldMapping: any, newMapping: any) {
const mappings = [...this.props.mappings];
const index = findIndex(mappings, oldMapping);
if (index >= 0) {
// This should be the only possible case, but need to handle `else` too
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
mappings[index] = newMapping;
} else {
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
mappings.push(newMapping);
}
this.props.onChange(mappings);
@@ -604,6 +629,7 @@ export class ParameterMappingListInput extends React.Component {
title="Default Value"
dataIndex="mapping"
key="value"
// @ts-expect-error ts-migrate(2339) FIXME: Property 'getDefaultValue' does not exist on type ... Remove this comment to see the full error message
render={mapping => this.constructor.getDefaultValue(mapping, this.props.existingParams)}
/>
<Table.Column
@@ -617,6 +643,7 @@ export class ParameterMappingListInput extends React.Component {
return (
<Fragment>
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'getSourceTypeLabel' does not exist on ty... Remove this comment to see the full error message */}
{this.constructor.getSourceTypeLabel(mapping)}{" "}
<MappingEditor
mapping={mapping}

View File

@@ -1,7 +1,6 @@
import { isEqual, isEmpty } from "lodash";
import { isEqual, isEmpty, map } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
import SelectWithVirtualScroll from "@/components/SelectWithVirtualScroll";
import Input from "antd/lib/input";
import InputNumber from "antd/lib/input-number";
import DateParameter from "@/components/dynamic-parameters/DateParameter";
@@ -10,24 +9,27 @@ import QueryBasedParameterInput from "./QueryBasedParameterInput";
import "./ParameterValueInput.less";
const { Option } = Select;
const multipleValuesProps = {
maxTagCount: 3,
maxTagTextLength: 10,
maxTagPlaceholder: num => `+${num.length} more`,
maxTagPlaceholder: (num: any) => `+${num.length} more`,
};
class ParameterValueInput extends React.Component {
static propTypes = {
type: PropTypes.string,
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
enumOptions: PropTypes.string,
queryId: PropTypes.number,
parameter: PropTypes.any, // eslint-disable-line react/forbid-prop-types
onSelect: PropTypes.func,
className: PropTypes.string,
};
type OwnProps = {
type?: string;
value?: any;
enumOptions?: string;
queryId?: number;
parameter?: any;
onSelect?: (...args: any[]) => any;
className?: string;
};
type State = any;
type Props = OwnProps & typeof ParameterValueInput.defaultProps;
class ParameterValueInput extends React.Component<Props, State> {
static defaultProps = {
type: "text",
@@ -39,28 +41,34 @@ class ParameterValueInput extends React.Component {
className: "",
};
constructor(props) {
constructor(props: Props) {
super(props);
this.state = {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parameter' does not exist on type 'never... Remove this comment to see the full error message
value: props.parameter.hasPendingValue ? props.parameter.pendingValue : props.value,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parameter' does not exist on type 'never... Remove this comment to see the full error message
isDirty: props.parameter.hasPendingValue,
};
}
componentDidUpdate = prevProps => {
componentDidUpdate = (prevProps: any) => {
const { value, parameter } = this.props;
// if value prop updated, reset dirty state
if (prevProps.value !== value || prevProps.parameter !== parameter) {
this.setState({
// @ts-expect-error ts-migrate(2339) FIXME: Property 'hasPendingValue' does not exist on type ... Remove this comment to see the full error message
value: parameter.hasPendingValue ? parameter.pendingValue : value,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'hasPendingValue' does not exist on type ... Remove this comment to see the full error message
isDirty: parameter.hasPendingValue,
});
}
};
onSelect = value => {
onSelect = (value: any) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type 'never'.
const isDirty = !isEqual(value, this.props.value);
this.setState({ value, isDirty });
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onSelect' does not exist on type 'never'... Remove this comment to see the full error message
this.props.onSelect(value, isDirty);
};
@@ -70,9 +78,11 @@ class ParameterValueInput extends React.Component {
return (
<DateParameter
type={type}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type 'never... Remove this comment to see the full error message
className={this.props.className}
value={value}
parameter={parameter}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(value: any) => void' is not assignable to t... Remove this comment to see the full error message
onSelect={this.onSelect}
/>
);
@@ -84,9 +94,11 @@ class ParameterValueInput extends React.Component {
return (
<DateRangeParameter
type={type}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type 'never... Remove this comment to see the full error message
className={this.props.className}
value={value}
parameter={parameter}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(value: any) => void' is not assignable to t... Remove this comment to see the full error message
onSelect={this.onSelect}
/>
);
@@ -95,28 +107,27 @@ class ParameterValueInput extends React.Component {
renderEnumInput() {
const { enumOptions, parameter } = this.props;
const { value } = this.state;
const enumOptionsArray = enumOptions.split("\n").filter(v => v !== "");
// @ts-expect-error ts-migrate(2339) FIXME: Property 'split' does not exist on type 'never'.
const enumOptionsArray = enumOptions.split("\n").filter((v: any) => v !== "");
// Antd Select doesn't handle null in multiple mode
const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'multiValuesOptions' does not exist on ty... Remove this comment to see the full error message
const normalize = (val: any) => parameter.multiValuesOptions && val === null ? [] : val;
return (
<Select
<SelectWithVirtualScroll
// @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type 'never... Remove this comment to see the full error message
className={this.props.className}
// @ts-expect-error ts-migrate(2322) FIXME: Type '"multiple" | "default"' is not assignable to... Remove this comment to see the full error message
mode={parameter.multiValuesOptions ? "multiple" : "default"}
optionFilterProp="children"
value={normalize(value)}
onChange={this.onSelect}
dropdownMatchSelectWidth={false}
options={map(enumOptionsArray, opt => ({ label: String(opt), value: opt }))}
showSearch
showArrow
style={{ minWidth: 60 }}
notFoundContent={isEmpty(enumOptionsArray) ? "No options available" : null}
{...multipleValuesProps}>
{enumOptionsArray.map(option => (
<Option key={option} value={option}>
{option}
</Option>
))}
</Select>
{...multipleValuesProps}
/>
);
}
@@ -125,13 +136,19 @@ class ParameterValueInput extends React.Component {
const { value } = this.state;
return (
<QueryBasedParameterInput
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
className={this.props.className}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'never'.
mode={parameter.multiValuesOptions ? "multiple" : "default"}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'never'.
optionFilterProp="children"
parameter={parameter}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
value={value}
queryId={queryId}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(value: any) => void' is not assignable to t... Remove this comment to see the full error message
onSelect={this.onSelect}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'number' is not assignable to type 'never'.
style={{ minWidth: 60 }}
{...multipleValuesProps}
/>
@@ -142,7 +159,7 @@ class ParameterValueInput extends React.Component {
const { className } = this.props;
const { value } = this.state;
const normalize = val => (isNaN(val) ? undefined : val);
const normalize = (val: any) => isNaN(val) ? undefined : val;
return (
<InputNumber className={className} value={normalize(value)} onChange={val => this.onSelect(normalize(val))} />

View File

@@ -1,8 +1,8 @@
import { size, filter, forEach, extend } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import { SortableContainer, SortableElement, DragHandle } from "@redash/viz/lib/components/sortable";
import location from "@/services/location";
// @ts-expect-error ts-migrate(6133) FIXME: 'Parameter' is declared but its value is never rea... Remove this comment to see the full error message
import { Parameter, createParameter } from "@/services/parameters";
import ParameterApplyButton from "@/components/ParameterApplyButton";
import ParameterValueInput from "@/components/ParameterValueInput";
@@ -11,24 +11,28 @@ import { toHuman } from "@/lib/utils";
import "./Parameters.less";
function updateUrl(parameters) {
function updateUrl(parameters: any) {
const params = extend({}, location.search);
parameters.forEach(param => {
parameters.forEach((param: any) => {
extend(params, param.toUrlParams());
});
location.setSearch(params, true);
}
export default class Parameters extends React.Component {
static propTypes = {
parameters: PropTypes.arrayOf(PropTypes.instanceOf(Parameter)),
editable: PropTypes.bool,
disableUrlUpdate: PropTypes.bool,
onValuesChange: PropTypes.func,
onPendingValuesChange: PropTypes.func,
onParametersEdit: PropTypes.func,
};
type OwnProps = {
parameters?: any[]; // TODO: PropTypes.instanceOf(Parameter)
editable?: boolean;
disableUrlUpdate?: boolean;
onValuesChange?: (...args: any[]) => any;
onPendingValuesChange?: (...args: any[]) => any;
onParametersEdit?: (...args: any[]) => any;
};
type State = any;
type Props = OwnProps & typeof Parameters.defaultProps;
export default class Parameters extends React.Component<Props, State> {
static defaultProps = {
parameters: [],
editable: false,
@@ -38,7 +42,9 @@ export default class Parameters extends React.Component {
onParametersEdit: () => {},
};
constructor(props) {
onBeforeSortStart: any;
constructor(props: Props) {
super(props);
const { parameters } = props;
this.state = { parameters };
@@ -47,7 +53,7 @@ export default class Parameters extends React.Component {
}
}
componentDidUpdate = prevProps => {
componentDidUpdate = (prevProps: any) => {
const { parameters, disableUrlUpdate } = this.props;
const parametersChanged = prevProps.parameters !== parameters;
const disableUrlUpdateChanged = prevProps.disableUrlUpdate !== disableUrlUpdate;
@@ -59,7 +65,7 @@ export default class Parameters extends React.Component {
}
};
handleKeyDown = e => {
handleKeyDown = (e: any) => {
// Cmd/Ctrl/Alt + Enter
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) {
e.stopPropagation();
@@ -67,9 +73,11 @@ export default class Parameters extends React.Component {
}
};
setPendingValue = (param, value, isDirty) => {
setPendingValue = (param: any, value: any, isDirty: any) => {
const { onPendingValuesChange } = this.props;
this.setState(({ parameters }) => {
this.setState(({
parameters
}: any) => {
if (isDirty) {
param.setPendingValue(value);
} else {
@@ -80,10 +88,15 @@ export default class Parameters extends React.Component {
});
};
moveParameter = ({ oldIndex, newIndex }) => {
moveParameter = ({
oldIndex,
newIndex
}: any) => {
const { onParametersEdit } = this.props;
if (oldIndex !== newIndex) {
this.setState(({ parameters }) => {
this.setState(({
parameters
}: any) => {
parameters.splice(newIndex, 0, parameters.splice(oldIndex, 1)[0]);
onParametersEdit();
return { parameters };
@@ -93,8 +106,10 @@ export default class Parameters extends React.Component {
applyChanges = () => {
const { onValuesChange, disableUrlUpdate } = this.props;
this.setState(({ parameters }) => {
const parametersWithPendingValues = parameters.filter(p => p.hasPendingValue);
this.setState(({
parameters
}: any) => {
const parametersWithPendingValues = parameters.filter((p: any) => p.hasPendingValue);
forEach(parameters, p => p.applyPendingValue());
if (!disableUrlUpdate) {
updateUrl(parameters);
@@ -104,10 +119,12 @@ export default class Parameters extends React.Component {
});
};
showParameterSettings = (parameter, index) => {
showParameterSettings = (parameter: any, index: any) => {
const { onParametersEdit } = this.props;
EditParameterSettingsDialog.showModal({ parameter }).onClose(updated => {
this.setState(({ parameters }) => {
EditParameterSettingsDialog.showModal({ parameter }).onClose((updated: any) => {
this.setState(({
parameters
}: any) => {
const updatedParameter = extend(parameter, updated);
parameters[index] = createParameter(updatedParameter, updatedParameter.parentQueryId);
onParametersEdit();
@@ -116,7 +133,7 @@ export default class Parameters extends React.Component {
});
};
renderParameter(param, index) {
renderParameter(param: any, index: any) {
const { editable } = this.props;
return (
<div key={param.name} className="di-block" data-test={`ParameterName-${param.name}`}>
@@ -133,12 +150,18 @@ export default class Parameters extends React.Component {
)}
</div>
<ParameterValueInput
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
type={param.type}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
value={param.normalizedValue}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
parameter={param}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
enumOptions={param.enumOptions}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
queryId={param.queryId}
onSelect={(value, isDirty) => this.setPendingValue(param, value, isDirty)}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(value: any, isDirty: any) => void' is not a... Remove this comment to see the full error message
onSelect={(value: any, isDirty: any) => this.setPendingValue(param, value, isDirty)}
/>
</div>
);
@@ -149,6 +172,7 @@ export default class Parameters extends React.Component {
const { editable } = this.props;
const dirtyParamCount = size(filter(parameters, "hasPendingValue"));
return (
// @ts-expect-error ts-migrate(2746) FIXME: This JSX tag's 'children' prop expects a single ch... Remove this comment to see the full error message
<SortableContainer
disabled={!editable}
axis="xy"
@@ -161,7 +185,7 @@ export default class Parameters extends React.Component {
className: "parameter-container",
onKeyDown: dirtyParamCount ? this.handleKeyDown : null,
}}>
{parameters.map((param, index) => (
{parameters.map((param: any, index: any) => (
<SortableElement key={param.name} index={index}>
<div className="parameter-block" data-editable={editable || null}>
{editable && <DragHandle data-test={`DragHandle-${param.name}`} />}

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect, useCallback } from "react";
import { axios } from "@/services/axios";
import PropTypes from "prop-types";
import { each, debounce, get, find } from "lodash";
import Button from "antd/lib/button";
import List from "antd/lib/list";
@@ -8,6 +7,7 @@ import Modal from "antd/lib/modal";
import Select from "antd/lib/select";
import Tag from "antd/lib/tag";
import Tooltip from "antd/lib/tooltip";
// @ts-expect-error ts-migrate(6133) FIXME: 'DialogPropType' is declared but its value is neve... Remove this comment to see the full error message
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { toHuman } from "@/lib/utils";
import HelpTrigger from "@/components/HelpTrigger";
@@ -20,13 +20,13 @@ import "./index.less";
const { Option } = Select;
const DEBOUNCE_SEARCH_DURATION = 200;
function useGrantees(url) {
function useGrantees(url: any) {
const loadGrantees = useCallback(
() =>
axios.get(url).then(data => {
const resultGrantees = [];
const resultGrantees: any = [];
each(data, (grantees, accessType) => {
grantees.forEach(grantee => {
grantees.forEach((grantee: any) => {
grantee.accessType = toHuman(accessType);
resultGrantees.push(grantee);
});
@@ -40,6 +40,7 @@ function useGrantees(url) {
(userId, accessType = "modify") =>
axios
.post(url, { access_type: accessType, user_id: userId })
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
.catch(() => notification.error("Could not grant permission to the user")),
[url]
);
@@ -48,6 +49,7 @@ function useGrantees(url) {
(userId, accessType = "modify") =>
axios
.delete(url, { data: { access_type: accessType, user_id: userId } })
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
.catch(() => notification.error("Could not remove permission from the user")),
[url]
);
@@ -55,37 +57,48 @@ function useGrantees(url) {
return { loadGrantees, addPermission, removePermission };
}
const searchUsers = searchTerm =>
User.query({ q: searchTerm })
.then(({ results }) => results)
.catch(() => []);
const searchUsers = (searchTerm: any) => User.query({ q: searchTerm })
// @ts-expect-error ts-migrate(2339) FIXME: Property 'results' does not exist on type 'AxiosRe... Remove this comment to see the full error message
.then(({ results }) => results)
.catch(() => []);
function PermissionsEditorDialogHeader({ context }) {
type OwnPermissionsEditorDialogHeaderProps = {
context?: "query" | "dashboard";
};
type PermissionsEditorDialogHeaderProps = OwnPermissionsEditorDialogHeaderProps & typeof PermissionsEditorDialogHeader.defaultProps;
function PermissionsEditorDialogHeader({ context }: PermissionsEditorDialogHeaderProps) {
return (
<>
Manage Permissions
<div className="modal-header-desc">
{`Editing this ${context} is enabled for the users in this list and for admins. `}
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'never'. */}
<HelpTrigger type="MANAGE_PERMISSIONS" />
</div>
</>
);
}
PermissionsEditorDialogHeader.propTypes = { context: PropTypes.oneOf(["query", "dashboard"]) };
PermissionsEditorDialogHeader.defaultProps = { context: "query" };
function UserSelect({ onSelect, shouldShowUser }) {
type OwnUserSelectProps = {
onSelect?: (...args: any[]) => any;
shouldShowUser?: (...args: any[]) => any;
};
type UserSelectProps = OwnUserSelectProps & typeof UserSelect.defaultProps;
function UserSelect({ onSelect, shouldShowUser }: UserSelectProps) {
const [loadingUsers, setLoadingUsers] = useState(true);
const [users, setUsers] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchUsers = useCallback(
debounce(
search =>
searchUsers(search)
.then(setUsers)
.finally(() => setLoadingUsers(false)),
(search: any) => searchUsers(search)
.then(setUsers)
.finally(() => setLoadingUsers(false)),
DEBOUNCE_SEARCH_DURATION
),
[]
@@ -109,6 +122,7 @@ function UserSelect({ onSelect, shouldShowUser }) {
getPopupContainer={trigger => trigger.parentNode}
onSelect={onSelect}>
{users.filter(shouldShowUser).map(user => (
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
<Option key={user.id} value={user.id}>
<UserPreviewCard user={user} />
</Option>
@@ -116,14 +130,19 @@ function UserSelect({ onSelect, shouldShowUser }) {
</Select>
);
}
UserSelect.propTypes = {
onSelect: PropTypes.func,
shouldShowUser: PropTypes.func,
};
UserSelect.defaultProps = { onSelect: () => {}, shouldShowUser: () => true };
function PermissionsEditorDialog({ dialog, author, context, aclUrl }) {
type OwnPermissionsEditorDialogProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'DialogPropType' refers to a value, but is being u... Remove this comment to see the full error message
dialog: DialogPropType;
author: any;
context?: "query" | "dashboard";
aclUrl: string;
};
type PermissionsEditorDialogProps = OwnPermissionsEditorDialogProps & typeof PermissionsEditorDialog.defaultProps;
function PermissionsEditorDialog({ dialog, author, context, aclUrl }: PermissionsEditorDialogProps) {
const [loadingGrantees, setLoadingGrantees] = useState(true);
const [grantees, setGrantees] = useState([]);
const { loadGrantees, addPermission, removePermission } = useGrantees(aclUrl);
@@ -131,6 +150,7 @@ function PermissionsEditorDialog({ dialog, author, context, aclUrl }) {
setLoadingGrantees(true);
loadGrantees()
.then(setGrantees)
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
.catch(() => notification.error("Failed to load grantees list"))
.finally(() => setLoadingGrantees(false));
}, [loadGrantees]);
@@ -151,8 +171,10 @@ function PermissionsEditorDialog({ dialog, author, context, aclUrl }) {
title={<PermissionsEditorDialogHeader context={context} />}
footer={<Button onClick={dialog.dismiss}>Close</Button>}>
<UserSelect
onSelect={userId => addPermission(userId).then(loadUsersWithPermissions)}
shouldShowUser={user => !userHasPermission(user)}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(userId: any) => Promise<void>' is not assig... Remove this comment to see the full error message
onSelect={(userId: any) => addPermission(userId).then(loadUsersWithPermissions)}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(user: any) => boolean' is not assignable to... Remove this comment to see the full error message
shouldShowUser={(user: any) => !userHasPermission(user)}
/>
<div className="d-flex align-items-center m-t-5">
<h5 className="flex-fill">Users with permissions</h5>
@@ -165,6 +187,7 @@ function PermissionsEditorDialog({ dialog, author, context, aclUrl }) {
renderItem={user => (
<List.Item>
<UserPreviewCard key={user.id} user={user}>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'null | u... Remove this comment to see the full error message */}
{user.id === author.id ? (
<Tag className="m-0">Author</Tag>
) : (
@@ -184,13 +207,6 @@ function PermissionsEditorDialog({ dialog, author, context, aclUrl }) {
);
}
PermissionsEditorDialog.propTypes = {
dialog: DialogPropType.isRequired,
author: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
context: PropTypes.oneOf(["query", "dashboard"]),
aclUrl: PropTypes.string.isRequired,
};
PermissionsEditorDialog.defaultProps = { context: "query" };
export default wrapDialog(PermissionsEditorDialog);

View File

@@ -1,11 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Link from "@/components/Link";
type OwnPreviewCardProps = {
imageUrl: string;
title: React.ReactNode;
body?: React.ReactNode;
roundedImage?: boolean;
className?: string;
children?: React.ReactNode;
};
type PreviewCardProps = OwnPreviewCardProps & typeof PreviewCard.defaultProps;
// PreviewCard
export function PreviewCard({ imageUrl, roundedImage, title, body, children, className, ...props }) {
export function PreviewCard({ imageUrl, roundedImage, title, body, children, className, ...props }: PreviewCardProps) {
return (
<div {...props} className={className + " w-100 d-flex align-items-center"}>
<img
@@ -24,15 +34,6 @@ export function PreviewCard({ imageUrl, roundedImage, title, body, children, cla
);
}
PreviewCard.propTypes = {
imageUrl: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
body: PropTypes.node,
roundedImage: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.node,
};
PreviewCard.defaultProps = {
body: null,
roundedImage: true,
@@ -40,36 +41,52 @@ PreviewCard.defaultProps = {
children: null,
};
type OwnUserPreviewCardProps = {
user: {
profile_image_url: string;
name: string;
email: string;
};
withLink?: boolean;
children?: React.ReactNode;
};
type UserPreviewCardProps = OwnUserPreviewCardProps & typeof UserPreviewCard.defaultProps;
// UserPreviewCard
export function UserPreviewCard({ user, withLink, children, ...props }) {
export function UserPreviewCard({ user, withLink, children, ...props }: UserPreviewCardProps) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type '{ profile_im... Remove this comment to see the full error message
const title = withLink ? <Link href={"users/" + user.id}>{user.name}</Link> : user.name;
return (
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'null | un... Remove this comment to see the full error message
<PreviewCard {...props} imageUrl={user.profile_image_url} title={title} body={user.email}>
{children}
</PreviewCard>
);
}
UserPreviewCard.propTypes = {
user: PropTypes.shape({
profile_image_url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
}).isRequired,
withLink: PropTypes.bool,
children: PropTypes.node,
};
UserPreviewCard.defaultProps = {
withLink: false,
children: null,
};
type OwnDataSourcePreviewCardProps = {
dataSource: {
name: string;
type: string;
};
withLink?: boolean;
children?: React.ReactNode;
};
type DataSourcePreviewCardProps = OwnDataSourcePreviewCardProps & typeof DataSourcePreviewCard.defaultProps;
// DataSourcePreviewCard
export function DataSourcePreviewCard({ dataSource, withLink, children, ...props }) {
export function DataSourcePreviewCard({ dataSource, withLink, children, ...props }: DataSourcePreviewCardProps) {
const imageUrl = `static/images/db-logos/${dataSource.type}.png`;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type '{ name: stri... Remove this comment to see the full error message
const title = withLink ? <Link href={"data_sources/" + dataSource.id}>{dataSource.name}</Link> : dataSource.name;
return (
<PreviewCard {...props} imageUrl={imageUrl} title={title}>
@@ -78,15 +95,6 @@ export function DataSourcePreviewCard({ dataSource, withLink, children, ...props
);
}
DataSourcePreviewCard.propTypes = {
dataSource: PropTypes.shape({
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
}).isRequired,
withLink: PropTypes.bool,
children: PropTypes.node,
};
DataSourcePreviewCard.defaultProps = {
withLink: false,
children: null,

View File

@@ -1,19 +1,21 @@
import { find, isArray, get, first, map, intersection, isEqual, isEmpty } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
import SelectWithVirtualScroll from "@/components/SelectWithVirtualScroll";
const { Option } = Select;
type OwnProps = {
parameter?: any;
value?: any;
mode?: "default" | "multiple";
queryId?: number;
onSelect?: (...args: any[]) => any;
className?: string;
};
export default class QueryBasedParameterInput extends React.Component {
static propTypes = {
parameter: PropTypes.any, // eslint-disable-line react/forbid-prop-types
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
mode: PropTypes.oneOf(["default", "multiple"]),
queryId: PropTypes.number,
onSelect: PropTypes.func,
className: PropTypes.string,
};
type State = any;
type Props = OwnProps & typeof QueryBasedParameterInput.defaultProps;
export default class QueryBasedParameterInput extends React.Component<Props, State> {
static defaultProps = {
value: null,
@@ -24,7 +26,7 @@ export default class QueryBasedParameterInput extends React.Component {
className: "",
};
constructor(props) {
constructor(props: Props) {
super(props);
this.state = {
options: [],
@@ -34,20 +36,26 @@ export default class QueryBasedParameterInput extends React.Component {
}
componentDidMount() {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'queryId' does not exist on type 'never'.
this._loadOptions(this.props.queryId);
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: Props) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'queryId' does not exist on type 'never'.
if (this.props.queryId !== prevProps.queryId) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'queryId' does not exist on type 'never'.
this._loadOptions(this.props.queryId);
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type 'never'.
if (this.props.value !== prevProps.value) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type 'never'.
this.setValue(this.props.value);
}
}
setValue(value) {
setValue(value: any) {
const { options } = this.state;
// @ts-expect-error ts-migrate(2339) FIXME: Property 'mode' does not exist on type 'never'.
if (this.props.mode === "multiple") {
value = isArray(value) ? value : [value];
const optionValues = map(options, option => option.value);
@@ -55,22 +63,28 @@ export default class QueryBasedParameterInput extends React.Component {
this.setState({ value: validValues });
return validValues;
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type 'never'.
const found = find(options, option => option.value === this.props.value) !== undefined;
value = found ? value : get(first(options), "value");
this.setState({ value });
return value;
}
async _loadOptions(queryId) {
async _loadOptions(queryId: any) {
if (queryId && queryId !== this.state.queryId) {
this.setState({ loading: true });
// @ts-expect-error ts-migrate(2339) FIXME: Property 'parameter' does not exist on type 'never... Remove this comment to see the full error message
const options = await this.props.parameter.loadDropdownValues();
// stale queryId check
// @ts-expect-error ts-migrate(2339) FIXME: Property 'queryId' does not exist on type 'never'.
if (this.props.queryId === queryId) {
this.setState({ options, loading: false }, () => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type 'never'.
const updatedValue = this.setValue(this.props.value);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'value' does not exist on type 'never'.
if (!isEqual(updatedValue, this.props.value)) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onSelect' does not exist on type 'never'... Remove this comment to see the full error message
this.props.onSelect(updatedValue);
}
});
@@ -79,29 +93,25 @@ export default class QueryBasedParameterInput extends React.Component {
}
render() {
const { className, value, mode, onSelect, ...otherProps } = this.props;
// @ts-expect-error ts-migrate(2700) FIXME: Rest types may only be created from object types.
const { className, mode, onSelect, queryId, value, ...otherProps } = this.props;
const { loading, options } = this.state;
return (
<span>
<Select
<SelectWithVirtualScroll
className={className}
disabled={loading}
loading={loading}
mode={mode}
value={this.state.value}
onChange={onSelect}
dropdownMatchSelectWidth={false}
options={map(options, ({ value, name }) => ({ label: String(name), value }))}
optionFilterProp="children"
showSearch
showArrow
notFoundContent={isEmpty(options) ? "No options available" : null}
{...otherProps}>
{options.map(option => (
<Option value={option.value} key={option.value}>
{option.name}
</Option>
))}
</Select>
{...otherProps}
/>
</span>
);
}

View File

@@ -1,39 +1,44 @@
import React from "react";
import PropTypes from "prop-types";
import { VisualizationType } from "@redash/viz/lib";
import Link from "@/components/Link";
import VisualizationName from "@/components/visualizations/VisualizationName";
import "./QueryLink.less";
function QueryLink({ query, visualization, readOnly }) {
type OwnProps = {
query: any;
visualization?: VisualizationType;
readOnly?: boolean;
};
type Props = OwnProps & typeof QueryLink.defaultProps;
function QueryLink({ query, visualization, readOnly }: Props) {
const getUrl = () => {
let hash = null;
if (visualization) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'never'.
if (visualization.type === "TABLE") {
// link to hard-coded table tab instead of the (hidden) visualization tab
hash = "table";
} else {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'id' does not exist on type 'never'.
hash = visualization.id;
}
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'getUrl' does not exist on type 'never'.
return query.getUrl(false, hash);
};
return (
<Link href={readOnly ? null : getUrl()} className="query-link">
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'name' does not exist on type 'never'. */}
<VisualizationName visualization={visualization} /> <span>{query.name}</span>
</Link>
);
}
QueryLink.propTypes = {
query: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
visualization: VisualizationType,
readOnly: PropTypes.bool,
};
QueryLink.defaultProps = {
visualization: null,
readOnly: false,

View File

@@ -1,6 +1,5 @@
import { find } from "lodash";
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Input from "antd/lib/input";
import Select from "antd/lib/select";
@@ -10,23 +9,38 @@ import { QueryTagsControl } from "@/components/tags-control/TagsControl";
import useSearchResults from "@/lib/hooks/useSearchResults";
const { Option } = Select;
function search(term) {
function search(term: any) {
if (term === null) {
return Promise.resolve(null);
}
// get recent
if (!term) {
return Query.recent().then(results => results.filter(item => !item.is_draft)); // filter out draft
// @ts-expect-error ts-migrate(2339) FIXME: Property 'recent' does not exist on type 'typeof Q... Remove this comment to see the full error message
return Query.recent().then((results: any) => results.filter((item: any) => !item.is_draft)); // filter out draft
}
// search by query
return Query.query({ q: term }).then(({ results }) => results);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'query' does not exist on type 'typeof Qu... Remove this comment to see the full error message
return Query.query({ q: term }).then(({
results
}: any) => results);
}
export default function QuerySelector(props) {
type OwnProps = {
onChange: (...args: any[]) => any;
selectedQuery?: any;
type?: "select" | "default";
className?: string;
disabled?: boolean;
};
type Props = OwnProps & typeof QuerySelector.defaultProps;
export default function QuerySelector(props: Props) {
const [searchTerm, setSearchTerm] = useState("");
const [selectedQuery, setSelectedQuery] = useState();
// @ts-expect-error ts-migrate(2322) FIXME: Type 'never[]' is not assignable to type 'null | u... Remove this comment to see the full error message
const [doSearch, searchResults, searching] = useSearchResults(search, { initialResults: [] });
const placeholder = "Search a query by name";
@@ -34,57 +48,71 @@ export default function QuerySelector(props) {
const spinIcon = <i className={cx("fa fa-spinner fa-pulse hide-in-percy", { hidden: !searching })} />;
useEffect(() => {
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
doSearch(searchTerm);
}, [doSearch, searchTerm]);
// set selected from prop
useEffect(() => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'selectedQuery' does not exist on type 'n... Remove this comment to see the full error message
if (props.selectedQuery) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'selectedQuery' does not exist on type 'n... Remove this comment to see the full error message
setSelectedQuery(props.selectedQuery);
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'selectedQuery' does not exist on type 'n... Remove this comment to see the full error message
}, [props.selectedQuery]);
function selectQuery(queryId) {
function selectQuery(queryId: any) {
let query = null;
if (queryId) {
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
query = find(searchResults, { id: queryId });
if (!query) {
// shouldn't happen
// @ts-expect-error ts-migrate(2554) FIXME: Expected 1 arguments, but got 2.
notification.error("Something went wrong...", "Couldn't select query");
}
}
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | null' is not assignable... Remove this comment to see the full error message
setSearchTerm(query ? null : ""); // empty string triggers recent fetch
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message
setSelectedQuery(query);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onChange' does not exist on type 'never'... Remove this comment to see the full error message
props.onChange(query);
}
function renderResults() {
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
if (!searchResults.length) {
return <div className="text-muted">No results matching search term.</div>;
}
return (
<div className="list-group">
{searchResults.map(q => (
<a
className={cx("query-selector-result", "list-group-item", { inactive: q.is_draft })}
key={q.id}
onClick={() => selectQuery(q.id)}
data-test={`QueryId${q.id}`}>
{q.name} <QueryTagsControl isDraft={q.is_draft} tags={q.tags} className="inline-tags-control" />
</a>
))}
{/* @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. */}
{searchResults.map((q: any) => <a
className={cx("query-selector-result", "list-group-item", { inactive: q.is_draft })}
key={q.id}
onClick={() => selectQuery(q.id)}
data-test={`QueryId${q.id}`}>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ isDraft: any; tags: any; className: string... Remove this comment to see the full error message */}
{q.name} <QueryTagsControl isDraft={q.is_draft} tags={q.tags} className="inline-tags-control" />
</a>)}
</div>
);
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'disabled' does not exist on type 'never'... Remove this comment to see the full error message
if (props.disabled) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
return <Input value={selectedQuery && selectedQuery.name} placeholder={placeholder} disabled />;
}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'type' does not exist on type 'never'.
if (props.type === "select") {
const suffixIcon = selectedQuery ? clearIcon : null;
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const value = selectedQuery ? selectedQuery.name : searchTerm;
return (
@@ -99,10 +127,12 @@ export default function QuerySelector(props) {
notFoundContent={null}
filterOption={false}
defaultActiveFirstOption={false}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'className' does not exist on type 'never... Remove this comment to see the full error message
className={props.className}
data-test="QuerySelector">
{searchResults &&
searchResults.map(q => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'map' does not exist on type 'true | ((se... Remove this comment to see the full error message
searchResults.map((q: any) => {
const disabled = q.is_draft;
return (
<Option
@@ -114,6 +144,7 @@ export default function QuerySelector(props) {
{q.name}{" "}
<QueryTagsControl
isDraft={q.is_draft}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ isDraft: any; tags: any; className: string... Remove this comment to see the full error message
tags={q.tags}
className={cx("inline-tags-control", { disabled })}
/>
@@ -127,6 +158,7 @@ export default function QuerySelector(props) {
return (
<span data-test="QuerySelector">
{selectedQuery ? (
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
<Input value={selectedQuery.name} suffix={clearIcon} readOnly />
) : (
<Input
@@ -143,14 +175,6 @@ export default function QuerySelector(props) {
);
}
QuerySelector.propTypes = {
onChange: PropTypes.func.isRequired,
selectedQuery: PropTypes.object, // eslint-disable-line react/forbid-prop-types
type: PropTypes.oneOf(["select", "default"]),
className: PropTypes.string,
disabled: PropTypes.bool,
};
QuerySelector.defaultProps = {
selectedQuery: null,
type: "default",

View File

@@ -1,12 +1,20 @@
import d3 from "d3";
import React, { useRef, useMemo, useCallback, useState, useEffect } from "react";
import PropTypes from "prop-types";
import { Resizable as ReactResizable } from "react-resizable";
import KeyboardShortcuts from "@/services/KeyboardShortcuts";
import "./index.less";
export default function Resizable({ toggleShortcut, direction, sizeAttribute, children }) {
type OwnProps = {
direction?: "horizontal" | "vertical";
sizeAttribute?: string;
toggleShortcut?: string;
children?: React.ReactElement;
};
type Props = OwnProps & typeof Resizable.defaultProps;
export default function Resizable({ toggleShortcut, direction, sizeAttribute, children }: Props) {
const [size, setSize] = useState(0);
const elementRef = useRef();
const wasUsingTouchEventsRef = useRef(false);
@@ -19,6 +27,7 @@ export default function Resizable({ toggleShortcut, direction, sizeAttribute, ch
if (!elementRef.current) {
return 0;
}
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
return Math.floor(elementRef.current.getBoundingClientRect()[sizeProp]);
}, [sizeProp]);
@@ -28,10 +37,12 @@ export default function Resizable({ toggleShortcut, direction, sizeAttribute, ch
return;
}
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
const element = d3.select(elementRef.current);
let targetSize;
if (savedSize.current === null) {
targetSize = "0px";
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'null'.
savedSize.current = `${getElementSize()}px`;
} else {
targetSize = savedSize.current;
@@ -42,10 +53,13 @@ export default function Resizable({ toggleShortcut, direction, sizeAttribute, ch
.style(sizeAttribute, savedSize.current || "0px")
.transition()
.duration(200)
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
.ease("swing")
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
.style(sizeAttribute, targetSize);
// update state to new element's size
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string | null' is not assignable... Remove this comment to see the full error message
setSize(parseInt(targetSize) || 0);
}, [getElementSize, sizeAttribute]);
@@ -92,8 +106,9 @@ export default function Resizable({ toggleShortcut, direction, sizeAttribute, ch
// updated here and in `draggableCore::onMouseDown` handler to ensure that right value will be used
setSize(getElementSize());
},
onResize: (unused, data) => {
onResize: (unused: any, data: any) => {
// update element directly for better UI responsiveness
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
d3.select(elementRef.current).style(sizeAttribute, `${data.size[sizeProp]}px`);
setSize(data.size[sizeProp]);
wasResizedRef.current = true;
@@ -109,7 +124,7 @@ export default function Resizable({ toggleShortcut, direction, sizeAttribute, ch
const draggableCoreOptions = useMemo(
() => ({
onMouseDown: e => {
onMouseDown: (e: any) => {
// In some cases this handler is executed twice during the same resize operation - first time
// with `touchstart` event and second time with `mousedown` (probably emulated by browser).
// Therefore we set the flag only when we receive `touchstart` because in ths case it's definitely
@@ -130,6 +145,7 @@ export default function Resizable({ toggleShortcut, direction, sizeAttribute, ch
return null;
}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'CElement<any, Component<any, any, any>>' is ... Remove this comment to see the full error message
children = React.createElement(children.type, { ...children.props, ref: elementRef });
return (
@@ -148,13 +164,6 @@ export default function Resizable({ toggleShortcut, direction, sizeAttribute, ch
);
}
Resizable.propTypes = {
direction: PropTypes.oneOf(["horizontal", "vertical"]),
sizeAttribute: PropTypes.string,
toggleShortcut: PropTypes.string,
children: PropTypes.element,
};
Resizable.defaultProps = {
direction: "horizontal",
sizeAttribute: null, // "width"/"height" - depending on `direction`

View File

@@ -1,24 +1,33 @@
import { filter, find, isEmpty, size } from "lodash";
import React, { useState, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Modal from "antd/lib/modal";
import Input from "antd/lib/input";
import List from "antd/lib/list";
import Button from "antd/lib/button";
// @ts-expect-error ts-migrate(6133) FIXME: 'DialogPropType' is declared but its value is neve... Remove this comment to see the full error message
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import BigMessage from "@/components/BigMessage";
import LoadingState from "@/components/items-list/components/LoadingState";
import notification from "@/services/notification";
import useSearchResults from "@/lib/hooks/useSearchResults";
function ItemsList({ items, renderItem, onItemClick }) {
type OwnItemsListProps = {
items?: any[];
renderItem?: (...args: any[]) => any;
onItemClick?: (...args: any[]) => any;
};
type ItemsListProps = OwnItemsListProps & typeof ItemsList.defaultProps;
function ItemsList({ items, renderItem, onItemClick }: ItemsListProps) {
const renderListItem = useCallback(
item => {
const { content, className, isDisabled } = renderItem(item);
return (
<List.Item
className={classNames("p-l-10", "p-r-10", { clickable: !isDisabled, disabled: isDisabled }, className)}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(() => any) | null' is not assignable to typ... Remove this comment to see the full error message
onClick={isDisabled ? null : () => onItemClick(item)}>
{content}
</List.Item>
@@ -30,42 +39,46 @@ function ItemsList({ items, renderItem, onItemClick }) {
return <List size="small" dataSource={items} renderItem={renderListItem} />;
}
ItemsList.propTypes = {
items: PropTypes.array,
renderItem: PropTypes.func,
onItemClick: PropTypes.func,
};
ItemsList.defaultProps = {
items: [],
renderItem: () => {},
onItemClick: () => {},
};
function SelectItemsDialog({
dialog,
dialogTitle,
inputPlaceholder,
itemKey,
renderItem,
renderStagedItem,
searchItems,
selectedItemsTitle,
width,
showCount,
extraFooterContent,
}) {
type OwnSelectItemsDialogProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'DialogPropType' refers to a value, but is being u... Remove this comment to see the full error message
dialog: DialogPropType;
dialogTitle?: string;
inputPlaceholder?: string;
selectedItemsTitle?: string;
searchItems: (...args: any[]) => any;
itemKey?: (...args: any[]) => any;
renderItem?: (...args: any[]) => any;
renderStagedItem?: (...args: any[]) => any;
width?: string | number;
extraFooterContent?: React.ReactNode;
showCount?: boolean;
};
type SelectItemsDialogProps = OwnSelectItemsDialogProps & typeof SelectItemsDialog.defaultProps;
function SelectItemsDialog({ dialog, dialogTitle, inputPlaceholder, itemKey, renderItem, renderStagedItem, searchItems, selectedItemsTitle, width, showCount, extraFooterContent, }: SelectItemsDialogProps) {
const [selectedItems, setSelectedItems] = useState([]);
// @ts-expect-error ts-migrate(2322) FIXME: Type 'never[]' is not assignable to type 'null | u... Remove this comment to see the full error message
const [search, items, isLoading] = useSearchResults(searchItems, { initialResults: [] });
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
const hasResults = items.length > 0;
useEffect(() => {
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
search();
}, [search]);
const isItemSelected = useCallback(
item => {
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
const key = itemKey(item);
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
return !!find(selectedItems, i => itemKey(i) === key);
},
[selectedItems, itemKey]
@@ -74,9 +87,12 @@ function SelectItemsDialog({
const toggleItem = useCallback(
item => {
if (isItemSelected(item)) {
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
const key = itemKey(item);
// @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable.
setSelectedItems(filter(selectedItems, i => itemKey(i) !== key));
} else {
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any[]' is not assignable to para... Remove this comment to see the full error message
setSelectedItems([...selectedItems, item]);
}
},
@@ -84,8 +100,10 @@ function SelectItemsDialog({
);
const save = useCallback(() => {
dialog.close(selectedItems).catch(error => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'close' does not exist on type 'never'.
dialog.close(selectedItems).catch((error: any) => {
if (error) {
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
notification.error("Failed to save some of selected items.");
}
});
@@ -93,6 +111,7 @@ function SelectItemsDialog({
return (
<Modal
// @ts-expect-error ts-migrate(2339) FIXME: Property 'props' does not exist on type 'never'.
{...dialog.props}
className="select-items-dialog"
width={width}
@@ -102,12 +121,15 @@ function SelectItemsDialog({
<span className="flex-fill m-r-5" style={{ textAlign: "left", color: "rgba(0, 0, 0, 0.5)" }}>
{extraFooterContent}
</span>
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'props' does not exist on type 'never'. */}
<Button {...dialog.props.cancelButtonProps} onClick={dialog.dismiss}>
Cancel
</Button>
<Button
// @ts-expect-error ts-migrate(2339) FIXME: Property 'props' does not exist on type 'never'.
{...dialog.props.okButtonProps}
onClick={save}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'props' does not exist on type 'never'.
disabled={selectedItems.length === 0 || dialog.props.okButtonProps.disabled}
type="primary">
Save
@@ -117,6 +139,7 @@ function SelectItemsDialog({
}>
<div className="d-flex align-items-center m-b-10">
<div className="flex-fill">
{/* @ts-expect-error ts-migrate(2349) FIXME: This expression is not callable. */}
<Input.Search onChange={event => search(event.target.value)} placeholder={inputPlaceholder} autoFocus />
</div>
{renderStagedItem && (
@@ -134,8 +157,11 @@ function SelectItemsDialog({
)}
{!isLoading && hasResults && (
<ItemsList
// @ts-expect-error ts-migrate(2322) FIXME: Type 'boolean | ((searchTerm: any) => void) | null... Remove this comment to see the full error message
items={items}
renderItem={item => renderItem(item, { isSelected: isItemSelected(item) })}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(item: any) => any' is not assignable to typ... Remove this comment to see the full error message
renderItem={(item: any) => renderItem(item, { isSelected: isItemSelected(item) })}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(item: any) => void' is not assignable to ty... Remove this comment to see the full error message
onItemClick={toggleItem}
/>
)}
@@ -145,7 +171,9 @@ function SelectItemsDialog({
{selectedItems.length > 0 && (
<ItemsList
items={selectedItems}
renderItem={item => renderStagedItem(item, { isSelected: true })}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(item: any) => any' is not assignable to typ... Remove this comment to see the full error message
renderItem={(item: any) => renderStagedItem(item, { isSelected: true })}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(item: any) => void' is not assignable to ty... Remove this comment to see the full error message
onItemClick={toggleItem}
/>
)}
@@ -156,32 +184,11 @@ function SelectItemsDialog({
);
}
SelectItemsDialog.propTypes = {
dialog: DialogPropType.isRequired,
dialogTitle: PropTypes.string,
inputPlaceholder: PropTypes.string,
selectedItemsTitle: PropTypes.string,
searchItems: PropTypes.func.isRequired, // (searchTerm: string): Promise<Items[]> if `searchTerm === ''` load all
itemKey: PropTypes.func, // (item) => string|number - return key of item (by default `id`)
// left list
// (item, { isSelected }) => {
// content: node, // item contents
// className: string = '', // additional class for item wrapper
// isDisabled: bool = false, // is item clickable or disabled
// }
renderItem: PropTypes.func,
// right list; args/results save as for `renderItem`. if not specified - `renderItem` will be used
renderStagedItem: PropTypes.func,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
extraFooterContent: PropTypes.node,
showCount: PropTypes.bool,
};
SelectItemsDialog.defaultProps = {
dialogTitle: "Add Items",
inputPlaceholder: "Search...",
selectedItemsTitle: "Selected items",
itemKey: item => item.id,
itemKey: (item: any) => item.id,
renderItem: () => "",
renderStagedItem: null, // hidden by default
width: "80%",

View File

@@ -0,0 +1,38 @@
import React, { useMemo } from "react";
import { maxBy } from "lodash";
import AntdSelect, { SelectProps, LabeledValue } from "antd/lib/select";
import { calculateTextWidth } from "@/lib/calculateTextWidth";
const MIN_LEN_FOR_VIRTUAL_SCROLL = 400;
interface VirtualScrollLabeledValue extends LabeledValue {
label: string;
}
interface VirtualScrollSelectProps extends SelectProps<string> {
options: Array<VirtualScrollLabeledValue>;
}
function SelectWithVirtualScroll({ options, ...props }: VirtualScrollSelectProps): JSX.Element {
const dropdownMatchSelectWidth = useMemo<number | boolean>(() => {
if (options && options.length > MIN_LEN_FOR_VIRTUAL_SCROLL) {
const largestOpt = maxBy(options, "label.length");
if (largestOpt) {
const offset = 40;
const optionText = largestOpt.label;
const width = calculateTextWidth(optionText);
if (width) {
return width + offset;
}
}
return true;
}
return false;
}, [options]);
return <AntdSelect<string> dropdownMatchSelectWidth={dropdownMatchSelectWidth} options={options} {...props} />;
}
export default SelectWithVirtualScroll;

View File

@@ -5,20 +5,24 @@ import Link from "@/components/Link";
import location from "@/services/location";
import settingsMenu from "@/services/settingsMenu";
function wrapSettingsTab(id, options, WrappedComponent) {
function wrapSettingsTab(id: any, options: any, WrappedComponent: any) {
settingsMenu.add(id, options);
return function SettingsTab(props) {
return function SettingsTab(props: any) {
const activeItem = settingsMenu.getActiveItem(location.path);
return (
<div className="settings-screen">
<div className="container">
<PageHeader title="Settings" />
<div className="bg-white tiled">
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'title' does not exist on type 'number | ... Remove this comment to see the full error message */}
<Menu selectedKeys={[activeItem && activeItem.title]} selectable={false} mode="horizontal">
{settingsMenu.getAvailableItems().map(item => (
// @ts-expect-error ts-migrate(2339) FIXME: Property 'title' does not exist on type 'number | ... Remove this comment to see the full error message
<Menu.Item key={item.title}>
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'path' does not exist on type 'number | (... Remove this comment to see the full error message */}
<Link href={item.path} data-test="SettingsScreenItem">
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'title' does not exist on type 'number | ... Remove this comment to see the full error message */}
{item.title}
</Link>
</Menu.Item>

View File

@@ -1,82 +0,0 @@
import { map } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Badge from "antd/lib/badge";
import Menu from "antd/lib/menu";
import getTags from "@/services/getTags";
import "./TagsList.less";
export default class TagsList extends React.Component {
static propTypes = {
tagsUrl: PropTypes.string.isRequired,
onUpdate: PropTypes.func,
};
static defaultProps = {
onUpdate: () => {},
};
constructor(props) {
super(props);
this.state = {
// An array of objects that with the name and count of the tagged items
allTags: [],
// A set of tag names
selectedTags: new Set(),
};
}
componentDidMount() {
getTags(this.props.tagsUrl).then(allTags => {
this.setState({ allTags });
});
}
toggleTag(event, tag) {
const { selectedTags } = this.state;
if (event.shiftKey) {
// toggle tag
if (selectedTags.has(tag)) {
selectedTags.delete(tag);
} else {
selectedTags.add(tag);
}
} else {
// if the tag is the only selected, deselect it, otherwise select only it
if (selectedTags.has(tag) && selectedTags.size === 1) {
selectedTags.clear();
} else {
selectedTags.clear();
selectedTags.add(tag);
}
}
this.forceUpdate();
this.props.onUpdate([...this.state.selectedTags]);
}
render() {
const { allTags, selectedTags } = this.state;
if (allTags.length > 0) {
return (
<div className="m-t-10 tags-list tiled">
<Menu className="invert-stripe-position" mode="inline" selectedKeys={[...selectedTags]}>
{map(allTags, tag => (
<Menu.Item key={tag.name} className="m-0">
<a
className="d-flex align-items-center justify-content-between"
onClick={event => this.toggleTag(event, tag.name)}>
<span className="max-character col-xs-11">{tag.name}</span>
<Badge count={tag.count} />
</a>
</Menu.Item>
))}
</Menu>
</div>
);
}
return null;
}
}

View File

@@ -1,15 +1,47 @@
@import '~@/assets/less/ant';
@import "~@/assets/less/ant";
.tags-list {
.tags-list-title {
margin: 15px 5px 5px 5px;
display: flex;
justify-content: space-between;
align-items: center;
label {
display: block;
white-space: nowrap;
margin: 0;
}
a {
display: block;
white-space: nowrap;
cursor: pointer;
.anticon {
font-size: 75%;
margin-right: 2px;
}
}
}
.ant-badge-count {
background-color: fade(@redash-gray, 10%);
color: fade(@redash-gray, 75%);
}
.ant-menu-item-selected {
.ant-badge-count {
background-color: @primary-color;
color: white;
.ant-menu.ant-menu-inline {
border: none;
.ant-menu-item {
width: 100%;
}
.ant-menu-item-selected {
.ant-badge-count {
background-color: @primary-color;
color: white;
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
import { map, includes, difference } from "lodash";
import React, { useState, useCallback, useEffect } from "react";
import Badge from "antd/lib/badge";
import Menu from "antd/lib/menu";
import CloseOutlinedIcon from "@ant-design/icons/CloseOutlined";
import getTags from "@/services/getTags";
import "./TagsList.less";
type Tag = {
name: string;
count?: number;
};
type TagsListProps = {
tagsUrl: string;
showUnselectAll: boolean;
onUpdate?: (selectedTags: string[]) => void;
};
function TagsList({ tagsUrl, showUnselectAll = false, onUpdate }: TagsListProps): JSX.Element | null {
const [allTags, setAllTags] = useState<Tag[]>([]);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
useEffect(() => {
let isCancelled = false;
getTags(tagsUrl).then(tags => {
if (!isCancelled) {
setAllTags(tags);
}
});
return () => {
isCancelled = true;
};
}, [tagsUrl]);
const toggleTag = useCallback(
(event, tag) => {
let newSelectedTags;
if (event.shiftKey) {
// toggle tag
if (includes(selectedTags, tag)) {
newSelectedTags = difference(selectedTags, [tag]);
} else {
newSelectedTags = [...selectedTags, tag];
}
} else {
// if the tag is the only selected, deselect it, otherwise select only it
if (includes(selectedTags, tag) && selectedTags.length === 1) {
newSelectedTags = [];
} else {
newSelectedTags = [tag];
}
}
setSelectedTags(newSelectedTags);
if (onUpdate) {
onUpdate([...newSelectedTags]);
}
},
[selectedTags, onUpdate]
);
const unselectAll = useCallback(() => {
setSelectedTags([]);
if (onUpdate) {
onUpdate([]);
}
}, [onUpdate]);
if (allTags.length === 0) {
return null;
}
return (
<div className="tags-list">
<div className="tags-list-title">
<label>Tags</label>
{showUnselectAll && selectedTags.length > 0 && (
<a onClick={unselectAll}>
<CloseOutlinedIcon />
clear selection
</a>
)}
</div>
<div className="tiled">
<Menu className="invert-stripe-position" mode="inline" selectedKeys={selectedTags}>
{map(allTags, tag => (
<Menu.Item key={tag.name} className="m-0">
<a
className="d-flex align-items-center justify-content-between"
onClick={event => toggleTag(event, tag.name)}>
<span className="max-character col-xs-11">{tag.name}</span>
<Badge count={tag.count} />
</a>
</Menu.Item>
))}
</Menu>
</div>
</div>
);
}
export default TagsList;

View File

@@ -1,19 +1,30 @@
import moment from "moment";
import { isNil } from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import PropTypes from "prop-types";
// @ts-expect-error ts-migrate(6133) FIXME: 'Moment' is declared but its value is never read.
import { Moment } from "@/components/proptypes";
import { clientConfig } from "@/services/auth";
import Tooltip from "antd/lib/tooltip";
function toMoment(value) {
function toMoment(value: any) {
value = !isNil(value) ? moment(value) : null;
return value && value.isValid() ? value : null;
}
export default function TimeAgo({ date, placeholder, autoUpdate }) {
type OwnProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
date?: string | number | any | Moment;
placeholder?: string;
autoUpdate?: boolean;
variation?: "timeAgoInTooltip";
};
type Props = OwnProps & typeof TimeAgo.defaultProps;
export default function TimeAgo({ date, placeholder, autoUpdate, variation }: Props) {
const startDate = toMoment(date);
const [value, setValue] = useState(null);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dateTimeFormat' does not exist on type '... Remove this comment to see the full error message
const title = useMemo(() => (startDate ? startDate.format(clientConfig.dateTimeFormat) : null), [startDate]);
useEffect(() => {
@@ -28,6 +39,13 @@ export default function TimeAgo({ date, placeholder, autoUpdate }) {
}
}, [autoUpdate, startDate, placeholder]);
if (variation === "timeAgoInTooltip") {
return (
<Tooltip title={value}>
<span data-test="TimeAgo">{title}</span>
</Tooltip>
);
}
return (
<Tooltip title={title}>
<span data-test="TimeAgo">{value}</span>
@@ -35,12 +53,6 @@ export default function TimeAgo({ date, placeholder, autoUpdate }) {
);
}
TimeAgo.propTypes = {
date: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date), Moment]),
placeholder: PropTypes.string,
autoUpdate: PropTypes.bool,
};
TimeAgo.defaultProps = {
date: null,
placeholder: "",

View File

@@ -1,9 +1,16 @@
import React, { useMemo, useState, useEffect } from "react";
import moment from "moment";
import PropTypes from "prop-types";
// @ts-expect-error ts-migrate(6133) FIXME: 'Moment' is declared but its value is never read.
import { Moment } from "@/components/proptypes";
export default function Timer({ from }) {
type OwnProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
from?: string | number | any | Moment;
};
type Props = OwnProps & typeof Timer.defaultProps;
export default function Timer({ from }: Props) {
const startTime = useMemo(() => moment(from).valueOf(), [from]);
const [value, setValue] = useState(null);
@@ -11,6 +18,7 @@ export default function Timer({ from }) {
function update() {
const diff = moment.now() - startTime;
const format = diff > 1000 * 60 * 60 ? "HH:mm:ss" : "mm:ss"; // no HH under an hour
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
setValue(moment.utc(diff).format(format));
}
update();
@@ -22,10 +30,6 @@ export default function Timer({ from }) {
return <span className="rd-timer">{value}</span>;
}
Timer.propTypes = {
from: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date), Moment]),
};
Timer.defaultProps = {
from: null,
};

View File

@@ -0,0 +1,7 @@
.user-groups {
margin: -5px 0 0 -5px;
.ant-tag {
margin: 5px 0 0 5px;
}
}

View File

@@ -0,0 +1,31 @@
import { map } from "lodash";
import React from "react";
import Tag from "antd/lib/tag";
import Link from "@/components/Link";
import "./UserGroups.less";
type OwnProps = {
groups?: {
id: number | string;
name?: string;
}[];
linkGroups?: boolean;
};
type Props = OwnProps & typeof UserGroups.defaultProps;
export default function UserGroups({ groups, linkGroups, ...props }: Props) {
return (
<div className="user-groups" {...props}>
{map(groups, group => (
<Tag key={group.id}>{linkGroups ? <Link href={`groups/${group.id}`}>{group.name}</Link> : group.name}</Tag>
))}
</div>
);
}
UserGroups.defaultProps = {
groups: [],
linkGroups: true,
};

View File

@@ -1,12 +1,18 @@
import React from "react";
import PropTypes from "prop-types";
import Menu from "antd/lib/menu";
import PageHeader from "@/components/PageHeader";
import Link from "@/components/Link";
import "./layout.less";
export default function Layout({ activeTab, children }) {
type OwnProps = {
activeTab?: string;
children?: React.ReactNode;
};
type Props = OwnProps & typeof Layout.defaultProps;
export default function Layout({ activeTab, children }: Props) {
return (
<div className="admin-page-layout">
<div className="container">
@@ -30,11 +36,6 @@ export default function Layout({ activeTab, children }) {
);
}
Layout.propTypes = {
activeTab: PropTypes.string,
children: PropTypes.node,
};
Layout.defaultProps = {
activeTab: "system_status",
children: null,

View File

@@ -1,6 +1,5 @@
import { map } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Badge from "antd/lib/badge";
import Card from "antd/lib/card";
@@ -8,9 +7,17 @@ import Spin from "antd/lib/spin";
import Table from "antd/lib/table";
import { Columns } from "@/components/items-list/components/ItemsTable";
type OwnCounterCardProps = {
title: string;
value?: number | string;
loading: boolean;
};
type CounterCardProps = OwnCounterCardProps & typeof CounterCard.defaultProps;
// CounterCard
export function CounterCard({ title, value, loading }) {
export function CounterCard({ title, value, loading }: CounterCardProps) {
return (
<Spin spinning={loading}>
<Card>
@@ -21,12 +28,6 @@ export function CounterCard({ title, value, loading }) {
);
}
CounterCard.propTypes = {
title: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
loading: PropTypes.bool.isRequired,
};
CounterCard.defaultProps = {
value: "",
};
@@ -39,7 +40,7 @@ const queryJobsColumns = [
{ title: "Org ID", dataIndex: "meta.org_id" },
{ title: "Data Source ID", dataIndex: "meta.data_source_id" },
{ title: "User ID", dataIndex: "meta.user_id" },
Columns.custom(scheduled => scheduled.toString(), { title: "Scheduled", dataIndex: "meta.scheduled" }),
Columns.custom((scheduled: any) => scheduled.toString(), { title: "Scheduled", dataIndex: "meta.scheduled" }),
Columns.timeAgo({ title: "Start Time", dataIndex: "started_at" }),
Columns.timeAgo({ title: "Enqueue Time", dataIndex: "enqueued_at" }),
];
@@ -53,12 +54,11 @@ const otherJobsColumns = [
const workersColumns = [
Columns.custom(
value => (
<span>
<Badge status={{ busy: "processing", idle: "default", started: "success", suspended: "warning" }[value]} />{" "}
{value}
</span>
),
(value: any) => <span>
{/* @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message */}
<Badge status={{ busy: "processing", idle: "default", started: "success", suspended: "warning" }[value]} />{" "}
{value}
</span>,
{ title: "State", dataIndex: "state" }
),
]
@@ -75,12 +75,27 @@ const workersColumns = [
const queuesColumns = map(["Name", "Started", "Queued"], c => ({ title: c, dataIndex: c.toLowerCase() }));
const TablePropTypes = {
loading: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
type WorkersTableProps = {
loading: boolean;
items: any[];
};
export function WorkersTable({ loading, items }) {
type QueuesTableProps = {
loading: boolean;
items: any[];
};
type QueryJobsTableProps = {
loading: boolean;
items: any[];
};
type OtherJobsTableProps = {
loading: boolean;
items: any[];
};
export function WorkersTable({ loading, items }: WorkersTableProps) {
return (
<Table
loading={loading}
@@ -96,9 +111,7 @@ export function WorkersTable({ loading, items }) {
);
}
WorkersTable.propTypes = TablePropTypes;
export function QueuesTable({ loading, items }) {
export function QueuesTable({ loading, items }: QueuesTableProps) {
return (
<Table
loading={loading}
@@ -114,9 +127,7 @@ export function QueuesTable({ loading, items }) {
);
}
QueuesTable.propTypes = TablePropTypes;
export function QueryJobsTable({ loading, items }) {
export function QueryJobsTable({ loading, items }: QueryJobsTableProps) {
return (
<Table
loading={loading}
@@ -132,9 +143,7 @@ export function QueryJobsTable({ loading, items }) {
);
}
QueryJobsTable.propTypes = TablePropTypes;
export function OtherJobsTable({ loading, items }) {
export function OtherJobsTable({ loading, items }: OtherJobsTableProps) {
return (
<Table
loading={loading}
@@ -149,5 +158,3 @@ export function OtherJobsTable({ loading, items }) {
/>
);
}
OtherJobsTable.propTypes = TablePropTypes;

View File

@@ -9,7 +9,9 @@ import TimeAgo from "@/components/TimeAgo";
import { toHuman, prettySize } from "@/lib/utils";
export function General({ info }) {
export function General({
info
}: any) {
info = toPairs(info);
return (
<Card title="General" size="small">
@@ -19,6 +21,7 @@ export function General({ info }) {
size="small"
itemLayout="vertical"
dataSource={info}
// @ts-expect-error ts-migrate(2322) FIXME: Type '([name, value]: [any, any]) => Element' is n... Remove this comment to see the full error message
renderItem={([name, value]) => (
<List.Item extra={<span className="badge">{value}</span>}>{toHuman(name)}</List.Item>
)}
@@ -28,7 +31,9 @@ export function General({ info }) {
);
}
export function DatabaseMetrics({ info }) {
export function DatabaseMetrics({
info
}: any) {
return (
<Card title="Redash Database" size="small">
{info.length === 0 && <div className="text-muted text-center">No data</div>}
@@ -37,6 +42,7 @@ export function DatabaseMetrics({ info }) {
size="small"
itemLayout="vertical"
dataSource={info}
// @ts-expect-error ts-migrate(2322) FIXME: Type '([name, size]: [any, any]) => Element' is no... Remove this comment to see the full error message
renderItem={([name, size]) => (
<List.Item extra={<span className="badge">{prettySize(size)}</span>}>{name}</List.Item>
)}
@@ -46,7 +52,9 @@ export function DatabaseMetrics({ info }) {
);
}
export function Queues({ info }) {
export function Queues({
info
}: any) {
info = toPairs(info);
return (
<Card title="Queues" size="small">
@@ -56,6 +64,7 @@ export function Queues({ info }) {
size="small"
itemLayout="vertical"
dataSource={info}
// @ts-expect-error ts-migrate(2322) FIXME: Type '([name, queue]: [any, any]) => Element' is n... Remove this comment to see the full error message
renderItem={([name, queue]) => (
<List.Item extra={<span className="badge">{queue.size}</span>}>{name}</List.Item>
)}
@@ -65,7 +74,9 @@ export function Queues({ info }) {
);
}
export function Manager({ info }) {
export function Manager({
info
}: any) {
const items = info
? [
<List.Item

View File

@@ -1,84 +0,0 @@
import Input from "antd/lib/input";
import { includes, isEmpty } from "lodash";
import PropTypes from "prop-types";
import React from "react";
import Link from "@/components/Link";
import EmptyState from "@/components/items-list/components/EmptyState";
import "./CardsList.less";
const { Search } = Input;
export default class CardsList extends React.Component {
static propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
imgSrc: PropTypes.string.isRequired,
onClick: PropTypes.func,
href: PropTypes.string,
})
),
showSearch: PropTypes.bool,
};
static defaultProps = {
items: [],
showSearch: false,
};
state = {
searchText: "",
};
constructor(props) {
super(props);
this.items = [];
let itemId = 1;
props.items.forEach(item => {
this.items.push({ id: itemId, ...item });
itemId += 1;
});
}
// eslint-disable-next-line class-methods-use-this
renderListItem(item) {
return (
<Link key={`card${item.id}`} className="visual-card" onClick={item.onClick} href={item.href}>
<img alt={item.title} src={item.imgSrc} />
<h3>{item.title}</h3>
</Link>
);
}
render() {
const { showSearch } = this.props;
const { searchText } = this.state;
const filteredItems = this.items.filter(
item => isEmpty(searchText) || includes(item.title.toLowerCase(), searchText.toLowerCase())
);
return (
<div data-test="CardsList">
{showSearch && (
<div className="row p-10">
<div className="col-md-4 col-md-offset-4">
<Search placeholder="Search..." onChange={e => this.setState({ searchText: e.target.value })} autoFocus />
</div>
</div>
)}
{isEmpty(filteredItems) ? (
<EmptyState className="" />
) : (
<div className="row">
<div className="col-lg-12 d-inline-flex flex-wrap visual-card-list">
{filteredItems.map(item => this.renderListItem(item))}
</div>
</div>
)}
</div>
);
}
}

View File

@@ -0,0 +1,80 @@
import { includes, isEmpty } from "lodash";
import PropTypes from "prop-types";
import React, { useState } from "react";
import Input from "antd/lib/input";
import Link from "@/components/Link";
import EmptyState from "@/components/items-list/components/EmptyState";
import "./CardsList.less";
export interface CardsListItem {
title: string;
imgSrc: string;
onClick?: () => void;
href?: string;
}
export interface CardsListProps {
items?: CardsListItem[];
showSearch?: boolean;
}
interface ListItemProps {
item: CardsListItem;
keySuffix: string;
}
function ListItem({ item, keySuffix }: ListItemProps) {
return (
<Link key={`card${keySuffix}`} className="visual-card" onClick={item.onClick} href={item.href}>
<img alt={item.title} src={item.imgSrc} />
<h3>{item.title}</h3>
</Link>
);
}
export default function CardsList({ items = [], showSearch = false }: CardsListProps) {
const [searchText, setSearchText] = useState("");
const filteredItems = items.filter(
item => isEmpty(searchText) || includes(item.title.toLowerCase(), searchText.toLowerCase())
);
return (
<div data-test="CardsList">
{showSearch && (
<div className="row p-10">
<div className="col-md-4 col-md-offset-4">
<Input.Search
placeholder="Search..."
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchText(e.target.value)}
autoFocus
/>
</div>
</div>
)}
{isEmpty(filteredItems) ? (
<EmptyState className="" />
) : (
<div className="row">
<div className="col-lg-12 d-inline-flex flex-wrap visual-card-list">
{filteredItems.map((item: CardsListItem, index: number) => (
<ListItem key={index} item={item} keySuffix={index.toString()} />
))}
</div>
</div>
)}
</div>
);
}
CardsList.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
imgSrc: PropTypes.string.isRequired,
onClick: PropTypes.func,
href: PropTypes.string,
})
),
showSearch: PropTypes.bool,
};

View File

@@ -1,15 +1,23 @@
import { map, includes, groupBy, first, find } from "lodash";
import React, { useState, useMemo, useCallback } from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
import Modal from "antd/lib/modal";
// @ts-expect-error ts-migrate(6133) FIXME: 'DialogPropType' is declared but its value is neve... Remove this comment to see the full error message
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { MappingType, ParameterMappingListInput } from "@/components/ParameterMappingInput";
import QuerySelector from "@/components/QuerySelector";
import notification from "@/services/notification";
import { Query } from "@/services/query";
function VisualizationSelect({ query, visualization, onChange }) {
type OwnVisualizationSelectProps = {
query?: any;
visualization?: any;
onChange?: (...args: any[]) => any;
};
type VisualizationSelectProps = OwnVisualizationSelectProps & typeof VisualizationSelect.defaultProps;
function VisualizationSelect({ query, visualization, onChange }: VisualizationSelectProps) {
const visualizationGroups = useMemo(() => {
return query ? groupBy(query.visualizations, "type") : {};
}, [query]);
@@ -50,19 +58,19 @@ function VisualizationSelect({ query, visualization, onChange }) {
);
}
VisualizationSelect.propTypes = {
query: PropTypes.object,
visualization: PropTypes.object,
onChange: PropTypes.func,
};
VisualizationSelect.defaultProps = {
query: null,
visualization: null,
onChange: () => {},
};
function AddWidgetDialog({ dialog, dashboard }) {
type AddWidgetDialogProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'DialogPropType' refers to a value, but is being u... Remove this comment to see the full error message
dialog: DialogPropType;
dashboard: any;
};
function AddWidgetDialog({ dialog, dashboard }: AddWidgetDialogProps) {
const [selectedQuery, setSelectedQuery] = useState(null);
const [selectedVisualization, setSelectedVisualization] = useState(null);
const [parameterMappings, setParameterMappings] = useState([]);
@@ -75,11 +83,13 @@ function AddWidgetDialog({ dialog, dashboard }) {
setParameterMappings([]);
if (queryId) {
Query.get({ id: queryId }).then(query => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'get' does not exist on type 'typeof Quer... Remove this comment to see the full error message
Query.get({ id: queryId }).then((query: any) => {
if (query) {
const existingParamNames = map(dashboard.getParametersDefs(), param => param.name);
setSelectedQuery(query);
setParameterMappings(
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ name: any; type: string; mapTo... Remove this comment to see the full error message
map(query.getParametersDefs(), param => ({
name: param.name,
type: includes(existingParamNames, param.name)
@@ -92,6 +102,7 @@ function AddWidgetDialog({ dialog, dashboard }) {
}))
);
if (query.visualizations.length > 0) {
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '((prevState: null) => null) | nu... Remove this comment to see the full error message
setSelectedVisualization(first(query.visualizations));
}
}
@@ -103,6 +114,7 @@ function AddWidgetDialog({ dialog, dashboard }) {
const saveWidget = useCallback(() => {
dialog.close({ visualization: selectedVisualization, parameterMappings }).catch(() => {
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
notification.error("Widget could not be added");
});
}, [dialog, selectedVisualization, parameterMappings]);
@@ -121,12 +133,14 @@ function AddWidgetDialog({ dialog, dashboard }) {
okText="Add to Dashboard"
width={700}>
<div data-test="AddWidgetDialog">
<QuerySelector onChange={query => selectQuery(query ? query.id : null)} />
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '(query: any) => void' is not assignable to t... Remove this comment to see the full error message */}
<QuerySelector onChange={(query: any) => selectQuery(query ? query.id : null)} />
{selectedQuery && (
<VisualizationSelect
query={selectedQuery}
visualization={selectedVisualization}
// @ts-expect-error ts-migrate(2322) FIXME: Type 'Dispatch<SetStateAction<null>>' is not assig... Remove this comment to see the full error message
onChange={setSelectedVisualization}
/>
)}
@@ -140,6 +154,7 @@ function AddWidgetDialog({ dialog, dashboard }) {
id="parameter-mappings"
mappings={parameterMappings}
existingParams={existingParams}
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
onChange={setParameterMappings}
/>,
]}
@@ -148,9 +163,4 @@ function AddWidgetDialog({ dialog, dashboard }) {
);
}
AddWidgetDialog.propTypes = {
dialog: DialogPropType.isRequired,
dashboard: PropTypes.object.isRequired,
};
export default wrapDialog(AddWidgetDialog);

View File

@@ -19,17 +19,17 @@ export default class AutoHeightController {
onHeightChange = null;
constructor(handler) {
constructor(handler: any) {
this.onHeightChange = handler;
}
update(widgets) {
update(widgets: any) {
const newWidgetIds = widgets
.filter(widget => widget.options.position.autoHeight)
.map(widget => widget.id.toString());
.filter((widget: any) => widget.options.position.autoHeight)
.map((widget: any) => widget.id.toString());
// added
newWidgetIds.filter(id => !includes(Object.keys(this.widgets), id)).forEach(this.add);
newWidgetIds.filter((id: any) => !includes(Object.keys(this.widgets), id)).forEach(this.add);
// removed
Object.keys(this.widgets)
@@ -37,12 +37,13 @@ export default class AutoHeightController {
.forEach(this.remove);
}
add = id => {
add = (id: any) => {
if (this.isEmpty()) {
this.start();
}
const selector = WIDGET_SELECTOR.replace("{0}", id);
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
this.widgets[id] = [
function getHeight() {
const widgetEl = document.querySelector(selector);
@@ -66,13 +67,14 @@ export default class AutoHeightController {
];
};
remove = id => {
remove = (id: any) => {
// ignore if not an active autoHeight widget
if (!this.exists(id)) {
return;
}
// not actually deleting from this.widgets to prevent case of unwanted re-adding
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
this.widgets[id.toString()] = false;
if (this.isEmpty()) {
@@ -80,7 +82,8 @@ export default class AutoHeightController {
}
};
exists = id => !!this.widgets[id.toString()];
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
exists = (id: any) => !!this.widgets[id.toString()];
isEmpty = () => !some(this.widgets);
@@ -88,10 +91,13 @@ export default class AutoHeightController {
Object.keys(this.widgets)
.filter(this.exists) // reject already removed items
.forEach(id => {
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
const [getHeight, prevHeight] = this.widgets[id];
const height = getHeight();
if (height && height !== prevHeight) {
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
this.widgets[id][1] = height; // save
// @ts-expect-error ts-migrate(2721) FIXME: Cannot invoke an object which is possibly 'null'.
this.onHeightChange(id, height); // dispatch
}
});
@@ -99,10 +105,12 @@ export default class AutoHeightController {
start = () => {
this.stop();
// @ts-expect-error ts-migrate(2322) FIXME: Type 'number' is not assignable to type 'null'.
this.interval = setInterval(this.checkHeightChanges, INTERVAL);
};
stop = () => {
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
clearInterval(this.interval);
};
@@ -114,6 +122,7 @@ export default class AutoHeightController {
destroy = () => {
this.stop();
// @ts-expect-error ts-migrate(2322) FIXME: Type 'null' is not assignable to type '{}'.
this.widgets = null;
};
}

View File

@@ -3,19 +3,25 @@ import React, { useState } from "react";
import Modal from "antd/lib/modal";
import Input from "antd/lib/input";
import DynamicComponent from "@/components/DynamicComponent";
// @ts-expect-error ts-migrate(6133) FIXME: 'DialogPropType' is declared but its value is neve... Remove this comment to see the full error message
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import navigateTo from "@/components/ApplicationArea/navigateTo";
import recordEvent from "@/services/recordEvent";
import { policy } from "@/services/policy";
import { Dashboard } from "@/services/dashboard";
function CreateDashboardDialog({ dialog }) {
type Props = {
// @ts-expect-error ts-migrate(2749) FIXME: 'DialogPropType' refers to a value, but is being u... Remove this comment to see the full error message
dialog: DialogPropType;
};
function CreateDashboardDialog({ dialog }: Props) {
const [name, setName] = useState("");
const [isValid, setIsValid] = useState(false);
const [saveInProgress, setSaveInProgress] = useState(false);
const isCreateDashboardEnabled = policy.isCreateDashboardEnabled();
function handleNameChange(event) {
function handleNameChange(event: any) {
const value = trim(event.target.value);
setName(value);
setIsValid(value !== "");
@@ -25,10 +31,12 @@ function CreateDashboardDialog({ dialog }) {
if (name !== "") {
setSaveInProgress(true);
Dashboard.save({ name }).then(data => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'save' does not exist on type 'typeof Das... Remove this comment to see the full error message
Dashboard.save({ name }).then((data: any) => {
dialog.close();
navigateTo(`${data.url}?edit`);
});
// @ts-expect-error ts-migrate(2554) FIXME: Expected 4 arguments, but got 2.
recordEvent("create", "dashboard");
}
}
@@ -55,6 +63,7 @@ function CreateDashboardDialog({ dialog }) {
"data-test": "CreateDashboardDialog",
}}>
<DynamicComponent name="CreateDashboardDialogExtra" disabled={!isCreateDashboardEnabled}>
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
<Input
defaultValue={name}
onChange={handleNameChange}
@@ -68,8 +77,4 @@ function CreateDashboardDialog({ dialog }) {
);
}
CreateDashboardDialog.propTypes = {
dialog: DialogPropType.isRequired,
};
export default wrapDialog(CreateDashboardDialog);

View File

@@ -4,6 +4,7 @@ import { chain, cloneDeep, find } from "lodash";
import cx from "classnames";
import { Responsive, WidthProvider } from "react-grid-layout";
import { VisualizationWidget, TextboxWidget, RestrictedWidget } from "@/components/dashboards/dashboard-widget";
// @ts-expect-error ts-migrate(6133) FIXME: 'FiltersType' is declared but its value is never r... Remove this comment to see the full error message
import { FiltersType } from "@/components/Filters";
import cfg from "@/config/dashboard-grid-options";
import AutoHeightController from "./AutoHeightController";
@@ -14,20 +15,36 @@ import "./dashboard-grid.less";
const ResponsiveGridLayout = WidthProvider(Responsive);
const WidgetType = PropTypes.shape({
id: PropTypes.number.isRequired,
options: PropTypes.shape({
position: PropTypes.shape({
col: PropTypes.number.isRequired,
row: PropTypes.number.isRequired,
sizeY: PropTypes.number.isRequired,
minSizeY: PropTypes.number.isRequired,
maxSizeY: PropTypes.number.isRequired,
sizeX: PropTypes.number.isRequired,
minSizeX: PropTypes.number.isRequired,
maxSizeX: PropTypes.number.isRequired,
type WidgetType = {
id: number;
options: {
position: {
col: number;
row: number;
sizeY: number;
minSizeY: number;
maxSizeY: number;
sizeX: number;
minSizeX: number;
maxSizeX: number;
};
};
};
const WidgetType: PropTypes.Requireable<WidgetType> = PropTypes.shape({
id: PropTypes.number.isRequired,
options: PropTypes.shape({
position: PropTypes.shape({
col: PropTypes.number.isRequired,
row: PropTypes.number.isRequired,
sizeY: PropTypes.number.isRequired,
minSizeY: PropTypes.number.isRequired,
maxSizeY: PropTypes.number.isRequired,
sizeX: PropTypes.number.isRequired,
minSizeX: PropTypes.number.isRequired,
maxSizeX: PropTypes.number.isRequired,
}).isRequired,
}).isRequired,
}).isRequired,
});
const SINGLE = "single-column";
@@ -35,15 +52,25 @@ const MULTI = "multi-column";
const DashboardWidget = React.memo(
function DashboardWidget({
// @ts-expect-error ts-migrate(2339) FIXME: Property 'widget' does not exist on type '{ childr... Remove this comment to see the full error message
widget,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'dashboard' does not exist on type '{ chi... Remove this comment to see the full error message
dashboard,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onLoadWidget' does not exist on type '{ ... Remove this comment to see the full error message
onLoadWidget,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onRefreshWidget' does not exist on type ... Remove this comment to see the full error message
onRefreshWidget,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onRemoveWidget' does not exist on type '... Remove this comment to see the full error message
onRemoveWidget,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'onParameterMappingsChange' does not exis... Remove this comment to see the full error message
onParameterMappingsChange,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'canEdit' does not exist on type '{ child... Remove this comment to see the full error message
canEdit,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isPublic' does not exist on type '{ chil... Remove this comment to see the full error message
isPublic,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isLoading' does not exist on type '{ chi... Remove this comment to see the full error message
isLoading,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'filters' does not exist on type '{ child... Remove this comment to see the full error message
filters,
}) {
const { type } = widget;
@@ -68,32 +95,44 @@ const DashboardWidget = React.memo(
);
}
if (type === WidgetTypeEnum.TEXTBOX) {
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ widget: any; canEdit: any; isPublic: any; ... Remove this comment to see the full error message
return <TextboxWidget widget={widget} canEdit={canEdit} isPublic={isPublic} onDelete={onDelete} />;
}
return <RestrictedWidget widget={widget} />;
},
(prevProps, nextProps) =>
// @ts-expect-error ts-migrate(2339) FIXME: Property 'widget' does not exist on type 'Readonly... Remove this comment to see the full error message
prevProps.widget === nextProps.widget &&
// @ts-expect-error ts-migrate(2339) FIXME: Property 'canEdit' does not exist on type 'Readonl... Remove this comment to see the full error message
prevProps.canEdit === nextProps.canEdit &&
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isPublic' does not exist on type 'Readon... Remove this comment to see the full error message
prevProps.isPublic === nextProps.isPublic &&
// @ts-expect-error ts-migrate(2339) FIXME: Property 'isLoading' does not exist on type 'Reado... Remove this comment to see the full error message
prevProps.isLoading === nextProps.isLoading &&
// @ts-expect-error ts-migrate(2339) FIXME: Property 'filters' does not exist on type 'Readonl... Remove this comment to see the full error message
prevProps.filters === nextProps.filters
);
class DashboardGrid extends React.Component {
static propTypes = {
isEditing: PropTypes.bool.isRequired,
isPublic: PropTypes.bool,
dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
widgets: PropTypes.arrayOf(WidgetType).isRequired,
filters: FiltersType,
onBreakpointChange: PropTypes.func,
onLoadWidget: PropTypes.func,
onRefreshWidget: PropTypes.func,
onRemoveWidget: PropTypes.func,
onLayoutChange: PropTypes.func,
onParameterMappingsChange: PropTypes.func,
};
type OwnProps = {
isEditing: boolean;
isPublic?: boolean;
dashboard: any;
widgets: WidgetType[];
// @ts-expect-error ts-migrate(2749) FIXME: 'FiltersType' refers to a value, but is being used... Remove this comment to see the full error message
filters?: FiltersType;
onBreakpointChange?: (...args: any[]) => any;
onLoadWidget?: (...args: any[]) => any;
onRefreshWidget?: (...args: any[]) => any;
onRemoveWidget?: (...args: any[]) => any;
onLayoutChange?: (...args: any[]) => any;
onParameterMappingsChange?: (...args: any[]) => any;
};
type State = any;
type Props = OwnProps & typeof DashboardGrid.defaultProps;
class DashboardGrid extends React.Component<Props, State> {
static defaultProps = {
isPublic: false,
@@ -106,7 +145,7 @@ class DashboardGrid extends React.Component {
onParameterMappingsChange: () => {},
};
static normalizeFrom(widget) {
static normalizeFrom(widget: any) {
const {
id,
options: { position: pos },
@@ -129,7 +168,7 @@ class DashboardGrid extends React.Component {
autoHeightCtrl = null;
constructor(props) {
constructor(props: Props) {
super(props);
this.state = {
@@ -138,7 +177,9 @@ class DashboardGrid extends React.Component {
};
// init AutoHeightController
// @ts-expect-error ts-migrate(2322) FIXME: Type 'AutoHeightController' is not assignable to t... Remove this comment to see the full error message
this.autoHeightCtrl = new AutoHeightController(this.onWidgetHeightUpdated);
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
this.autoHeightCtrl.update(this.props.widgets);
}
@@ -153,14 +194,16 @@ class DashboardGrid extends React.Component {
componentDidUpdate() {
// update, in case widgets added or removed
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
this.autoHeightCtrl.update(this.props.widgets);
}
componentWillUnmount() {
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
this.autoHeightCtrl.destroy();
}
onLayoutChange = (_, layouts) => {
onLayoutChange = (_: any, layouts: any) => {
// workaround for when dashboard starts at single mode and then multi is empty or carries single col data
// fixes test dashboard_spec['shows widgets with full width']
// TODO: open react-grid-layout issue
@@ -170,6 +213,7 @@ class DashboardGrid extends React.Component {
// workaround for https://github.com/STRML/react-grid-layout/issues/889
// remove next line when fix lands
// @ts-expect-error ts-migrate(2322) FIXME: Type '"single-column" | "multi-column"' is not ass... Remove this comment to see the full error message
this.mode = document.body.offsetWidth <= cfg.mobileBreakPoint ? SINGLE : MULTI;
// end workaround
@@ -186,14 +230,16 @@ class DashboardGrid extends React.Component {
this.props.onLayoutChange(normalized);
};
onBreakpointChange = mode => {
onBreakpointChange = (mode: any) => {
this.mode = mode;
this.props.onBreakpointChange(mode === SINGLE);
};
// height updated by auto-height
onWidgetHeightUpdated = (widgetId, newHeight) => {
this.setState(({ layouts }) => {
onWidgetHeightUpdated = (widgetId: any, newHeight: any) => {
this.setState(({
layouts
}: any) => {
const layout = cloneDeep(layouts[MULTI]); // must clone to allow react-grid-layout to compare prev/next state
const item = find(layout, { i: widgetId.toString() });
if (item) {
@@ -206,20 +252,23 @@ class DashboardGrid extends React.Component {
};
// height updated by manual resize
onWidgetResize = (layout, oldItem, newItem) => {
onWidgetResize = (layout: any, oldItem: any, newItem: any) => {
if (oldItem.h !== newItem.h) {
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
this.autoHeightCtrl.remove(Number(newItem.i));
}
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
this.autoHeightCtrl.resume();
};
normalizeTo = layout => ({
normalizeTo = (layout: any) => ({
col: layout.x,
row: layout.y,
sizeX: layout.w,
sizeY: layout.h,
autoHeight: this.autoHeightCtrl.exists(layout.i),
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
autoHeight: this.autoHeightCtrl.exists(layout.i)
});
render() {
@@ -238,12 +287,14 @@ class DashboardGrid extends React.Component {
return (
<div className={className}>
<ResponsiveGridLayout
draggableCancel="input"
className={cx("layout", { "disable-animations": this.state.disableAnimations })}
cols={{ [MULTI]: cfg.columns, [SINGLE]: 1 }}
rowHeight={cfg.rowHeight - cfg.margins}
margin={[cfg.margins, cfg.margins]}
isDraggable={this.props.isEditing}
isResizable={this.props.isEditing}
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
onResizeStart={this.autoHeightCtrl.stop}
onResizeStop={this.onWidgetResize}
layouts={this.state.layouts}
@@ -257,13 +308,16 @@ class DashboardGrid extends React.Component {
data-widgetid={widget.id}
data-test={`WidgetId${widget.id}`}
className={cx("dashboard-widget-wrapper", {
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
"widget-auto-height-enabled": this.autoHeightCtrl.exists(widget.id),
})}>
<DashboardWidget
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ dashboard: any; widget: WidgetType; filter... Remove this comment to see the full error message
dashboard={dashboard}
widget={widget}
filters={filters}
isPublic={isPublic}
// @ts-expect-error ts-migrate(2339) FIXME: Property 'loading' does not exist on type 'WidgetT... Remove this comment to see the full error message
isLoading={widget.loading}
canEdit={dashboard.canEdit()}
onLoadWidget={onLoadWidget}

View File

@@ -1,7 +1,7 @@
import { isMatch, map, find, sortBy } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Modal from "antd/lib/modal";
// @ts-expect-error ts-migrate(6133) FIXME: 'DialogPropType' is declared but its value is neve... Remove this comment to see the full error message
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import {
MappingType,
@@ -12,7 +12,7 @@ import {
} from "@/components/ParameterMappingInput";
import notification from "@/services/notification";
export function getParamValuesSnapshot(mappings, dashboardParameters) {
export function getParamValuesSnapshot(mappings: any, dashboardParameters: any) {
return map(
sortBy(mappings, m => m.name),
m => {
@@ -32,24 +32,30 @@ export function getParamValuesSnapshot(mappings, dashboardParameters) {
);
}
class EditParameterMappingsDialog extends React.Component {
static propTypes = {
dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
dialog: DialogPropType.isRequired,
};
type Props = {
dashboard: any;
widget: any;
// @ts-expect-error ts-migrate(2749) FIXME: 'DialogPropType' refers to a value, but is being u... Remove this comment to see the full error message
dialog: DialogPropType;
};
type State = any;
class EditParameterMappingsDialog extends React.Component<Props, State> {
originalParamValuesSnapshot = null;
constructor(props) {
constructor(props: Props) {
super(props);
const parameterMappings = parameterMappingsToEditableMappings(
props.widget.options.parameterMappings,
props.widget.query.getParametersDefs(),
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any[]' is not assignable to para... Remove this comment to see the full error message
map(this.props.dashboard.getParametersDefs(), p => p.name)
);
// @ts-expect-error ts-migrate(2322) FIXME: Type '(any[] | undefined)[]' is not assignable to ... Remove this comment to see the full error message
this.originalParamValuesSnapshot = getParamValuesSnapshot(
parameterMappings,
this.props.dashboard.getParametersDefs()
@@ -70,6 +76,7 @@ class EditParameterMappingsDialog extends React.Component {
widget.options.parameterMappings = newMappings;
const valuesChanged = !isMatch(
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'null' is not assignable to param... Remove this comment to see the full error message
this.originalParamValuesSnapshot,
getParamValuesSnapshot(this.state.parameterMappings, this.props.dashboard.getParametersDefs())
);
@@ -84,6 +91,7 @@ class EditParameterMappingsDialog extends React.Component {
this.props.dialog.close(valuesChanged);
})
.catch(() => {
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
notification.error("Widget cannot be updated");
})
.finally(() => {
@@ -91,7 +99,7 @@ class EditParameterMappingsDialog extends React.Component {
});
}
updateParamMappings(parameterMappings) {
updateParamMappings(parameterMappings: any) {
this.setState({ parameterMappings });
}
@@ -108,7 +116,8 @@ class EditParameterMappingsDialog extends React.Component {
<ParameterMappingListInput
mappings={this.state.parameterMappings}
existingParams={this.props.dashboard.getParametersDefs()}
onChange={mappings => this.updateParamMappings(mappings)}
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
onChange={(mappings: any) => this.updateParamMappings(mappings)}
/>
)}
</Modal>

View File

@@ -1,34 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Modal from "antd/lib/modal";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import VisualizationRenderer from "@/components/visualizations/VisualizationRenderer";
import VisualizationName from "@/components/visualizations/VisualizationName";
function ExpandedWidgetDialog({ dialog, widget }) {
return (
<Modal
{...dialog.props}
title={
<>
<VisualizationName visualization={widget.visualization} /> <span>{widget.getQuery().name}</span>
</>
}
width="95%"
footer={<Button onClick={dialog.dismiss}>Close</Button>}>
<VisualizationRenderer
visualization={widget.visualization}
queryResult={widget.getQueryResult()}
context="widget"
/>
</Modal>
);
}
ExpandedWidgetDialog.propTypes = {
dialog: DialogPropType.isRequired,
widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};
export default wrapDialog(ExpandedWidgetDialog);

View File

@@ -0,0 +1,46 @@
import React from "react";
import Button from "antd/lib/button";
import Modal from "antd/lib/modal";
// @ts-expect-error ts-migrate(6133) FIXME: 'DialogPropType' is declared but its value is neve... Remove this comment to see the full error message
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
// @ts-expect-error ts-migrate(6133) FIXME: 'FiltersType' is declared but its value is never r... Remove this comment to see the full error message
import { FiltersType } from "@/components/Filters";
import VisualizationRenderer from "@/components/visualizations/VisualizationRenderer";
import VisualizationName from "@/components/visualizations/VisualizationName";
type OwnProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'DialogPropType' refers to a value, but is being u... Remove this comment to see the full error message
dialog: DialogPropType;
widget: any;
// @ts-expect-error ts-migrate(2749) FIXME: 'FiltersType' refers to a value, but is being used... Remove this comment to see the full error message
filters?: FiltersType;
};
type Props = OwnProps & typeof ExpandedWidgetDialog.defaultProps;
function ExpandedWidgetDialog({ dialog, widget, filters }: Props) {
return (
<Modal
{...dialog.props}
title={
<>
<VisualizationName visualization={widget.visualization} /> <span>{widget.getQuery().name}</span>
</>
}
width="95%"
footer={<Button onClick={dialog.dismiss}>Close</Button>}>
<VisualizationRenderer
visualization={widget.visualization}
queryResult={widget.getQueryResult()}
filters={filters}
context="widget"
/>
</Modal>
);
}
ExpandedWidgetDialog.defaultProps = {
filters: [],
};
export default wrapDialog(ExpandedWidgetDialog);

View File

@@ -1,7 +1,7 @@
import { toString } from "lodash";
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'mark... Remove this comment to see the full error message
import { markdown } from "markdown";
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import { useDebouncedCallback } from "use-debounce";
import Modal from "antd/lib/modal";
import Input from "antd/lib/input";
@@ -9,12 +9,22 @@ import Tooltip from "antd/lib/tooltip";
import Divider from "antd/lib/divider";
import Link from "@/components/Link";
import HtmlContent from "@redash/viz/lib/components/HtmlContent";
// @ts-expect-error ts-migrate(6133) FIXME: 'DialogPropType' is declared but its value is neve... Remove this comment to see the full error message
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import notification from "@/services/notification";
import "./TextboxDialog.less";
function TextboxDialog({ dialog, isNew, ...props }) {
type OwnProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'DialogPropType' refers to a value, but is being u... Remove this comment to see the full error message
dialog: DialogPropType;
isNew?: boolean;
text?: string;
};
type Props = OwnProps & typeof TextboxDialog.defaultProps;
function TextboxDialog({ dialog, isNew, ...props }: Props) {
const [text, setText] = useState(toString(props.text));
const [preview, setPreview] = useState(null);
@@ -37,6 +47,7 @@ function TextboxDialog({ dialog, isNew, ...props }) {
const saveWidget = useCallback(() => {
dialog.close(text).catch(() => {
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'string' is not assignable to par... Remove this comment to see the full error message
notification.error(isNew ? "Widget could not be added" : "Widget could not be saved");
});
}, [dialog, isNew, text]);
@@ -71,6 +82,7 @@ function TextboxDialog({ dialog, isNew, ...props }) {
<div className="textbox-dialog">
<Input.TextArea
className="resize-vertical"
// @ts-expect-error ts-migrate(2322) FIXME: Type 'string' is not assignable to type 'number | ... Remove this comment to see the full error message
rows="5"
value={text}
onChange={handleInputChange}
@@ -83,6 +95,7 @@ function TextboxDialog({ dialog, isNew, ...props }) {
target="_blank"
rel="noopener noreferrer"
href="https://www.markdownguide.org/cheat-sheet/#basic-syntax">
{/* @ts-expect-error ts-migrate(2747) FIXME: 'Tooltip' components don't accept text as child el... Remove this comment to see the full error message */}
<Tooltip title="Markdown guide opens in new window">Markdown</Tooltip>
</Link>
.
@@ -91,6 +104,7 @@ function TextboxDialog({ dialog, isNew, ...props }) {
<React.Fragment>
<Divider dashed />
<strong className="preview-title">Preview:</strong>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: null; className: string; }' is n... Remove this comment to see the full error message */}
<HtmlContent className="preview markdown">{preview}</HtmlContent>
</React.Fragment>
)}
@@ -99,12 +113,6 @@ function TextboxDialog({ dialog, isNew, ...props }) {
);
}
TextboxDialog.propTypes = {
dialog: DialogPropType.isRequired,
isNew: PropTypes.bool,
text: PropTypes.string,
};
TextboxDialog.defaultProps = {
isNew: false,
text: "",

View File

@@ -48,10 +48,10 @@
top: 0;
left: 0;
bottom: 85px;
right: 15px;
right: 0;
background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px),
linear-gradient(to left, #b3babf, #b3babf 1px, transparent 1px, transparent);
background-size: calc((100vw - 15px) / 6) 5px;
background-size: calc((100% + 15px) / 6) 5px;
background-position: -7px 1px;
}
}

View File

@@ -1,7 +1,7 @@
import React from "react";
import Widget from "./Widget";
function RestrictedWidget(props) {
function RestrictedWidget(props: any) {
return (
<Widget {...props} className="d-flex justify-content-center align-items-center widget-restricted">
<div className="t-body scrollbox">

View File

@@ -1,19 +1,26 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'mark... Remove this comment to see the full error message
import { markdown } from "markdown";
import Menu from "antd/lib/menu";
import HtmlContent from "@redash/viz/lib/components/HtmlContent";
import TextboxDialog from "@/components/dashboards/TextboxDialog";
import Widget from "./Widget";
function TextboxWidget(props) {
type OwnProps = {
widget: any;
canEdit?: boolean;
};
type Props = OwnProps & typeof TextboxWidget.defaultProps;
function TextboxWidget(props: Props) {
const { widget, canEdit } = props;
const [text, setText] = useState(widget.text);
const editTextBox = () => {
TextboxDialog.showModal({
text: widget.text,
}).onClose(newText => {
}).onClose((newText: any) => {
widget.text = newText;
setText(newText);
return widget.save();
@@ -31,17 +38,14 @@ function TextboxWidget(props) {
}
return (
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<Widget {...props} menuOptions={canEdit ? TextboxMenuOptions : null} className="widget-text">
{/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: any; className: string; }' is no... Remove this comment to see the full error message */}
<HtmlContent className="body-row-auto scrollbox t-body p-15 markdown">{markdown.toHTML(text || "")}</HtmlContent>
</Widget>
);
}
TextboxWidget.propTypes = {
widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool,
};
TextboxWidget.defaultProps = {
canEdit: false,
};

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { compact, isEmpty, invoke } from "lodash";
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'mark... Remove this comment to see the full error message
import { markdown } from "markdown";
import cx from "classnames";
import Menu from "antd/lib/menu";
@@ -12,22 +12,28 @@ import Link from "@/components/Link";
import Parameters from "@/components/Parameters";
import TimeAgo from "@/components/TimeAgo";
import Timer from "@/components/Timer";
// @ts-expect-error ts-migrate(6133) FIXME: 'Moment' is declared but its value is never read.
import { Moment } from "@/components/proptypes";
import QueryLink from "@/components/QueryLink";
// @ts-expect-error ts-migrate(6133) FIXME: 'FiltersType' is declared but its value is never r... Remove this comment to see the full error message
import { FiltersType } from "@/components/Filters";
import ExpandedWidgetDialog from "@/components/dashboards/ExpandedWidgetDialog";
import EditParameterMappingsDialog from "@/components/dashboards/EditParameterMappingsDialog";
import VisualizationRenderer from "@/components/visualizations/VisualizationRenderer";
import Widget from "./Widget";
function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParametersEdit }) {
function visualizationWidgetMenuOptions({
widget,
canEditDashboard,
onParametersEdit
}: any) {
const canViewQuery = currentUser.hasPermission("view_query");
const canEditParameters = canEditDashboard && !isEmpty(invoke(widget, "query.getParametersDefs"));
const widgetQueryResult = widget.getQueryResult();
const isQueryResultEmpty = !widgetQueryResult || !widgetQueryResult.isEmpty || widgetQueryResult.isEmpty();
const downloadLink = fileType => widgetQueryResult.getLink(widget.getQuery().id, fileType);
const downloadName = fileType => widgetQueryResult.getName(widget.getQuery().name, fileType);
const downloadLink = (fileType: any) => widgetQueryResult.getLink(widget.getQuery().id, fileType);
const downloadName = (fileType: any) => widgetQueryResult.getName(widget.getQuery().name, fileType);
return compact([
<Menu.Item key="download_csv" disabled={isQueryResultEmpty}>
{!isQueryResultEmpty ? (
@@ -70,7 +76,14 @@ function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParameters
]);
}
function RefreshIndicator({ refreshStartedAt }) {
type OwnRefreshIndicatorProps = {
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
refreshStartedAt?: Moment;
};
type RefreshIndicatorProps = OwnRefreshIndicatorProps & typeof RefreshIndicator.defaultProps;
function RefreshIndicator({ refreshStartedAt }: RefreshIndicatorProps) {
return (
<div className="refresh-indicator">
<div className="refresh-icon">
@@ -80,11 +93,19 @@ function RefreshIndicator({ refreshStartedAt }) {
</div>
);
}
RefreshIndicator.propTypes = { refreshStartedAt: Moment };
RefreshIndicator.defaultProps = { refreshStartedAt: null };
function VisualizationWidgetHeader({ widget, refreshStartedAt, parameters, onParametersUpdate }) {
type OwnVisualizationWidgetHeaderProps = {
widget: any;
// @ts-expect-error ts-migrate(2749) FIXME: 'Moment' refers to a value, but is being used as a... Remove this comment to see the full error message
refreshStartedAt?: Moment;
parameters?: any[];
onParametersUpdate?: (...args: any[]) => any;
};
type VisualizationWidgetHeaderProps = OwnVisualizationWidgetHeaderProps & typeof VisualizationWidgetHeader.defaultProps;
function VisualizationWidgetHeader({ widget, refreshStartedAt, parameters, onParametersUpdate }: VisualizationWidgetHeaderProps) {
const canViewQuery = currentUser.hasPermission("view_query");
return (
@@ -93,9 +114,11 @@ function VisualizationWidgetHeader({ widget, refreshStartedAt, parameters, onPar
<div className="t-header widget clearfix">
<div className="th-title">
<p>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'. */}
<QueryLink query={widget.getQuery()} visualization={widget.visualization} readOnly={!canViewQuery} />
</p>
{!isEmpty(widget.getQuery().description) && (
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: any; className: string; }' is no... Remove this comment to see the full error message
<HtmlContent className="text-muted markdown query--description">
{markdown.toHTML(widget.getQuery().description || "")}
</HtmlContent>
@@ -111,27 +134,30 @@ function VisualizationWidgetHeader({ widget, refreshStartedAt, parameters, onPar
);
}
VisualizationWidgetHeader.propTypes = {
widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
refreshStartedAt: Moment,
parameters: PropTypes.arrayOf(PropTypes.object),
onParametersUpdate: PropTypes.func,
};
VisualizationWidgetHeader.defaultProps = {
refreshStartedAt: null,
onParametersUpdate: () => {},
parameters: [],
};
function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) {
type OwnVisualizationWidgetFooterProps = {
widget: any;
isPublic?: boolean;
onRefresh: (...args: any[]) => any;
onExpand: (...args: any[]) => any;
};
type VisualizationWidgetFooterProps = OwnVisualizationWidgetFooterProps & typeof VisualizationWidgetFooter.defaultProps;
function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }: VisualizationWidgetFooterProps) {
const widgetQueryResult = widget.getQueryResult();
const updatedAt = invoke(widgetQueryResult, "getUpdatedAt");
const [refreshClickButtonId, setRefreshClickButtonId] = useState();
const refreshWidget = buttonId => {
const refreshWidget = (buttonId: any) => {
if (!refreshClickButtonId) {
setRefreshClickButtonId(buttonId);
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'null' is not assignable to param... Remove this comment to see the full error message
onRefresh().finally(() => setRefreshClickButtonId(null));
}
};
@@ -173,28 +199,27 @@ function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) {
) : null;
}
VisualizationWidgetFooter.propTypes = {
widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isPublic: PropTypes.bool,
onRefresh: PropTypes.func.isRequired,
onExpand: PropTypes.func.isRequired,
};
VisualizationWidgetFooter.defaultProps = { isPublic: false };
class VisualizationWidget extends React.Component {
static propTypes = {
widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
filters: FiltersType,
isPublic: PropTypes.bool,
isLoading: PropTypes.bool,
canEdit: PropTypes.bool,
onLoad: PropTypes.func,
onRefresh: PropTypes.func,
onDelete: PropTypes.func,
onParameterMappingsChange: PropTypes.func,
};
type OwnVisualizationWidgetProps = {
widget: any;
dashboard: any;
// @ts-expect-error ts-migrate(2749) FIXME: 'FiltersType' refers to a value, but is being used... Remove this comment to see the full error message
filters?: FiltersType;
isPublic?: boolean;
isLoading?: boolean;
canEdit?: boolean;
onLoad?: (...args: any[]) => any;
onRefresh?: (...args: any[]) => any;
onDelete?: (...args: any[]) => any;
onParameterMappingsChange?: (...args: any[]) => any;
};
type VisualizationWidgetState = any;
type VisualizationWidgetProps = OwnVisualizationWidgetProps & typeof VisualizationWidget.defaultProps;
class VisualizationWidget extends React.Component<VisualizationWidgetProps, VisualizationWidgetState> {
static defaultProps = {
filters: [],
@@ -207,9 +232,12 @@ class VisualizationWidget extends React.Component {
onParameterMappingsChange: () => {},
};
constructor(props) {
constructor(props: VisualizationWidgetProps) {
super(props);
this.state = { localParameters: props.widget.getLocalParameters() };
this.state = {
localParameters: props.widget.getLocalParameters(),
localFilters: props.filters,
};
}
componentDidMount() {
@@ -219,8 +247,12 @@ class VisualizationWidget extends React.Component {
onLoad();
}
onLocalFiltersChange = (localFilters: any) => {
this.setState({ localFilters });
};
expandWidget = () => {
ExpandedWidgetDialog.showModal({ widget: this.props.widget });
ExpandedWidgetDialog.showModal({ widget: this.props.widget, filters: this.state.localFilters });
};
editParameterMappings = () => {
@@ -228,7 +260,7 @@ class VisualizationWidget extends React.Component {
EditParameterMappingsDialog.showModal({
dashboard,
widget,
}).onClose(valuesChanged => {
}).onClose((valuesChanged: any) => {
// refresh widget if any parameter value has been updated
if (valuesChanged) {
onRefresh();
@@ -260,6 +292,8 @@ class VisualizationWidget extends React.Component {
visualization={widget.visualization}
queryResult={widgetQueryResult}
filters={filters}
// @ts-expect-error ts-migrate(2322) FIXME: Type '(localFilters: any) => void' is not assignab... Remove this comment to see the full error message
onFiltersChange={this.onLocalFiltersChange}
context="widget"
/>
</div>
@@ -282,6 +316,7 @@ class VisualizationWidget extends React.Component {
const isRefreshing = isLoading && !!(widgetQueryResult && widgetQueryResult.getStatus());
return (
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
<Widget
{...this.props}
className="widget-visualization"

Some files were not shown because too many files have changed in this diff Show More