mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Compare commits
56 Commits
system-sta
...
ts-migrate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
521ca6afa4 | ||
|
|
1ba94d30a1 | ||
|
|
e4d2c82338 | ||
|
|
95621a93bc | ||
|
|
2f1ed63bd5 | ||
|
|
f23f1d1924 | ||
|
|
c426379bef | ||
|
|
501ca0bef8 | ||
|
|
4c385f85f1 | ||
|
|
698d87ed48 | ||
|
|
c290864ccd | ||
|
|
b70e95a323 | ||
|
|
18ee5343aa | ||
|
|
fdf636a393 | ||
|
|
88c13868a3 | ||
|
|
aab11dc79b | ||
|
|
00c77cf36e | ||
|
|
6e2631dec2 | ||
|
|
4b88959341 | ||
|
|
fa2b57a209 | ||
|
|
132fed64b3 | ||
|
|
fa7ecca485 | ||
|
|
8f484706b1 | ||
|
|
e2e8714155 | ||
|
|
c6bf8a1c55 | ||
|
|
12f71925c2 | ||
|
|
cae088f35b | ||
|
|
a3c79f26b9 | ||
|
|
c7c92a3192 | ||
|
|
55cf17aa47 | ||
|
|
8dd76a00c5 | ||
|
|
e242ac2b10 | ||
|
|
66463aedd4 | ||
|
|
8a6524c1ba | ||
|
|
9097feb100 | ||
|
|
db4e97fa6f | ||
|
|
0d4615a482 | ||
|
|
ff008a076b | ||
|
|
8d548ecbac | ||
|
|
2992c382d1 | ||
|
|
f4dcb2918a | ||
|
|
c821cab4cb | ||
|
|
4fb77867b0 | ||
|
|
a473611cb0 | ||
|
|
210008c714 | ||
|
|
aa5d4f5f4e | ||
|
|
6b811c5245 | ||
|
|
83726da48a | ||
|
|
72dc157bbe | ||
|
|
1b8ff8e810 | ||
|
|
31ddd0fb79 | ||
|
|
5cabf7a724 | ||
|
|
59b135ace7 | ||
|
|
32b41e4112 | ||
|
|
2e31b91054 | ||
|
|
205915e6db |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
2
.gitignore
vendored
@@ -5,6 +5,8 @@ venv/
|
||||
.coveralls.yml
|
||||
.idea
|
||||
*.pyc
|
||||
.nyc_output
|
||||
coverage
|
||||
.coverage
|
||||
coverage.xml
|
||||
client/dist
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -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
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -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
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -20,5 +20,10 @@
|
||||
"globals": ["Error"]
|
||||
}
|
||||
]
|
||||
]
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["istanbul"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
5
client/app/__tests__/enzyme_setup.ts
Normal file
5
client/app/__tests__/enzyme_setup.ts
Normal 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 |
@@ -141,6 +141,7 @@ a.label-tag {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.query-fullscreen {
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 */}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 don’t 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,
|
||||
};
|
||||
@@ -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);
|
||||
72
client/app/components/ApplicationArea/ErrorMessage.tsx
Normal file
72
client/app/components/ApplicationArea/ErrorMessage.tsx
Normal 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 don’t 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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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: () => {},
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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} />,
|
||||
};
|
||||
}
|
||||
@@ -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} />
|
||||
),
|
||||
};
|
||||
}
|
||||
109
client/app/components/ApplicationArea/routeWithUserSession.tsx
Normal file
109
client/app/components/ApplicationArea/routeWithUserSession.tsx
Normal 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} />}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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" />
|
||||
</>
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -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>,
|
||||
@@ -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;
|
||||
48
client/app/components/DateInput.tsx
Normal file
48
client/app/components/DateInput.tsx
Normal 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;
|
||||
@@ -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;
|
||||
51
client/app/components/DateRangeInput.tsx
Normal file
51
client/app/components/DateRangeInput.tsx
Normal 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;
|
||||
@@ -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;
|
||||
51
client/app/components/DateTimeInput.tsx
Normal file
51
client/app/components/DateTimeInput.tsx
Normal 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;
|
||||
@@ -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,
|
||||
4
client/app/components/DialogWrapper.d.ts
vendored
4
client/app/components/DialogWrapper.d.ts
vendored
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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: [],
|
||||
};
|
||||
@@ -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,
|
||||
@@ -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",
|
||||
@@ -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: "",
|
||||
};
|
||||
@@ -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'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",
|
||||
@@ -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;
|
||||
|
||||
@@ -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: () => {},
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
282
client/app/components/HelpTrigger.tsx
Normal file
282
client/app/components/HelpTrigger.tsx
Normal 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;
|
||||
@@ -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();
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
<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,
|
||||
};
|
||||
22
client/app/components/NoTaggedObjectsFound.tsx
Normal file
22
client/app/components/NoTaggedObjectsFound.tsx
Normal 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
|
||||
{/* @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. */}
|
||||
<TagsControl className="inline-tags-control" tags={Array.from(tags)} tagSeparator={"+"} />.
|
||||
</BigMessage>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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: () => {},
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
@@ -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))} />
|
||||
@@ -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}`} />}
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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",
|
||||
@@ -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`
|
||||
@@ -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%",
|
||||
38
client/app/components/SelectWithVirtualScroll.tsx
Normal file
38
client/app/components/SelectWithVirtualScroll.tsx
Normal 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;
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
107
client/app/components/TagsList.tsx
Normal file
107
client/app/components/TagsList.tsx
Normal 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;
|
||||
@@ -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: "",
|
||||
@@ -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,
|
||||
};
|
||||
7
client/app/components/UserGroups.less
Normal file
7
client/app/components/UserGroups.less
Normal file
@@ -0,0 +1,7 @@
|
||||
.user-groups {
|
||||
margin: -5px 0 0 -5px;
|
||||
|
||||
.ant-tag {
|
||||
margin: 5px 0 0 5px;
|
||||
}
|
||||
}
|
||||
31
client/app/components/UserGroups.tsx
Normal file
31
client/app/components/UserGroups.tsx
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
80
client/app/components/cards-list/CardsList.tsx
Normal file
80
client/app/components/cards-list/CardsList.tsx
Normal 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,
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
46
client/app/components/dashboards/ExpandedWidgetDialog.tsx
Normal file
46
client/app/components/dashboards/ExpandedWidgetDialog.tsx
Normal 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);
|
||||
@@ -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: "",
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user