Prettier all the Javascript code & GitHub Action (#4433)

* Prettier all the JS files

* Add GitHub Action to autoformat code pushed to master

* Fix eslint violation due to formatting.

* Remove GitHub actions for styling

* Add restyled.io config
This commit is contained in:
Arik Fraimovich
2019-12-11 17:05:38 +02:00
committed by GitHub
parent 81b14a58ef
commit 56d3be2248
375 changed files with 10019 additions and 9148 deletions

View File

@@ -1,31 +0,0 @@
name: Python Code Formatting (Black)
on:
push:
branches:
- master
jobs:
format:
runs-on: ubuntu-latest
container:
image: python:3.7.4-alpine
steps:
- uses: actions/checkout@v1
- name: Install Black
run: apk add gcc musl-dev && pip install black
- name: Run Black
run: black redash tests migrations/versions
- name: Commit formatted code
uses: EndBug/add-and-commit@v2.1.0
with:
author_name: Black
author_email: team@redash.io
message: "Autoformatted Python code with Black"
path: "."
pattern: "*.py"
force: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

61
.restyled.yaml Normal file
View File

@@ -0,0 +1,61 @@
enabled: true
auto: false
# Open Restyle PRs?
pull_requests: true
# Leave comments on the original PR linking to the Restyle PR?
comments: true
# Set commit statuses on the original PR?
statuses:
# Red status in the case of differences found
differences: true
# Green status in the case of no differences found
no_differences: true
# Red status if we encounter errors restyling
error: true
# Request review on the Restyle PR?
#
# Possible values:
#
# author: From the author of the original PR
# owner: From the owner of the repository
# none: Don't
#
# One value will apply to both origin and forked PRs, but you can also specify
# separate values.
#
# request_review:
# origin: author
# forked: owner
#
request_review: author
# Add labels to any created Restyle PRs
#
# These can be used to tell other automation to avoid our PRs.
#
labels: ["Skip CI"]
# Labels to ignore
#
# PRs with any of these labels will be ignored by Restyled.
#
# ignore_labels:
# - restyled-ignore
# Restylers to run, and how
restylers:
- name: black:
include:
- redash
- tests
- migrations/versions
- name: prettier:
include:
- client/app/**/*.js
- client/app/**/*.jsx
- client/cypress/**/*.js

View File

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

View File

@@ -1,5 +1,5 @@
import MockDate from 'mockdate';
import MockDate from "mockdate";
const date = new Date('2000-01-01T02:00:00.000');
const date = new Date("2000-01-01T02:00:00.000");
MockDate.set(date);

View File

@@ -1,7 +1,7 @@
import React, { forwardRef } from 'react';
import AceEditor from 'react-ace';
import React, { forwardRef } from "react";
import AceEditor from "react-ace";
import './AceEditorInput.less';
import "./AceEditorInput.less";
function AceEditorInput(props, ref) {
return (

View File

@@ -1,24 +1,24 @@
import React from 'react';
import Tooltip from 'antd/lib/tooltip';
import PropTypes from 'prop-types';
import '@/redash-font/style.less';
import recordEvent from '@/services/recordEvent';
import React from "react";
import Tooltip from "antd/lib/tooltip";
import PropTypes from "prop-types";
import "@/redash-font/style.less";
import recordEvent from "@/services/recordEvent";
export default function AutocompleteToggle({ state, disabled, onToggle }) {
let tooltipMessage = 'Live Autocomplete Enabled';
let icon = 'icon-flash';
let tooltipMessage = "Live Autocomplete Enabled";
let icon = "icon-flash";
if (!state) {
tooltipMessage = 'Live Autocomplete Disabled';
icon = 'icon-flash-off';
tooltipMessage = "Live Autocomplete Disabled";
icon = "icon-flash-off";
}
if (disabled) {
tooltipMessage = 'Live Autocomplete Not Available (Use Ctrl+Space to Trigger)';
icon = 'icon-flash-off';
tooltipMessage = "Live Autocomplete Not Available (Use Ctrl+Space to Trigger)";
icon = "icon-flash-off";
}
const toggle = (newState) => {
recordEvent('toggle_autocomplete', 'screen', 'query_editor', { state: newState });
const toggle = newState => {
recordEvent("toggle_autocomplete", "screen", "query_editor", { state: newState });
onToggle(newState);
};
@@ -26,11 +26,10 @@ export default function AutocompleteToggle({ state, disabled, onToggle }) {
<Tooltip placement="top" title={tooltipMessage}>
<button
type="button"
className={'btn btn-default m-r-5' + (disabled ? ' disabled' : '')}
className={"btn btn-default m-r-5" + (disabled ? " disabled" : "")}
onClick={() => toggle(!state)}
disabled={disabled}
>
<i className={'icon ' + icon} />
disabled={disabled}>
<i className={"icon " + icon} />
</button>
</Tooltip>
);

View File

@@ -1,11 +1,11 @@
import React, { useState } from 'react';
import Card from 'antd/lib/card';
import Button from 'antd/lib/button';
import Typography from 'antd/lib/typography';
import { clientConfig } from '@/services/auth';
import HelpTrigger from '@/components/HelpTrigger';
import DynamicComponent from '@/components/DynamicComponent';
import OrgSettings from '@/services/organizationSettings';
import React, { useState } from "react";
import Card from "antd/lib/card";
import Button from "antd/lib/button";
import Typography from "antd/lib/typography";
import { clientConfig } from "@/services/auth";
import HelpTrigger from "@/components/HelpTrigger";
import DynamicComponent from "@/components/DynamicComponent";
import OrgSettings from "@/services/organizationSettings";
const Text = Typography.Text;
@@ -21,11 +21,11 @@ function BeaconConsent() {
setHide(true);
};
const confirmConsent = (confirm) => {
let message = '🙏 Thank you.';
const confirmConsent = confirm => {
let message = "🙏 Thank you.";
if (!confirm) {
message = 'Settings Saved.';
message = "Settings Saved.";
}
OrgSettings.save({ beacon_consent: confirm }, message)
@@ -40,14 +40,13 @@ function BeaconConsent() {
<DynamicComponent name="BeaconConsent">
<div className="m-t-10 tiled">
<Card
title={(
title={
<>
Would you be ok with sharing anonymous usage data with the Redash team?{' '}
Would you be ok with sharing anonymous usage data with the Redash team?{" "}
<HelpTrigger type="USAGE_DATA_SHARING" />
</>
)}
bordered={false}
>
}
bordered={false}>
<Text>Help Redash improve by automatically sending anonymous usage data:</Text>
<div className="m-t-5">
<ul>
@@ -66,7 +65,8 @@ function BeaconConsent() {
</div>
<div className="m-t-15">
<Text type="secondary">
You can change this setting anytime from the <a href="settings/organization">Organization Settings</a> page.
You can change this setting anytime from the <a href="settings/organization">Organization Settings</a>{" "}
page.
</Text>
</div>
</Card>

View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import React from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
export function BigMessage({ message, icon, children, className }) {
return (
<div className={'p-15 text-center ' + className}>
<div className={"p-15 text-center " + className}>
<h3 className="m-t-0 m-b-0">
<i className={'fa ' + icon} />
<i className={"fa " + icon} />
</h3>
<br />
{message}
@@ -23,13 +23,13 @@ BigMessage.propTypes = {
};
BigMessage.defaultProps = {
message: '',
message: "",
children: null,
className: 'tiled bg-white',
className: "tiled bg-white",
};
export default function init(ngModule) {
ngModule.component('bigMessage', react2angular(BigMessage));
ngModule.component("bigMessage", react2angular(BigMessage));
}
init.init = true;

View File

@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Tooltip from 'antd/lib/tooltip';
import './CodeBlock.less';
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Tooltip from "antd/lib/tooltip";
import "./CodeBlock.less";
export default class CodeBlock extends React.Component {
static propTypes = {
@@ -20,7 +20,7 @@ export default class CodeBlock extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported('copy');
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported("copy");
this.resetCopyState = null;
}
@@ -36,14 +36,14 @@ export default class CodeBlock extends React.Component {
// copy
try {
const success = document.execCommand('copy');
const success = document.execCommand("copy");
if (!success) {
throw new Error();
}
this.setState({ copied: 'Copied!' });
this.setState({ copied: "Copied!" });
} catch (err) {
this.setState({
copied: 'Copy failed',
copied: "Copy failed",
});
}
@@ -58,13 +58,8 @@ export default class CodeBlock extends React.Component {
const { copyable, children, ...props } = this.props;
const copyButton = (
<Tooltip title={this.state.copied || 'Copy'}>
<Button
icon="copy"
type="dashed"
size="small"
onClick={this.copy}
/>
<Tooltip title={this.state.copied || "Copy"}>
<Button icon="copy" type="dashed" size="small" onClick={this.copy} />
</Tooltip>
);

View File

@@ -1,12 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import AntCollapse from 'antd/lib/collapse';
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import AntCollapse from "antd/lib/collapse";
export default function Collapse({ collapsed, children, className, ...props }) {
return (
<AntCollapse {...props} activeKey={collapsed ? null : 'content'} className={cx(className, 'ant-collapse-headerless')}>
<AntCollapse.Panel key="content" header="">{children}</AntCollapse.Panel>
<AntCollapse
{...props}
activeKey={collapsed ? null : "content"}
className={cx(className, "ant-collapse-headerless")}>
<AntCollapse.Panel key="content" header="">
{children}
</AntCollapse.Panel>
</AntCollapse>
);
}
@@ -20,5 +25,5 @@ Collapse.propTypes = {
Collapse.defaultProps = {
collapsed: true,
children: null,
className: '',
className: "",
};

View File

@@ -1,12 +1,12 @@
// ANGULAR_REMOVE_ME
import { react2angular } from 'react2angular';
import { react2angular } from "react2angular";
import ColorPicker from '@/components/ColorPicker';
import ColorPicker from "@/components/ColorPicker";
import './color-box.less';
import "./color-box.less";
export default function init(ngModule) {
ngModule.component('colorBox', react2angular(ColorPicker.Swatch));
ngModule.component("colorBox", react2angular(ColorPicker.Swatch));
}
init.init = true;

View File

@@ -1,12 +1,12 @@
import { isNil, isArray, chunk, map, filter, toPairs } from 'lodash';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import tinycolor from 'tinycolor2';
import TextInput from 'antd/lib/input';
import Typography from 'antd/lib/typography';
import Swatch from './Swatch';
import { isNil, isArray, chunk, map, filter, toPairs } from "lodash";
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import tinycolor from "tinycolor2";
import TextInput from "antd/lib/input";
import Typography from "antd/lib/typography";
import Swatch from "./Swatch";
import './input.less';
import "./input.less";
function preparePresets(presetColors, presetColumns) {
presetColors = isArray(presetColors) ? map(presetColors, v => [null, v]) : toPairs(presetColors);
@@ -16,14 +16,14 @@ function preparePresets(presetColors, presetColumns) {
}
value = tinycolor(value);
if (value.isValid()) {
return [title, '#' + value.toHex().toUpperCase()];
return [title, "#" + value.toHex().toUpperCase()];
}
return null;
});
return chunk(filter(presetColors), presetColumns);
}
function validateColor(value, callback, prefix = '#') {
function validateColor(value, callback, prefix = "#") {
if (isNil(value)) {
callback(null);
}
@@ -34,7 +34,7 @@ function validateColor(value, callback, prefix = '#') {
}
export default function Input({ color, presetColors, presetColumns, onChange, onPressEnter }) {
const [inputValue, setInputValue] = useState('');
const [inputValue, setInputValue] = useState("");
const [isInputFocused, setIsInputFocused] = useState(false);
const presets = preparePresets(presetColors, presetColumns);
@@ -46,7 +46,7 @@ export default function Input({ color, presetColors, presetColumns, onChange, on
useEffect(() => {
if (!isInputFocused) {
validateColor(color, setInputValue, '');
validateColor(color, setInputValue, "");
}
}, [color, isInputFocused]);
@@ -86,7 +86,7 @@ Input.propTypes = {
};
Input.defaultProps = {
color: '#FFFFFF',
color: "#FFFFFF",
presetColors: null,
presetColumns: 8,
onChange: () => {},

View File

@@ -1,17 +1,18 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import React, { useMemo } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { validateColor, getColorName } from './utils';
import './label.less';
import { validateColor, getColorName } from "./utils";
import "./label.less";
export default function Label({ className, color, presetColors, ...props }) {
const name = useMemo(
() => getColorName(validateColor(color), presetColors),
[color, presetColors],
);
const name = useMemo(() => getColorName(validateColor(color), presetColors), [color, presetColors]);
return <span className={cx('color-label', className)} {...props}>{name}</span>;
return (
<span className={cx("color-label", className)} {...props}>
{name}
</span>
);
}
Label.propTypes = {
@@ -25,6 +26,6 @@ Label.propTypes = {
Label.defaultProps = {
className: null,
color: '#FFFFFF',
color: "#FFFFFF",
presetColors: null,
};

View File

@@ -1,23 +1,21 @@
import { isString } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Tooltip from 'antd/lib/tooltip';
import { isString } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Tooltip from "antd/lib/tooltip";
import './swatch.less';
import "./swatch.less";
export default function Swatch({ className, color, title, size, ...props }) {
const result = (
<span
className={cx('color-swatch', className)}
style={{ backgroundColor: color, width: size }}
{...props}
/>
<span className={cx("color-swatch", className)} style={{ backgroundColor: color, width: size }} {...props} />
);
if (isString(title) && (title !== '')) {
if (isString(title) && title !== "") {
return (
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>{result}</Tooltip>
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>
{result}
</Tooltip>
);
}
return result;
@@ -33,6 +31,6 @@ Swatch.propTypes = {
Swatch.defaultProps = {
className: null,
title: null,
color: 'transparent',
color: "transparent",
size: 12,
};

View File

@@ -1,27 +1,35 @@
import { toString } from 'lodash';
import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Popover from 'antd/lib/popover';
import Card from 'antd/lib/card';
import Tooltip from 'antd/lib/tooltip';
import Icon from 'antd/lib/icon';
import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
import { toString } from "lodash";
import React, { useState, useEffect, useMemo } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Popover from "antd/lib/popover";
import Card from "antd/lib/card";
import Tooltip from "antd/lib/tooltip";
import Icon from "antd/lib/icon";
import chooseTextColorForBackground from "@/lib/chooseTextColorForBackground";
import ColorInput from './Input';
import Swatch from './Swatch';
import Label from './Label';
import { validateColor } from './utils';
import ColorInput from "./Input";
import Swatch from "./Swatch";
import Label from "./Label";
import { validateColor } from "./utils";
import './index.less';
import "./index.less";
export default function ColorPicker({
color, placement, presetColors, presetColumns, interactive, children, onChange, triggerProps,
addonBefore, addonAfter,
color,
placement,
presetColors,
presetColumns,
interactive,
children,
onChange,
triggerProps,
addonBefore,
addonAfter,
}) {
const [visible, setVisible] = useState(false);
const validatedColor = useMemo(() => validateColor(color), [color]);
const [currentColor, setCurrentColor] = useState('');
const [currentColor, setCurrentColor] = useState("");
function handleApply() {
setVisible(false);
@@ -36,16 +44,16 @@ export default function ColorPicker({
const actions = [];
if (!interactive) {
actions.push((
actions.push(
<Tooltip key="cancel" title="Cancel">
<Icon type="close" onClick={handleCancel} />
</Tooltip>
));
actions.push((
);
actions.push(
<Tooltip key="apply" title="Apply">
<Icon type="check" onClick={handleApply} />
</Tooltip>
));
);
}
function handleInputChange(newColor) {
@@ -66,9 +74,9 @@ export default function ColorPicker({
{addonBefore}
<Popover
arrowPointAtCenter
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
overlayStyle={{ '--color-picker-selected-color': currentColor }}
content={(
overlayClassName={`color-picker ${interactive ? "color-picker-interactive" : "color-picker-with-actions"}`}
overlayStyle={{ "--color-picker-selected-color": currentColor }}
content={
<Card
data-test="ColorPicker"
className="color-picker-panel"
@@ -78,8 +86,7 @@ export default function ColorPicker({
backgroundColor: currentColor,
color: chooseTextColorForBackground(currentColor),
}}
actions={actions}
>
actions={actions}>
<ColorInput
color={currentColor}
presetColors={presetColors}
@@ -88,18 +95,17 @@ export default function ColorPicker({
onPressEnter={handleApply}
/>
</Card>
)}
}
trigger="click"
placement={placement}
visible={visible}
onVisibleChange={setVisible}
>
onVisibleChange={setVisible}>
{children || (
<Swatch
color={validatedColor}
size={30}
{...triggerProps}
className={cx('color-picker-trigger', triggerProps.className)}
className={cx("color-picker-trigger", triggerProps.className)}
/>
)}
</Popover>
@@ -111,9 +117,18 @@ export default function ColorPicker({
ColorPicker.propTypes = {
color: PropTypes.string,
placement: PropTypes.oneOf([
'top', 'left', 'right', 'bottom',
'topLeft', 'topRight', 'bottomLeft', 'bottomRight',
'leftTop', 'leftBottom', 'rightTop', 'rightBottom',
"top",
"left",
"right",
"bottom",
"topLeft",
"topRight",
"bottomLeft",
"bottomRight",
"leftTop",
"leftBottom",
"rightTop",
"rightBottom",
]),
presetColors: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
@@ -129,8 +144,8 @@ ColorPicker.propTypes = {
};
ColorPicker.defaultProps = {
color: '#FFFFFF',
placement: 'top',
color: "#FFFFFF",
placement: "top",
presetColors: null,
presetColumns: 8,
interactive: false,

View File

@@ -1,9 +1,9 @@
import { isArray, findKey } from 'lodash';
import tinycolor from 'tinycolor2';
import { isArray, findKey } from "lodash";
import tinycolor from "tinycolor2";
export function validateColor(value, fallback = null) {
value = tinycolor(value);
return value.isValid() ? '#' + value.toHex().toUpperCase() : fallback;
return value.isValid() ? "#" + value.toHex().toUpperCase() : fallback;
}
export function getColorName(color, presetColors) {

View File

@@ -1,17 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { isEmpty, toUpper, includes } from 'lodash';
import Button from 'antd/lib/button';
import List from 'antd/lib/list';
import Modal from 'antd/lib/modal';
import Input from 'antd/lib/input';
import Steps from 'antd/lib/steps';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { PreviewCard } from '@/components/PreviewCard';
import EmptyState from '@/components/items-list/components/EmptyState';
import DynamicForm from '@/components/dynamic-form/DynamicForm';
import helper from '@/components/dynamic-form/dynamicFormHelper';
import HelpTrigger, { TYPES as HELP_TRIGGER_TYPES } from '@/components/HelpTrigger';
import React from "react";
import PropTypes from "prop-types";
import { isEmpty, toUpper, includes } from "lodash";
import Button from "antd/lib/button";
import List from "antd/lib/list";
import Modal from "antd/lib/modal";
import Input from "antd/lib/input";
import Steps from "antd/lib/steps";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { PreviewCard } from "@/components/PreviewCard";
import EmptyState from "@/components/items-list/components/EmptyState";
import DynamicForm from "@/components/dynamic-form/DynamicForm";
import helper from "@/components/dynamic-form/dynamicFormHelper";
import HelpTrigger, { TYPES as HELP_TRIGGER_TYPES } from "@/components/HelpTrigger";
const { Step } = Steps;
const { Search } = Input;
@@ -38,19 +38,19 @@ class CreateSourceDialog extends React.Component {
};
state = {
searchText: '',
searchText: "",
selectedType: null,
savingSource: false,
currentStep: StepEnum.SELECT_TYPE,
};
selectType = (selectedType) => {
selectType = selectedType => {
this.setState({ selectedType, currentStep: StepEnum.CONFIGURE_IT });
};
resetType = () => {
if (this.state.currentStep === StepEnum.CONFIGURE_IT) {
this.setState({ searchText: '', selectedType: null, currentStep: StepEnum.SELECT_TYPE });
this.setState({ searchText: "", selectedType: null, currentStep: StepEnum.SELECT_TYPE });
}
};
@@ -58,10 +58,13 @@ class CreateSourceDialog extends React.Component {
const { selectedType, savingSource } = this.state;
if (!savingSource) {
this.setState({ savingSource: true, currentStep: StepEnum.DONE });
this.props.onCreate(selectedType, values).then((data) => {
successCallback('Saved.');
this.props
.onCreate(selectedType, values)
.then(data => {
successCallback("Saved.");
this.props.dialog.close({ success: true, data });
}).catch((error) => {
})
.catch(error => {
this.setState({ savingSource: false, currentStep: StepEnum.CONFIGURE_IT });
errorCallback(error.message);
});
@@ -71,8 +74,9 @@ class CreateSourceDialog extends React.Component {
renderTypeSelector() {
const { types } = this.props;
const { searchText } = this.state;
const filteredTypes = types.filter(type => isEmpty(searchText) ||
includes(type.name.toLowerCase(), searchText.toLowerCase()));
const filteredTypes = types.filter(
type => isEmpty(searchText) || includes(type.name.toLowerCase(), searchText.toLowerCase())
);
return (
<div className="m-t-10">
<Search
@@ -81,13 +85,11 @@ class CreateSourceDialog extends React.Component {
autoFocus
data-test="SearchSource"
/>
<div className="scrollbox p-5 m-t-10" style={{ minHeight: '30vh', maxHeight: '40vh' }}>
{isEmpty(filteredTypes) ? (<EmptyState className="" />) : (
<List
size="small"
dataSource={filteredTypes}
renderItem={item => this.renderItem(item)}
/>
<div className="scrollbox p-5 m-t-10" style={{ minHeight: "30vh", maxHeight: "40vh" }}>
{isEmpty(filteredTypes) ? (
<EmptyState className="" />
) : (
<List size="small" dataSource={filteredTypes} renderItem={item => this.renderItem(item)} />
)}
</div>
</div>
@@ -102,12 +104,7 @@ class CreateSourceDialog extends React.Component {
return (
<div>
<div className="d-flex justify-content-center align-items-center">
<img
className="p-5"
src={`${imageFolder}/${selectedType.type}.png`}
alt={selectedType.name}
width="48"
/>
<img className="p-5" src={`${imageFolder}/${selectedType.type}.png`} alt={selectedType.name} width="48" />
<h4 className="m-0">{selectedType.name}</h4>
</div>
<div className="text-right">
@@ -117,13 +114,7 @@ class CreateSourceDialog extends React.Component {
</HelpTrigger>
)}
</div>
<DynamicForm
id="sourceForm"
fields={fields}
onSubmit={this.createSource}
feedbackIcons
hideSubmitButton
/>
<DynamicForm id="sourceForm" fields={fields} onSubmit={this.createSource} feedbackIcons hideSubmitButton />
</div>
);
}
@@ -131,10 +122,7 @@ class CreateSourceDialog extends React.Component {
renderItem(item) {
const { imageFolder } = this.props;
return (
<List.Item
className="p-l-10 p-r-10 clickable"
onClick={() => this.selectType(item)}
>
<List.Item className="p-l-10 p-r-10 clickable" onClick={() => this.selectType(item)}>
<PreviewCard title={item.name} imageUrl={`${imageFolder}/${item.type}.png`} roundedImage={false}>
<i className="fa fa-angle-double-right" />
</PreviewCard>
@@ -149,34 +137,38 @@ class CreateSourceDialog extends React.Component {
<Modal
{...dialog.props}
title={`Create a New ${sourceType}`}
footer={(currentStep === StepEnum.SELECT_TYPE) ? [
(<Button key="cancel" onClick={() => dialog.dismiss()}>Cancel</Button>),
(<Button key="submit" type="primary" disabled>Create</Button>),
] : [
(<Button key="previous" onClick={this.resetType}>Previous</Button>),
(
footer={
currentStep === StepEnum.SELECT_TYPE
? [
<Button key="cancel" onClick={() => dialog.dismiss()}>
Cancel
</Button>,
<Button key="submit" type="primary" disabled>
Create
</Button>,
]
: [
<Button key="previous" onClick={this.resetType}>
Previous
</Button>,
<Button
key="submit"
htmlType="submit"
form="sourceForm"
type="primary"
loading={savingSource}
data-test="CreateSourceButton"
>
data-test="CreateSourceButton">
Create
</Button>
),
]}
>
</Button>,
]
}>
<div data-test="CreateSourceDialog">
<Steps className="hidden-xs m-b-10" size="small" current={currentStep} progressDot>
{currentStep === StepEnum.CONFIGURE_IT ? (
<Step
title={<a>Type Selection</a>}
className="clickable"
onClick={this.resetType}
/>
) : (<Step title="Type Selection" />)}
<Step title={<a>Type Selection</a>} className="clickable" onClick={this.resetType} />
) : (
<Step title="Type Selection" />
)}
<Step title="Configuration" />
<Step title="Done" />
</Steps>

View File

@@ -1,17 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
import { Moment } from "@/components/proptypes";
const DateInput = React.forwardRef(({
defaultValue,
value,
onSelect,
className,
...props
}, ref) => {
const format = clientConfig.dateFormat || 'YYYY-MM-DD';
const DateInput = React.forwardRef(({ defaultValue, value, onSelect, className, ...props }, ref) => {
const format = clientConfig.dateFormat || "YYYY-MM-DD";
const additionalAttributes = {};
if (defaultValue && defaultValue.isValid()) {
additionalAttributes.defaultValue = defaultValue;
@@ -43,7 +37,7 @@ DateInput.defaultProps = {
defaultValue: null,
value: undefined,
onSelect: () => {},
className: '',
className: "",
};
export default DateInput;

View File

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

View File

@@ -1,19 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
import { Moment } from "@/components/proptypes";
const DateTimeInput = React.forwardRef(({
defaultValue,
value,
withSeconds,
onSelect,
className,
...props
}, ref) => {
const format = (clientConfig.dateFormat || 'YYYY-MM-DD') +
(withSeconds ? ' HH:mm:ss' : ' HH:mm');
const DateTimeInput = React.forwardRef(({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
const additionalAttributes = {};
if (defaultValue && defaultValue.isValid()) {
additionalAttributes.defaultValue = defaultValue;
@@ -48,7 +40,7 @@ DateTimeInput.defaultProps = {
value: undefined,
withSeconds: false,
onSelect: () => {},
className: '',
className: "",
};
export default DateTimeInput;

View File

@@ -1,22 +1,15 @@
import { isArray } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';
import { isArray } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
import { Moment } from "@/components/proptypes";
const { RangePicker } = DatePicker;
const DateTimeRangeInput = React.forwardRef(({
defaultValue,
value,
withSeconds,
onSelect,
className,
...props
}, ref) => {
const format = (clientConfig.dateFormat || 'YYYY-MM-DD') +
(withSeconds ? ' HH:mm:ss' : ' HH:mm');
const DateTimeRangeInput = React.forwardRef(
({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
const additionalAttributes = {};
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
additionalAttributes.defaultValue = defaultValue;
@@ -35,7 +28,8 @@ const DateTimeRangeInput = React.forwardRef(({
{...props}
/>
);
});
}
);
DateTimeRangeInput.propTypes = {
defaultValue: PropTypes.arrayOf(Moment),
@@ -50,7 +44,7 @@ DateTimeRangeInput.defaultProps = {
value: undefined,
withSeconds: false,
onSelect: () => {},
className: '',
className: "",
};
export default DateTimeRangeInput;

View File

@@ -1,7 +1,7 @@
import { isFunction } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { isFunction } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import ReactDOM from "react-dom";
/**
Wrapper for dialogs based on Ant's <Modal> component.
@@ -140,7 +140,7 @@ function openDialog(DialogComponent, props) {
reject: () => {},
};
const container = document.createElement('div');
const container = document.createElement("div");
document.body.appendChild(container);
function render() {
@@ -176,7 +176,7 @@ function openDialog(DialogComponent, props) {
const result = {
close: closeDialog,
dismiss: dismissDialog,
update: (newProps) => {
update: newProps => {
props = { ...props, ...newProps };
render();
},

View File

@@ -1,15 +1,15 @@
import { isFunction, isString } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { isFunction, isString } from "lodash";
import React from "react";
import PropTypes from "prop-types";
const componentsRegistry = new Map();
const activeInstances = new Set();
export function registerComponent(name, component) {
if (isString(name) && name !== '') {
if (isString(name) && name !== "") {
componentsRegistry.set(name, isFunction(component) ? component : null);
// Refresh active DynamicComponent instances which use this component
activeInstances.forEach((dynamicComponent) => {
activeInstances.forEach(dynamicComponent => {
if (dynamicComponent.props.name === name) {
dynamicComponent.forceUpdate();
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { trim } from 'lodash';
import React from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
import { trim } from "lodash";
export class EditInPlace extends React.Component {
static propTypes = {
@@ -16,8 +16,8 @@ export class EditInPlace extends React.Component {
static defaultProps = {
ignoreBlanks: false,
isEditable: true,
placeholder: '',
value: '',
placeholder: "",
value: "",
};
constructor(props) {
@@ -42,14 +42,14 @@ export class EditInPlace extends React.Component {
stopEditing = () => {
const newValue = trim(this.inputRef.current.value);
const ignorableBlank = this.props.ignoreBlanks && newValue === '';
const ignorableBlank = this.props.ignoreBlanks && newValue === "";
if (!ignorableBlank && newValue !== this.props.value) {
this.props.onDone(newValue);
}
this.setState({ editing: false });
};
keyDown = (event) => {
keyDown = event => {
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
this.stopEditing();
@@ -63,15 +63,15 @@ export class EditInPlace extends React.Component {
role="presentation"
onFocus={this.startEditing}
onClick={this.startEditing}
className={this.props.isEditable ? 'editable' : ''}
>
className={this.props.isEditable ? "editable" : ""}>
{this.props.value || this.props.placeholder}
</span>
);
renderEdit = () => React.createElement(this.props.editor, {
renderEdit = () =>
React.createElement(this.props.editor, {
ref: this.inputRef,
className: 'rd-form-control',
className: "rd-form-control",
defaultValue: this.props.value,
onBlur: this.stopEditing,
onKeyDown: this.keyDown,
@@ -79,7 +79,7 @@ export class EditInPlace extends React.Component {
render() {
return (
<span className={'edit-in-place' + (this.state.editing ? ' active' : '')}>
<span className={"edit-in-place" + (this.state.editing ? " active" : "")}>
{this.state.editing ? this.renderEdit() : this.renderNormal()}
</span>
);
@@ -87,7 +87,7 @@ export class EditInPlace extends React.Component {
}
export default function init(ngModule) {
ngModule.component('editInPlace', react2angular(EditInPlace));
ngModule.component("editInPlace", react2angular(EditInPlace));
}
init.init = true;

View File

@@ -1,23 +1,22 @@
import { includes, words, capitalize, clone, isNull } from 'lodash';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Checkbox from 'antd/lib/checkbox';
import Modal from 'antd/lib/modal';
import Form from 'antd/lib/form';
import Button from 'antd/lib/button';
import Select from 'antd/lib/select';
import Input from 'antd/lib/input';
import Divider from 'antd/lib/divider';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { QuerySelector } from '@/components/QuerySelector';
import { Query } from '@/services/query';
import { includes, words, capitalize, clone, isNull } from "lodash";
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import Checkbox from "antd/lib/checkbox";
import Modal from "antd/lib/modal";
import Form from "antd/lib/form";
import Button from "antd/lib/button";
import Select from "antd/lib/select";
import Input from "antd/lib/input";
import Divider from "antd/lib/divider";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { QuerySelector } from "@/components/QuerySelector";
import { Query } from "@/services/query";
const { Option } = Select;
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
function getDefaultTitle(text) {
return capitalize(words(text).join(' ')); // humanize
return capitalize(words(text).join(" ")); // humanize
}
function isTypeDateRange(type) {
@@ -26,30 +25,26 @@ function isTypeDateRange(type) {
function joinExampleList(multiValuesOptions) {
const { prefix, suffix } = multiValuesOptions;
return ['value1', 'value2', 'value3']
.map(value => `${prefix}${value}${suffix}`)
.join(',');
return ["value1", "value2", "value3"].map(value => `${prefix}${value}${suffix}`).join(",");
}
function NameInput({ name, type, onChange, existingNames, setValidation }) {
let helpText = '';
let validateStatus = '';
let helpText = "";
let validateStatus = "";
if (!name) {
helpText = 'Choose a keyword for this parameter';
helpText = "Choose a keyword for this parameter";
setValidation(false);
} else if (includes(existingNames, name)) {
helpText = 'Parameter with this name already exists';
helpText = "Parameter with this name already exists";
setValidation(false);
validateStatus = 'error';
validateStatus = "error";
} else {
if (isTypeDateRange(type)) {
helpText = (
<React.Fragment>
Appears in query as {' '}
<code style={{ display: 'inline-block', color: 'inherit' }}>
{`{{${name}.start}} {{${name}.end}}`}
</code>
Appears in query as{" "}
<code style={{ display: "inline-block", color: "inherit" }}>{`{{${name}.start}} {{${name}.end}}`}</code>
</React.Fragment>
);
}
@@ -57,13 +52,7 @@ function NameInput({ name, type, onChange, existingNames, setValidation }) {
}
return (
<Form.Item
required
label="Keyword"
help={helpText}
validateStatus={validateStatus}
{...formItemProps}
>
<Form.Item required label="Keyword" help={helpText} validateStatus={validateStatus} {...formItemProps}>
<Input onChange={e => onChange(e.target.value)} autoFocus />
</Form.Item>
);
@@ -88,7 +77,7 @@ function EditParameterSettingsDialog(props) {
useEffect(() => {
const queryId = props.parameter.queryId;
if (queryId) {
Query.get({ id: queryId }, (query) => {
Query.get({ id: queryId }, query => {
setInitialQuery(query);
});
}
@@ -101,12 +90,12 @@ function EditParameterSettingsDialog(props) {
}
// title
if (param.title === '') {
if (param.title === "") {
return false;
}
// query
if (param.type === 'query' && !param.queryId) {
if (param.type === "query" && !param.queryId) {
return false;
}
@@ -129,16 +118,22 @@ function EditParameterSettingsDialog(props) {
return (
<Modal
{...props.dialog.props}
title={isNew ? 'Add Parameter' : param.name}
title={isNew ? "Add Parameter" : param.name}
width={600}
footer={[(
<Button key="cancel" onClick={props.dialog.dismiss}>Cancel</Button>
), (
<Button key="submit" htmlType="submit" disabled={!isFulfilled()} type="primary" form="paramForm" data-test="SaveParameterSettings">
{isNew ? 'Add Parameter' : 'OK'}
</Button>
)]}
>
footer={[
<Button key="cancel" onClick={props.dialog.dismiss}>
Cancel
</Button>,
<Button
key="submit"
htmlType="submit"
disabled={!isFulfilled()}
type="primary"
form="paramForm"
data-test="SaveParameterSettings">
{isNew ? "Add Parameter" : "OK"}
</Button>,
]}>
<Form layout="horizontal" onSubmit={onConfirm} id="paramForm">
{isNew && (
<NameInput
@@ -158,25 +153,35 @@ function EditParameterSettingsDialog(props) {
</Form.Item>
<Form.Item label="Type" {...formItemProps}>
<Select value={param.type} onChange={type => setParam({ ...param, type })} data-test="ParameterTypeSelect">
<Option value="text" data-test="TextParameterTypeOption">Text</Option>
<Option value="number" data-test="NumberParameterTypeOption">Number</Option>
<Option value="text" data-test="TextParameterTypeOption">
Text
</Option>
<Option value="number" data-test="NumberParameterTypeOption">
Number
</Option>
<Option value="enum">Dropdown List</Option>
<Option value="query">Query Based Dropdown List</Option>
<Option disabled key="dv1">
<Divider className="select-option-divider" />
</Option>
<Option value="date" data-test="DateParameterTypeOption">Date</Option>
<Option value="datetime-local" data-test="DateTimeParameterTypeOption">Date and Time</Option>
<Option value="date" data-test="DateParameterTypeOption">
Date
</Option>
<Option value="datetime-local" data-test="DateTimeParameterTypeOption">
Date and Time
</Option>
<Option value="datetime-with-seconds">Date and Time (with seconds)</Option>
<Option disabled key="dv2">
<Divider className="select-option-divider" />
</Option>
<Option value="date-range" data-test="DateRangeParameterTypeOption">Date Range</Option>
<Option value="date-range" data-test="DateRangeParameterTypeOption">
Date Range
</Option>
<Option value="datetime-range">Date and Time Range</Option>
<Option value="datetime-range-with-seconds">Date and Time Range (with seconds)</Option>
</Select>
</Form.Item>
{param.type === 'enum' && (
{param.type === "enum" && (
<Form.Item label="Values" help="Dropdown list values (newline delimited)" {...formItemProps}>
<Input.TextArea
rows={3}
@@ -185,7 +190,7 @@ function EditParameterSettingsDialog(props) {
/>
</Form.Item>
)}
{param.type === 'query' && (
{param.type === "query" && (
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
<QuerySelector
selectedQuery={initialQuery}
@@ -194,45 +199,54 @@ function EditParameterSettingsDialog(props) {
/>
</Form.Item>
)}
{(param.type === 'enum' || param.type === 'query') && (
{(param.type === "enum" || param.type === "query") && (
<Form.Item className="m-b-0" label=" " colon={false} {...formItemProps}>
<Checkbox
defaultChecked={!!param.multiValuesOptions}
onChange={e => setParam({ ...param,
multiValuesOptions: e.target.checked ? {
prefix: '',
suffix: '',
separator: ',',
} : null })}
data-test="AllowMultipleValuesCheckbox"
>
onChange={e =>
setParam({
...param,
multiValuesOptions: e.target.checked
? {
prefix: "",
suffix: "",
separator: ",",
}
: null,
})
}
data-test="AllowMultipleValuesCheckbox">
Allow multiple values
</Checkbox>
</Form.Item>
)}
{(param.type === 'enum' || param.type === 'query') && param.multiValuesOptions && (
{(param.type === "enum" || param.type === "query") && param.multiValuesOptions && (
<Form.Item
label="Quotation"
help={(
help={
<React.Fragment>
Placed in query as: <code>{joinExampleList(param.multiValuesOptions)}</code>
</React.Fragment>
)}
{...formItemProps}
>
}
{...formItemProps}>
<Select
value={param.multiValuesOptions.prefix}
onChange={quoteOption => setParam({ ...param,
onChange={quoteOption =>
setParam({
...param,
multiValuesOptions: {
...param.multiValuesOptions,
prefix: quoteOption,
suffix: quoteOption,
} })}
data-test="QuotationSelect"
>
},
})
}
data-test="QuotationSelect">
<Option value="">None (default)</Option>
<Option value="'">Single Quotation Mark</Option>
<Option value={'"'} data-test="DoubleQuotationMarkOption">Double Quotation Mark</Option>
<Option value={'"'} data-test="DoubleQuotationMarkOption">
Double Quotation Mark
</Option>
</Select>
</Form.Item>
)}

View File

@@ -1,13 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import Dropdown from 'antd/lib/dropdown';
import Menu from 'antd/lib/menu';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import { react2angular } from 'react2angular';
import QueryResultsLink from './QueryResultsLink';
import React from "react";
import PropTypes from "prop-types";
import Dropdown from "antd/lib/dropdown";
import Menu from "antd/lib/menu";
import Button from "antd/lib/button";
import Icon from "antd/lib/icon";
import { react2angular } from "react2angular";
import QueryResultsLink from "./QueryResultsLink";
export function QueryControlDropdown(props) {
const menu = (
@@ -32,8 +31,7 @@ export function QueryControlDropdown(props) {
query={props.query}
queryResult={props.queryResult}
embed={props.embed}
apiKey={props.apiKey}
>
apiKey={props.apiKey}>
<Icon type="file" /> Download as CSV File
</QueryResultsLink>
</Menu.Item>
@@ -44,8 +42,7 @@ export function QueryControlDropdown(props) {
query={props.query}
queryResult={props.queryResult}
embed={props.embed}
apiKey={props.apiKey}
>
apiKey={props.apiKey}>
<Icon type="file-excel" /> Download as Excel File
</QueryResultsLink>
</Menu.Item>
@@ -53,11 +50,7 @@ export function QueryControlDropdown(props) {
);
return (
<Dropdown
trigger={['click']}
overlay={menu}
overlayClassName="query-control-dropdown-overlay"
>
<Dropdown trigger={["click"]} overlay={menu} overlayClassName="query-control-dropdown-overlay">
<Button data-test="QueryControlDropdownButton">
<Icon type="ellipsis" rotate={90} />
</Button>
@@ -72,22 +65,19 @@ QueryControlDropdown.propTypes = {
showEmbedDialog: PropTypes.func.isRequired,
embed: PropTypes.bool,
apiKey: PropTypes.string,
selectedTab: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
selectedTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
openAddToDashboardForm: PropTypes.func.isRequired,
};
QueryControlDropdown.defaultProps = {
queryResult: {},
embed: false,
apiKey: '',
selectedTab: '',
apiKey: "",
selectedTab: "",
};
export default function init(ngModule) {
ngModule.component('queryControlDropdown', react2angular(QueryControlDropdown));
ngModule.component("queryControlDropdown", react2angular(QueryControlDropdown));
}
init.init = true;

View File

@@ -1,9 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import React from "react";
import PropTypes from "prop-types";
export default function QueryResultsLink(props) {
let href = '';
let href = "";
const { query, queryResult, fileType } = props;
const resultId = queryResult.getId && queryResult.getId();
@@ -11,9 +10,7 @@ export default function QueryResultsLink(props) {
if (resultId && resultData && query.name) {
if (query.id) {
href = `api/queries/${query.id}/results/${resultId}.${fileType}${
props.embed ? `?api_key=${props.apiKey}` : ''
}`;
href = `api/queries/${query.id}/results/${resultId}.${fileType}${props.embed ? `?api_key=${props.apiKey}` : ""}`;
} else {
href = `api/query_results/${resultId}.${fileType}`;
}
@@ -33,15 +30,12 @@ QueryResultsLink.propTypes = {
disabled: PropTypes.bool.isRequired,
embed: PropTypes.bool,
apiKey: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
};
QueryResultsLink.defaultProps = {
queryResult: {},
fileType: 'csv',
fileType: "csv",
embed: false,
apiKey: '',
apiKey: "",
};

View File

@@ -1,39 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import { react2angular } from 'react2angular';
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Icon from "antd/lib/icon";
import { react2angular } from "react2angular";
export function EditVisualizationButton(props) {
return (
<Button
data-test="EditVisualization"
className="edit-visualization"
onClick={() => props.openVisualizationEditor(props.selectedTab)}
>
onClick={() => props.openVisualizationEditor(props.selectedTab)}>
<Icon type="form" />
<span className="hidden-xs hidden-s hidden-m">
Edit Visualization
</span>
<span className="hidden-xs hidden-s hidden-m">Edit Visualization</span>
</Button>
);
}
EditVisualizationButton.propTypes = {
openVisualizationEditor: PropTypes.func.isRequired,
selectedTab: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
selectedTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
EditVisualizationButton.defaultProps = {
selectedTab: '',
selectedTab: "",
};
export default function init(ngModule) {
ngModule.component('editVisualizationButton', react2angular(EditVisualizationButton));
ngModule.component("editVisualizationButton", react2angular(EditVisualizationButton));
}
init.init = true;

View File

@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { clientConfig, currentUser } from '@/services/auth';
import Tooltip from 'antd/lib/tooltip';
import Alert from 'antd/lib/alert';
import HelpTrigger from '@/components/HelpTrigger';
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { clientConfig, currentUser } from "@/services/auth";
import Tooltip from "antd/lib/tooltip";
import Alert from "antd/lib/alert";
import HelpTrigger from "@/components/HelpTrigger";
export default function EmailSettingsWarning({ featureName, className, mode, adminOnly }) {
if (!clientConfig.mailSettingsMissing) {
@@ -17,33 +17,31 @@ export default function EmailSettingsWarning({ featureName, className, mode, adm
const message = (
<span>
Your mail server isn&apos;t configured correctly, and is needed for {featureName} to work.{' '}
Your mail server isn&apos;t configured correctly, and is needed for {featureName} to work.{" "}
<HelpTrigger type="MAIL_CONFIG" className="f-inherit" />
</span>
);
if (mode === 'icon') {
if (mode === "icon") {
return (
<Tooltip title={message}>
<i className={cx('fa fa-exclamation-triangle', className)} />
<i className={cx("fa fa-exclamation-triangle", className)} />
</Tooltip>
);
}
return (
<Alert message={message} type="error" className={className} />
);
return <Alert message={message} type="error" className={className} />;
}
EmailSettingsWarning.propTypes = {
featureName: PropTypes.string.isRequired,
className: PropTypes.string,
mode: PropTypes.oneOf(['alert', 'icon']),
mode: PropTypes.oneOf(["alert", "icon"]),
adminOnly: PropTypes.bool,
};
EmailSettingsWarning.defaultProps = {
className: null,
mode: 'alert',
mode: "alert",
adminOnly: false,
};

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { $rootScope } from '@/services/ng';
import React from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
import { $rootScope } from "@/services/ng";
export class FavoritesControl extends React.Component {
static propTypes = {
@@ -16,7 +16,7 @@ export class FavoritesControl extends React.Component {
static defaultProps = {
onChange: () => {},
forceUpdate: '',
forceUpdate: "",
};
toggleItem(event, item, callback) {
@@ -26,21 +26,17 @@ export class FavoritesControl extends React.Component {
action().then(() => {
item.is_favorite = !savedIsFavorite;
this.forceUpdate();
$rootScope.$broadcast('reloadFavorites');
$rootScope.$broadcast("reloadFavorites");
callback();
});
}
render() {
const { item, onChange } = this.props;
const icon = item.is_favorite ? 'fa fa-star' : 'fa fa-star-o';
const title = item.is_favorite ? 'Remove from favorites' : 'Add to favorites';
const icon = item.is_favorite ? "fa fa-star" : "fa fa-star-o";
const title = item.is_favorite ? "Remove from favorites" : "Add to favorites";
return (
<a
title={title}
className="btn-favourite"
onClick={event => this.toggleItem(event, item, onChange)}
>
<a title={title} className="btn-favourite" onClick={event => this.toggleItem(event, item, onChange)}>
<i className={icon} aria-hidden="true" />
</a>
);
@@ -48,8 +44,8 @@ export class FavoritesControl extends React.Component {
}
export default function init(ngModule) {
ngModule.component('favoritesControlImpl', react2angular(FavoritesControl));
ngModule.component('favoritesControl', {
ngModule.component("favoritesControlImpl", react2angular(FavoritesControl));
ngModule.component("favoritesControl", {
template: `
<favorites-control-impl
ng-if="$ctrl.item"
@@ -59,13 +55,13 @@ export default function init(ngModule) {
></favorites-control-impl>
`,
bindings: {
item: '=',
item: "=",
},
controller($scope) {
// See comment for FavoritesControl.propTypes.forceUpdate
this.forceUpdateTag = 'force' + Date.now();
$scope.$on('reloadFavorites', () => {
this.forceUpdateTag = 'force' + Date.now();
this.forceUpdateTag = "force" + Date.now();
$scope.$on("reloadFavorites", () => {
this.forceUpdateTag = "force" + Date.now();
});
this.onChange = () => {

View File

@@ -1,22 +1,19 @@
import { isArray, indexOf, get, map, includes, every, some, toNumber } from 'lodash';
import moment from 'moment';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Select from 'antd/lib/select';
import { formatColumnValue } from '@/filters';
import { isArray, indexOf, get, map, includes, every, some, toNumber } from "lodash";
import moment from "moment";
import React from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
import Select from "antd/lib/select";
import { formatColumnValue } from "@/filters";
const ALL_VALUES = '###Redash::Filters::SelectAll###';
const NONE_VALUES = '###Redash::Filters::Clear###';
const ALL_VALUES = "###Redash::Filters::SelectAll###";
const NONE_VALUES = "###Redash::Filters::Clear###";
export const FilterType = PropTypes.shape({
name: PropTypes.string.isRequired,
friendlyName: PropTypes.string.isRequired,
multiple: PropTypes.bool,
current: PropTypes.oneOfType([
PropTypes.any,
PropTypes.arrayOf(PropTypes.any),
]),
current: PropTypes.oneOfType([PropTypes.any, PropTypes.arrayOf(PropTypes.any)]),
values: PropTypes.arrayOf(PropTypes.any).isRequired,
});
@@ -49,23 +46,22 @@ export function filterData(rows, filters = []) {
let result = rows;
if (isArray(filters) && (filters.length > 0)) {
if (isArray(filters) && filters.length > 0) {
// "every" field's value should match "some" of corresponding filter's values
result = result.filter(row => every(
filters,
(filter) => {
result = result.filter(row =>
every(filters, filter => {
const rowValue = row[filter.name];
const filterValues = isArray(filter.current) ? filter.current : [filter.current];
return some(filterValues, (filterValue) => {
return some(filterValues, filterValue => {
if (moment.isMoment(rowValue)) {
return rowValue.isSame(filterValue);
}
// We compare with either the value or the String representation of the value,
// because Select2 casts true/false to "true"/"false".
return (filterValue === rowValue) || (String(rowValue) === filterValue);
return filterValue === rowValue || String(rowValue) === filterValue;
});
},
));
})
);
}
return result;
@@ -82,36 +78,45 @@ export function Filters({ filters, onChange }) {
<div className="filters-wrapper">
<div className="container bg-white">
<div className="row">
{map(filters, (filter) => {
{map(filters, filter => {
const options = map(filter.values, (value, index) => (
<Select.Option key={index}>{formatColumnValue(value, get(filter, 'column.type'))}</Select.Option>
<Select.Option key={index}>{formatColumnValue(value, get(filter, "column.type"))}</Select.Option>
));
return (
<div key={filter.name} className="col-sm-6 p-l-0 filter-container">
<label>{filter.friendlyName}</label>
{(options.length === 0) && (
<Select className="w-100" disabled value="No values" />
)}
{(options.length > 0) && (
{options.length === 0 && <Select className="w-100" disabled value="No values" />}
{options.length > 0 && (
<Select
labelInValue
className="w-100"
mode={filter.multiple ? 'multiple' : 'default'}
value={isArray(filter.current) ?
map(filter.current,
value => ({ key: `${indexOf(filter.values, value)}`, label: formatColumnValue(value) })) :
({ key: `${indexOf(filter.values, filter.current)}`, label: formatColumnValue(filter.current) })}
mode={filter.multiple ? "multiple" : "default"}
value={
isArray(filter.current)
? map(filter.current, value => ({
key: `${indexOf(filter.values, value)}`,
label: formatColumnValue(value),
}))
: { key: `${indexOf(filter.values, filter.current)}`, label: formatColumnValue(filter.current) }
}
allowClear={filter.multiple}
optionFilterProp="children"
showSearch
onChange={values => onChange(filter, values)}
>
onChange={values => onChange(filter, values)}>
{!filter.multiple && options}
{filter.multiple && [
<Select.Option key={NONE_VALUES}><i className="fa fa-square-o m-r-5" />Clear</Select.Option>,
<Select.Option key={ALL_VALUES}><i className="fa fa-check-square-o m-r-5" />Select All</Select.Option>,
<Select.OptGroup key="Values" title="Values">{options}</Select.OptGroup>,
<Select.Option key={NONE_VALUES}>
<i className="fa fa-square-o m-r-5" />
Clear
</Select.Option>,
<Select.Option key={ALL_VALUES}>
<i className="fa fa-check-square-o m-r-5" />
Select All
</Select.Option>,
<Select.OptGroup key="Values" title="Values">
{options}
</Select.OptGroup>,
]}
</Select>
)}
@@ -134,7 +139,7 @@ Filters.defaultProps = {
};
export default function init(ngModule) {
ngModule.component('filters', react2angular(Filters));
ngModule.component("filters", react2angular(Filters));
}
init.init = true;

View File

@@ -1,97 +1,43 @@
import { startsWith } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Tooltip from 'antd/lib/tooltip';
import Drawer from 'antd/lib/drawer';
import Icon from 'antd/lib/icon';
import { BigMessage } from '@/components/BigMessage';
import DynamicComponent from '@/components/DynamicComponent';
import { startsWith } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Tooltip from "antd/lib/tooltip";
import Drawer from "antd/lib/drawer";
import Icon from "antd/lib/icon";
import { BigMessage } from "@/components/BigMessage";
import DynamicComponent from "@/components/DynamicComponent";
import './HelpTrigger.less';
import "./HelpTrigger.less";
const DOMAIN = 'https://redash.io';
const HELP_PATH = '/help';
const DOMAIN = "https://redash.io";
const HELP_PATH = "/help";
const IFRAME_TIMEOUT = 20000;
const IFRAME_URL_UPDATE_MESSAGE = 'iframe_url';
const IFRAME_URL_UPDATE_MESSAGE = "iframe_url";
export const TYPES = {
HOME: [
'',
'Help',
],
VALUE_SOURCE_OPTIONS: [
'/user-guide/querying/query-parameters#Value-Source-Options',
'Guide: Value Source Options',
],
SHARE_DASHBOARD: [
'/user-guide/dashboards/sharing-dashboards',
'Guide: Sharing and Embedding Dashboards',
],
AUTHENTICATION_OPTIONS: [
'/user-guide/users/authentication-options',
'Guide: Authentication Options',
],
USAGE_DATA_SHARING: [
'/open-source/admin-guide/usage-data',
'Help: Anonymous Usage Data Sharing',
],
DS_ATHENA: [
'/data-sources/amazon-athena-setup',
'Guide: Help Setting up Amazon Athena',
],
DS_BIGQUERY: [
'/data-sources/bigquery-setup',
'Guide: Help Setting up BigQuery',
],
DS_URL: [
'/data-sources/querying-urls',
'Guide: Help Setting up URL',
],
DS_MONGODB: [
'/data-sources/mongodb-setup',
'Guide: Help Setting up MongoDB',
],
DS_GOOGLE_SPREADSHEETS: [
'/data-sources/querying-a-google-spreadsheet',
'Guide: Help Setting up Google Spreadsheets',
],
DS_GOOGLE_ANALYTICS: [
'/data-sources/google-analytics-setup',
'Guide: Help Setting up Google Analytics',
],
DS_AXIBASETSD: [
'/data-sources/axibase-time-series-database',
'Guide: Help Setting up Axibase Time Series',
],
DS_RESULTS: [
'/user-guide/querying/query-results-data-source',
'Guide: Help Setting up Query Results',
],
ALERT_SETUP: [
'/user-guide/alerts/setting-up-an-alert',
'Guide: Setting Up a New Alert',
],
MAIL_CONFIG: [
'/open-source/setup/#Mail-Configuration',
'Guide: Mail Configuration',
],
ALERT_NOTIF_TEMPLATE_GUIDE: [
'/user-guide/alerts/custom-alert-notifications',
'Guide: Custom Alerts Notifications',
],
FAVORITES: [
'/user-guide/querying/favorites-tagging/#Favorites',
'Guide: Favorites',
],
HOME: ["", "Help"],
VALUE_SOURCE_OPTIONS: ["/user-guide/querying/query-parameters#Value-Source-Options", "Guide: Value Source Options"],
SHARE_DASHBOARD: ["/user-guide/dashboards/sharing-dashboards", "Guide: Sharing and Embedding Dashboards"],
AUTHENTICATION_OPTIONS: ["/user-guide/users/authentication-options", "Guide: Authentication Options"],
USAGE_DATA_SHARING: ["/open-source/admin-guide/usage-data", "Help: Anonymous Usage Data Sharing"],
DS_ATHENA: ["/data-sources/amazon-athena-setup", "Guide: Help Setting up Amazon Athena"],
DS_BIGQUERY: ["/data-sources/bigquery-setup", "Guide: Help Setting up BigQuery"],
DS_URL: ["/data-sources/querying-urls", "Guide: Help Setting up URL"],
DS_MONGODB: ["/data-sources/mongodb-setup", "Guide: Help Setting up MongoDB"],
DS_GOOGLE_SPREADSHEETS: ["/data-sources/querying-a-google-spreadsheet", "Guide: Help Setting up Google Spreadsheets"],
DS_GOOGLE_ANALYTICS: ["/data-sources/google-analytics-setup", "Guide: Help Setting up Google Analytics"],
DS_AXIBASETSD: ["/data-sources/axibase-time-series-database", "Guide: Help Setting up Axibase Time Series"],
DS_RESULTS: ["/user-guide/querying/query-results-data-source", "Guide: Help Setting up Query Results"],
ALERT_SETUP: ["/user-guide/alerts/setting-up-an-alert", "Guide: Setting Up a New Alert"],
MAIL_CONFIG: ["/open-source/setup/#Mail-Configuration", "Guide: Mail Configuration"],
ALERT_NOTIF_TEMPLATE_GUIDE: ["/user-guide/alerts/custom-alert-notifications", "Guide: Custom Alerts Notifications"],
FAVORITES: ["/user-guide/querying/favorites-tagging/#Favorites", "Guide: Favorites"],
MANAGE_PERMISSIONS: [
'/user-guide/querying/writing-queries#Managing-Query-Permissions',
'Guide: Managing Query Permissions',
],
NUMBER_FORMAT_SPECS: [
'/user-guide/visualizations/formatting-numbers',
'Formatting Numbers',
"/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 {
@@ -120,15 +66,15 @@ export default class HelpTrigger extends React.Component {
};
componentDidMount() {
window.addEventListener('message', this.onPostMessageReceived, false);
window.addEventListener("message", this.onPostMessageReceived, false);
}
componentWillUnmount() {
window.removeEventListener('message', this.onPostMessageReceived);
window.removeEventListener("message", this.onPostMessageReceived);
clearTimeout(this.iframeLoadingTimeout);
}
loadIframe = (url) => {
loadIframe = url => {
clearTimeout(this.iframeLoadingTimeout);
this.setState({ loading: true, error: false });
@@ -143,7 +89,7 @@ export default class HelpTrigger extends React.Component {
clearTimeout(this.iframeLoadingTimeout);
};
onPostMessageReceived = (event) => {
onPostMessageReceived = event => {
if (!startsWith(event.origin, DOMAIN)) {
return;
}
@@ -165,7 +111,7 @@ export default class HelpTrigger extends React.Component {
setTimeout(() => this.loadIframe(url), 300);
};
closeDrawer = (event) => {
closeDrawer = event => {
if (event) {
event.preventDefault();
}
@@ -175,7 +121,7 @@ export default class HelpTrigger extends React.Component {
render() {
const [, tooltip] = TYPES[this.props.type];
const className = cx('help-trigger', this.props.className);
const className = cx("help-trigger", this.props.className);
const url = this.state.currentUrl;
return (
@@ -192,8 +138,7 @@ export default class HelpTrigger extends React.Component {
visible={this.state.visible}
className="help-drawer"
destroyOnClose
width={400}
>
width={400}>
<div className="drawer-wrapper">
<div className="drawer-menu">
{url && (
@@ -230,20 +175,19 @@ export default class HelpTrigger extends React.Component {
{/* error message */}
{this.state.error && (
<BigMessage icon="fa-exclamation-circle" className="help-message">
Something went wrong.<br />
Something went wrong.
<br />
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a href={this.state.error} target="_blank" rel="noopener">Click here</a>{' '}
<a href={this.state.error} target="_blank" rel="noopener">
Click here
</a>{" "}
to open the page in a new window.
</BigMessage>
)}
</div>
{/* extra content */}
<DynamicComponent
name="HelpDrawerExtraContent"
onLeave={this.closeDrawer}
openPageUrl={this.loadIframe}
/>
<DynamicComponent name="HelpDrawerExtraContent" onLeave={this.closeDrawer} openPageUrl={this.loadIframe} />
</Drawer>
</React.Fragment>
);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { $sanitize } from '@/services/ng';
import React from "react";
import PropTypes from "prop-types";
import { $sanitize } from "@/services/ng";
export default function HtmlContent({ children, ...props }) {
return (
@@ -16,5 +16,5 @@ HtmlContent.propTypes = {
};
HtmlContent.defaultProps = {
children: '',
children: "",
};

View File

@@ -1,14 +1,14 @@
import React from 'react';
import Input from 'antd/lib/input';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import React from "react";
import Input from "antd/lib/input";
import Icon from "antd/lib/icon";
import Tooltip from "antd/lib/tooltip";
export default class InputWithCopy extends React.Component {
constructor(props) {
super(props);
this.state = { copied: null };
this.ref = React.createRef();
this.copyFeatureSupported = document.queryCommandSupported('copy');
this.copyFeatureSupported = document.queryCommandSupported("copy");
this.resetCopyState = null;
}
@@ -24,14 +24,14 @@ export default class InputWithCopy extends React.Component {
// copy
try {
const success = document.execCommand('copy');
const success = document.execCommand("copy");
if (!success) {
throw new Error();
}
this.setState({ copied: 'Copied!' });
this.setState({ copied: "Copied!" });
} catch (err) {
this.setState({
copied: 'Copy failed',
copied: "Copy failed",
});
}
@@ -41,17 +41,11 @@ export default class InputWithCopy extends React.Component {
render() {
const copyButton = (
<Tooltip title={this.state.copied || 'Copy'}>
<Icon
type="copy"
style={{ cursor: 'pointer' }}
onClick={this.copy}
/>
<Tooltip title={this.state.copied || "Copy"}>
<Icon type="copy" style={{ cursor: "pointer" }} onClick={this.copy} />
</Tooltip>
);
return (
<Input {...this.props} ref={this.ref} addonAfter={this.copyFeatureSupported && copyButton} />
);
return <Input {...this.props} ref={this.ref} addonAfter={this.copyFeatureSupported && copyButton} />;
}
}

View File

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

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import React from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
export function PageHeader({ title }) {
return (
@@ -17,7 +17,7 @@ PageHeader.propTypes = {
};
export default function init(ngModule) {
ngModule.component('pageHeader', react2angular(PageHeader));
ngModule.component("pageHeader", react2angular(PageHeader));
}
init.init = true;

View File

@@ -1,25 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Pagination from 'antd/lib/pagination';
import React from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
import Pagination from "antd/lib/pagination";
export function Paginator({
page,
itemsPerPage,
totalCount,
onChange,
}) {
export function Paginator({ page, itemsPerPage, totalCount, onChange }) {
if (totalCount <= itemsPerPage) {
return null;
}
return (
<div className="paginator-container">
<Pagination
defaultCurrent={page}
defaultPageSize={itemsPerPage}
total={totalCount}
onChange={onChange}
/>
<Pagination defaultCurrent={page} defaultPageSize={itemsPerPage} total={totalCount} onChange={onChange} />
</div>
);
}
@@ -36,8 +26,8 @@ Paginator.defaultProps = {
};
export default function init(ngModule) {
ngModule.component('paginatorImpl', react2angular(Paginator));
ngModule.component('paginator', {
ngModule.component("paginatorImpl", react2angular(Paginator));
ngModule.component("paginator", {
template: `
<paginator-impl
page="$ctrl.paginator.page"
@@ -46,10 +36,10 @@ export default function init(ngModule) {
on-change="$ctrl.onPageChanged"
></paginator-impl>`,
bindings: {
paginator: '<',
paginator: "<",
},
controller($scope) {
this.onPageChanged = (page) => {
this.onPageChanged = page => {
this.paginator.setPage(page);
$scope.$applyAsync();
};

View File

@@ -1,13 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Badge from 'antd/lib/badge';
import Tooltip from 'antd/lib/tooltip';
import { KeyboardShortcuts } from '@/services/keyboard-shortcuts';
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Badge from "antd/lib/badge";
import Tooltip from "antd/lib/tooltip";
import { KeyboardShortcuts } from "@/services/keyboard-shortcuts";
function ParameterApplyButton({ paramCount, onClick }) {
// show spinner when count is empty so the fade out is consistent
const icon = !paramCount ? 'spinner fa-pulse' : 'check';
const icon = !paramCount ? "spinner fa-pulse" : "check";
return (
<div className="parameter-apply-button" data-show={!!paramCount} data-test="ParameterApplyButton">

View File

@@ -1,38 +1,37 @@
/* eslint-disable react/no-multi-comp */
import { isString, extend, each, has, map, includes, findIndex, find,
fromPairs, clone, isEmpty } from 'lodash';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Select from 'antd/lib/select';
import Table from 'antd/lib/table';
import Popover from 'antd/lib/popover';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import Tag from 'antd/lib/tag';
import Input from 'antd/lib/input';
import Radio from 'antd/lib/radio';
import Form from 'antd/lib/form';
import Tooltip from 'antd/lib/tooltip';
import ParameterValueInput from '@/components/ParameterValueInput';
import { ParameterMappingType } from '@/services/widget';
import { Parameter } from '@/services/parameters';
import HelpTrigger from '@/components/HelpTrigger';
import { isString, extend, each, has, map, includes, findIndex, find, fromPairs, clone, isEmpty } from "lodash";
import React, { Fragment } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Select from "antd/lib/select";
import Table from "antd/lib/table";
import Popover from "antd/lib/popover";
import Button from "antd/lib/button";
import Icon from "antd/lib/icon";
import Tag from "antd/lib/tag";
import Input from "antd/lib/input";
import Radio from "antd/lib/radio";
import Form from "antd/lib/form";
import Tooltip from "antd/lib/tooltip";
import ParameterValueInput from "@/components/ParameterValueInput";
import { ParameterMappingType } from "@/services/widget";
import { Parameter } from "@/services/parameters";
import HelpTrigger from "@/components/HelpTrigger";
import './ParameterMappingInput.less';
import "./ParameterMappingInput.less";
const { Option } = Select;
export const MappingType = {
DashboardAddNew: 'dashboard-add-new',
DashboardMapToExisting: 'dashboard-map-to-existing',
WidgetLevel: 'widget-level',
StaticValue: 'static-value',
DashboardAddNew: "dashboard-add-new",
DashboardMapToExisting: "dashboard-map-to-existing",
WidgetLevel: "widget-level",
StaticValue: "static-value",
};
export function parameterMappingsToEditableMappings(mappings, parameters, existingParameterNames = []) {
return map(mappings, (mapping) => {
return map(mappings, mapping => {
const result = extend({}, mapping);
const alreadyExists = includes(existingParameterNames, mapping.mapTo);
result.param = find(parameters, p => p.name === mapping.name);
@@ -57,9 +56,11 @@ export function parameterMappingsToEditableMappings(mappings, parameters, existi
}
export function editableMappingsToParameterMappings(mappings) {
return fromPairs(map( // convert to map
return fromPairs(
map(
// convert to map
mappings,
(mapping) => {
mapping => {
const result = extend({}, mapping);
switch (mapping.type) {
case MappingType.DashboardAddNew:
@@ -84,22 +85,23 @@ export function editableMappingsToParameterMappings(mappings) {
}
delete result.param;
return [result.name, result];
},
));
}
)
);
}
export function synchronizeWidgetTitles(sourceMappings, widgets) {
const affectedWidgets = [];
each(sourceMappings, (sourceMapping) => {
each(sourceMappings, sourceMapping => {
if (sourceMapping.type === ParameterMappingType.DashboardLevel) {
each(widgets, (widget) => {
each(widgets, widget => {
const widgetMappings = widget.options.parameterMappings;
each(widgetMappings, (widgetMapping) => {
each(widgetMappings, widgetMapping => {
// check if mapped to the same dashboard-level parameter
if (
(widgetMapping.type === ParameterMappingType.DashboardLevel) &&
(widgetMapping.mapTo === sourceMapping.mapTo)
widgetMapping.type === ParameterMappingType.DashboardLevel &&
widgetMapping.mapTo === sourceMapping.mapTo
) {
// dirty check - update only when needed
if (widgetMapping.title !== sourceMapping.title) {
@@ -133,33 +135,32 @@ export class ParameterMappingInput extends React.Component {
formItemProps = {
labelCol: { span: 5 },
wrapperCol: { span: 16 },
className: 'form-item',
className: "form-item",
};
updateSourceType = (type) => {
let { mapping: { mapTo } } = this.props;
updateSourceType = type => {
let {
mapping: { mapTo },
} = this.props;
const { existingParamNames } = this.props;
// if mapped name doesn't already exists
// default to first select option
if (
type === MappingType.DashboardMapToExisting &&
!includes(existingParamNames, mapTo)
) {
if (type === MappingType.DashboardMapToExisting && !includes(existingParamNames, mapTo)) {
mapTo = existingParamNames[0];
}
this.updateParamMapping({ type, mapTo });
};
updateParamMapping = (update) => {
updateParamMapping = update => {
const { onChange, mapping } = this.props;
const newMapping = extend({}, mapping, update);
if (newMapping.value !== mapping.value) {
newMapping.param = newMapping.param.clone();
newMapping.param.setValue(newMapping.value);
}
if (has(update, 'type')) {
if (has(update, "type")) {
if (update.type === MappingType.StaticValue) {
newMapping.value = newMapping.param.value;
} else {
@@ -172,19 +173,12 @@ export class ParameterMappingInput extends React.Component {
renderMappingTypeSelector() {
const noExisting = isEmpty(this.props.existingParamNames);
return (
<Radio.Group
value={this.props.mapping.type}
onChange={e => this.updateSourceType(e.target.value)}
>
<Radio.Group value={this.props.mapping.type} onChange={e => this.updateSourceType(e.target.value)}>
<Radio className="radio" value={MappingType.DashboardAddNew} data-test="NewDashboardParameterOption">
New dashboard parameter
</Radio>
<Radio
className="radio"
value={MappingType.DashboardMapToExisting}
disabled={noExisting}
>
Existing dashboard parameter{' '}
<Radio className="radio" value={MappingType.DashboardMapToExisting} disabled={noExisting}>
Existing dashboard parameter{" "}
{noExisting ? (
<Tooltip title="There are no dashboard parameters corresponding to this data type">
<Icon type="question-circle" theme="filled" />
@@ -202,13 +196,10 @@ export class ParameterMappingInput extends React.Component {
}
renderDashboardAddNew() {
const { mapping: { mapTo } } = this.props;
return (
<Input
value={mapTo}
onChange={e => this.updateParamMapping({ mapTo: e.target.value })}
/>
);
const {
mapping: { mapTo },
} = this.props;
return <Input value={mapTo} onChange={e => this.updateParamMapping({ mapTo: e.target.value })} />;
}
renderDashboardMapToExisting() {
@@ -218,10 +209,11 @@ export class ParameterMappingInput extends React.Component {
<Select
value={mapping.mapTo}
onChange={mapTo => this.updateParamMapping({ mapTo })}
dropdownMatchSelectWidth={false}
>
dropdownMatchSelectWidth={false}>
{map(existingParamNames, name => (
<Option value={name} key={name}>{ name }</Option>
<Option value={name} key={name}>
{name}
</Option>
))}
</Select>
);
@@ -245,24 +237,13 @@ export class ParameterMappingInput extends React.Component {
const { mapping } = this.props;
switch (mapping.type) {
case MappingType.DashboardAddNew:
return [
'Key',
'Enter a new parameter keyword',
this.renderDashboardAddNew(),
];
return ["Key", "Enter a new parameter keyword", this.renderDashboardAddNew()];
case MappingType.DashboardMapToExisting:
return [
'Key',
'Select from a list of existing parameters',
this.renderDashboardMapToExisting(),
];
return ["Key", "Select from a list of existing parameters", this.renderDashboardMapToExisting()];
case MappingType.StaticValue:
return [
'Value',
null,
this.renderStaticValue(),
];
default: return [];
return ["Value", null, this.renderStaticValue()];
default:
return [];
}
}
@@ -276,10 +257,10 @@ export class ParameterMappingInput extends React.Component {
{this.renderMappingTypeSelector()}
</Form.Item>
<Form.Item
style={{ height: 60, visibility: input ? 'visible' : 'hidden' }}
style={{ height: 60, visibility: input ? "visible" : "hidden" }}
label={label}
{...this.formItemProps}
validateStatus={inputError ? 'error' : ''}
validateStatus={inputError ? "error" : ""}
help={inputError || help} // empty space so line doesn't collapse
>
{input}
@@ -305,18 +286,19 @@ class MappingEditor extends React.Component {
};
}
onVisibleChange = (visible) => {
if (visible) this.show(); else this.hide();
onVisibleChange = visible => {
if (visible) this.show();
else this.hide();
};
onChange = (mapping) => {
onChange = mapping => {
let inputError = null;
if (mapping.type === MappingType.DashboardAddNew) {
if (isEmpty(mapping.mapTo)) {
inputError = 'Keyword must have a value';
inputError = "Keyword must have a value";
} else if (includes(this.props.existingParamNames, mapping.mapTo)) {
inputError = 'A parameter with this name already exists';
inputError = "A parameter with this name already exists";
}
}
@@ -355,7 +337,9 @@ class MappingEditor extends React.Component {
/>
<footer>
<Button onClick={this.hide}>Cancel</Button>
<Button onClick={this.save} disabled={!!inputError} type="primary">OK</Button>
<Button onClick={this.save} disabled={!!inputError} type="primary">
OK
</Button>
</footer>
</div>
);
@@ -369,8 +353,7 @@ class MappingEditor extends React.Component {
trigger="click"
content={this.renderContent()}
visible={visible}
onVisibleChange={this.onVisibleChange}
>
onVisibleChange={this.onVisibleChange}>
<Button size="small" type="dashed" data-test={`EditParamMappingButon-${mapping.param.name}`}>
<Icon type="edit" />
</Button>
@@ -392,24 +375,24 @@ class TitleEditor extends React.Component {
state = {
showPopup: false,
title: '', // will be set on editing
title: "", // will be set on editing
};
onPopupVisibleChange = (showPopup) => {
onPopupVisibleChange = showPopup => {
this.setState({
showPopup,
title: showPopup ? this.getMappingTitle() : '',
title: showPopup ? this.getMappingTitle() : "",
});
};
onEditingTitleChange = (event) => {
onEditingTitleChange = event => {
this.setState({ title: event.target.value });
};
getMappingTitle() {
let { mapping } = this.props;
if (isString(mapping.title) && (mapping.title !== '')) {
if (isString(mapping.title) && mapping.title !== "") {
return mapping.title;
}
@@ -435,7 +418,9 @@ class TitleEditor extends React.Component {
};
renderPopover() {
const { param: { title: paramTitle } } = this.props.mapping;
const {
param: { title: paramTitle },
} = this.props.mapping;
return (
<div className="parameter-mapping-title-editor">
@@ -473,8 +458,7 @@ class TitleEditor extends React.Component {
trigger="click"
content={this.renderPopover()}
visible={this.state.showPopup}
onVisibleChange={this.onPopupVisibleChange}
>
onVisibleChange={this.onPopupVisibleChange}>
<Button size="small" type="dashed">
<Icon type="edit" />
</Button>
@@ -488,7 +472,7 @@ class TitleEditor extends React.Component {
const disabled = mapping.type === MappingType.StaticValue;
return (
<div className={classNames('parameter-mapping-title', { disabled })}>
<div className={classNames("parameter-mapping-title", { disabled })}>
<span className="text">{this.getMappingTitle()}</span>
{this.renderEditButton()}
</div>
@@ -512,17 +496,17 @@ export class ParameterMappingListInput extends React.Component {
static getStringValue(value) {
// null
if (!value) {
return '';
return "";
}
// range
if (value instanceof Object && 'start' in value && 'end' in value) {
if (value instanceof Object && "start" in value && "end" in value) {
return `${value.start} ~ ${value.end}`;
}
// just to be safe, array or object
if (typeof value === 'object') {
return map(value, v => this.getStringValue(v)).join(', ');
if (typeof value === "object") {
return map(value, v => this.getStringValue(v)).join(", ");
}
// rest
@@ -536,7 +520,8 @@ export class ParameterMappingListInput extends React.Component {
// if mapped to another param, swap 'em
if (type === MappingType.DashboardMapToExisting && mapTo !== name) {
const mappedTo = find(existingParams, { name: mapTo });
if (mappedTo) { // just being safe
if (mappedTo) {
// just being safe
param = mappedTo;
}
@@ -561,16 +546,15 @@ export class ParameterMappingListInput extends React.Component {
case MappingType.DashboardMapToExisting:
return (
<Fragment>
Dashboard{' '}
<Tag className="tag">{mapTo}</Tag>
Dashboard <Tag className="tag">{mapTo}</Tag>
</Fragment>
);
case MappingType.WidgetLevel:
return 'Widget parameter';
return "Widget parameter";
case MappingType.StaticValue:
return 'Static value';
return "Static value";
default:
return ''; // won't happen (typescript-ftw)
return ""; // won't happen (typescript-ftw)
}
}
@@ -592,12 +576,7 @@ export class ParameterMappingListInput extends React.Component {
return (
<div className="parameters-mapping-list">
<Table
dataSource={dataSource}
size="middle"
pagination={false}
rowKey={(record, idx) => `row${idx}`}
>
<Table dataSource={dataSource} size="middle" pagination={false} rowKey={(record, idx) => `row${idx}`}>
<Table.Column
title="Title"
dataIndex="mapping"
@@ -621,22 +600,20 @@ export class ParameterMappingListInput extends React.Component {
title="Default Value"
dataIndex="mapping"
key="value"
render={mapping => (
this.constructor.getDefaultValue(mapping, this.props.existingParams)
)}
render={mapping => this.constructor.getDefaultValue(mapping, this.props.existingParams)}
/>
<Table.Column
title="Value Source"
dataIndex="mapping"
key="source"
render={(mapping) => {
render={mapping => {
const existingParamsNames = existingParams
.filter(({ type }) => type === mapping.param.type) // exclude mismatching param types
.map(({ name }) => name); // keep names only
return (
<Fragment>
{this.constructor.getSourceTypeLabel(mapping)}{' '}
{this.constructor.getSourceTypeLabel(mapping)}{" "}
<MappingEditor
mapping={mapping}
existingParamNames={existingParamsNames}

View File

@@ -1,14 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'antd/lib/select';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
import DateParameter from '@/components/dynamic-parameters/DateParameter';
import DateRangeParameter from '@/components/dynamic-parameters/DateRangeParameter';
import { isEqual } from 'lodash';
import { QueryBasedParameterInput } from './QueryBasedParameterInput';
import React from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
import Input from "antd/lib/input";
import InputNumber from "antd/lib/input-number";
import DateParameter from "@/components/dynamic-parameters/DateParameter";
import DateRangeParameter from "@/components/dynamic-parameters/DateRangeParameter";
import { isEqual } from "lodash";
import { QueryBasedParameterInput } from "./QueryBasedParameterInput";
import './ParameterValueInput.less';
import "./ParameterValueInput.less";
const { Option } = Select;
@@ -30,13 +30,13 @@ class ParameterValueInput extends React.Component {
};
static defaultProps = {
type: 'text',
type: "text",
value: null,
enumOptions: '',
enumOptions: "",
queryId: null,
parameter: null,
onSelect: () => {},
className: '',
className: "",
};
constructor(props) {
@@ -47,7 +47,7 @@ class ParameterValueInput extends React.Component {
};
}
componentDidUpdate = (prevProps) => {
componentDidUpdate = prevProps => {
const { value, parameter } = this.props;
// if value prop updated, reset dirty state
if (prevProps.value !== value || prevProps.parameter !== parameter) {
@@ -56,13 +56,13 @@ class ParameterValueInput extends React.Component {
isDirty: parameter.hasPendingValue,
});
}
}
};
onSelect = (value) => {
onSelect = value => {
const isDirty = !isEqual(value, this.props.value);
this.setState({ value, isDirty });
this.props.onSelect(value, isDirty);
}
};
renderDateParameter() {
const { type, parameter } = this.props;
@@ -95,13 +95,13 @@ class ParameterValueInput extends React.Component {
renderEnumInput() {
const { enumOptions, parameter } = this.props;
const { value } = this.state;
const enumOptionsArray = enumOptions.split('\n').filter(v => v !== '');
const enumOptionsArray = enumOptions.split("\n").filter(v => v !== "");
// Antd Select doesn't handle null in multiple mode
const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val);
return (
<Select
className={this.props.className}
mode={parameter.multiValuesOptions ? 'multiple' : 'default'}
mode={parameter.multiValuesOptions ? "multiple" : "default"}
optionFilterProp="children"
disabled={enumOptionsArray.length === 0}
value={normalize(value)}
@@ -111,9 +111,12 @@ class ParameterValueInput extends React.Component {
showArrow
style={{ minWidth: 60 }}
notFoundContent={null}
{...multipleValuesProps}
>
{enumOptionsArray.map(option => (<Option key={option} value={option}>{ option }</Option>))}
{...multipleValuesProps}>
{enumOptionsArray.map(option => (
<Option key={option} value={option}>
{option}
</Option>
))}
</Select>
);
}
@@ -124,7 +127,7 @@ class ParameterValueInput extends React.Component {
return (
<QueryBasedParameterInput
className={this.props.className}
mode={parameter.multiValuesOptions ? 'multiple' : 'default'}
mode={parameter.multiValuesOptions ? "multiple" : "default"}
optionFilterProp="children"
parameter={parameter}
value={value}
@@ -143,11 +146,7 @@ class ParameterValueInput extends React.Component {
const normalize = val => (isNaN(val) ? undefined : val);
return (
<InputNumber
className={className}
value={normalize(value)}
onChange={val => this.onSelect(normalize(val))}
/>
<InputNumber className={className} value={normalize(value)} onChange={val => this.onSelect(normalize(val))} />
);
}
@@ -168,16 +167,22 @@ class ParameterValueInput extends React.Component {
renderInput() {
const { type } = this.props;
switch (type) {
case 'datetime-with-seconds':
case 'datetime-local':
case 'date': return this.renderDateParameter();
case 'datetime-range-with-seconds':
case 'datetime-range':
case 'date-range': return this.renderDateRangeParameter();
case 'enum': return this.renderEnumInput();
case 'query': return this.renderQueryBasedInput();
case 'number': return this.renderNumberInput();
default: return this.renderTextInput();
case "datetime-with-seconds":
case "datetime-local":
case "date":
return this.renderDateParameter();
case "datetime-range-with-seconds":
case "datetime-range":
case "date-range":
return this.renderDateRangeParameter();
case "enum":
return this.renderEnumInput();
case "query":
return this.renderQueryBasedInput();
case "number":
return this.renderNumberInput();
default:
return this.renderTextInput();
}
}

View File

@@ -1,20 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { size, filter, forEach, extend } from 'lodash';
import { react2angular } from 'react2angular';
import { SortableContainer, SortableElement, DragHandle } from '@/components/sortable';
import { $location } from '@/services/ng';
import { Parameter } from '@/services/parameters';
import ParameterApplyButton from '@/components/ParameterApplyButton';
import ParameterValueInput from '@/components/ParameterValueInput';
import EditParameterSettingsDialog from './EditParameterSettingsDialog';
import { toHuman } from '@/filters';
import React from "react";
import PropTypes from "prop-types";
import { size, filter, forEach, extend } from "lodash";
import { react2angular } from "react2angular";
import { SortableContainer, SortableElement, DragHandle } from "@/components/sortable";
import { $location } from "@/services/ng";
import { Parameter } from "@/services/parameters";
import ParameterApplyButton from "@/components/ParameterApplyButton";
import ParameterValueInput from "@/components/ParameterValueInput";
import EditParameterSettingsDialog from "./EditParameterSettingsDialog";
import { toHuman } from "@/filters";
import './Parameters.less';
import "./Parameters.less";
function updateUrl(parameters) {
const params = extend({}, $location.search());
parameters.forEach((param) => {
parameters.forEach(param => {
extend(params, param.toUrlParams());
});
Object.keys(params).forEach(key => params[key] == null && delete params[key]);
@@ -49,7 +49,7 @@ export class Parameters extends React.Component {
}
}
componentDidUpdate = (prevProps) => {
componentDidUpdate = prevProps => {
const { parameters, disableUrlUpdate } = this.props;
if (prevProps.parameters !== parameters) {
this.setState({ parameters });
@@ -59,7 +59,7 @@ export class Parameters extends React.Component {
}
};
handleKeyDown = (e) => {
handleKeyDown = e => {
// Cmd/Ctrl/Alt + Enter
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) {
e.stopPropagation();
@@ -106,9 +106,7 @@ export class Parameters extends React.Component {
showParameterSettings = (parameter, index) => {
const { onParametersEdit } = this.props;
EditParameterSettingsDialog
.showModal({ parameter })
.result.then((updated) => {
EditParameterSettingsDialog.showModal({ parameter }).result.then(updated => {
this.setState(({ parameters }) => {
const updatedParameter = extend(parameter, updated);
parameters[index] = Parameter.create(updatedParameter, updatedParameter.parentQueryId);
@@ -121,11 +119,7 @@ export class Parameters extends React.Component {
renderParameter(param, index) {
const { editable } = this.props;
return (
<div
key={param.name}
className="di-block"
data-test={`ParameterName-${param.name}`}
>
<div key={param.name} className="di-block" data-test={`ParameterName-${param.name}`}>
<div className="parameter-heading">
<label>{param.title || toHuman(param.name)}</label>
{editable && (
@@ -133,8 +127,7 @@ export class Parameters extends React.Component {
className="btn btn-default btn-xs m-l-5"
onClick={() => this.showParameterSettings(param, index)}
data-test={`ParameterSettings-${param.name}`}
type="button"
>
type="button">
<i className="fa fa-cog" />
</button>
)}
@@ -154,7 +147,7 @@ export class Parameters extends React.Component {
render() {
const { parameters } = this.state;
const { editable } = this.props;
const dirtyParamCount = size(filter(parameters, 'hasPendingValue'));
const dirtyParamCount = size(filter(parameters, "hasPendingValue"));
return (
<SortableContainer
disabled={!editable}
@@ -165,10 +158,9 @@ export class Parameters extends React.Component {
updateBeforeSortStart={this.onBeforeSortStart}
onSortEnd={this.moveParameter}
containerProps={{
className: 'parameter-container',
className: "parameter-container",
onKeyDown: dirtyParamCount ? this.handleKeyDown : null,
}}
>
}}>
{parameters.map((param, index) => (
<SortableElement key={param.name} index={index}>
<div className="parameter-block" data-editable={editable || null}>
@@ -184,7 +176,7 @@ export class Parameters extends React.Component {
}
export default function init(ngModule) {
ngModule.component('parameters', react2angular(Parameters));
ngModule.component("parameters", react2angular(Parameters));
}
init.init = true;

View File

@@ -1,17 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
// PreviewCard
export function PreviewCard({ imageUrl, roundedImage, title, body, children, className, ...props }) {
return (
<div {...props} className={className + ' w-100 d-flex align-items-center'}>
<div {...props} className={className + " w-100 d-flex align-items-center"}>
<img
src={imageUrl}
width="32"
height="32"
className={classNames({ 'profile__image--settings': roundedImage }, 'm-r-5')}
className={classNames({ "profile__image--settings": roundedImage }, "m-r-5")}
alt="Logo/Avatar"
/>
<div className="flex-fill">
@@ -35,14 +35,14 @@ PreviewCard.propTypes = {
PreviewCard.defaultProps = {
body: null,
roundedImage: true,
className: '',
className: "",
children: null,
};
// UserPreviewCard
export function UserPreviewCard({ user, withLink, children, ...props }) {
const title = withLink ? <a href={'users/' + user.id}>{user.name}</a> : user.name;
const title = withLink ? <a href={"users/" + user.id}>{user.name}</a> : user.name;
return (
<PreviewCard {...props} imageUrl={user.profile_image_url} title={title} body={user.email}>
{children}
@@ -69,8 +69,12 @@ UserPreviewCard.defaultProps = {
export function DataSourcePreviewCard({ dataSource, withLink, children, ...props }) {
const imageUrl = `/static/images/db-logos/${dataSource.type}.png`;
const title = withLink ? <a href={'data_sources/' + dataSource.id}>{dataSource.name}</a> : dataSource.name;
return <PreviewCard {...props} imageUrl={imageUrl} title={title}>{children}</PreviewCard>;
const title = withLink ? <a href={"data_sources/" + dataSource.id}>{dataSource.name}</a> : dataSource.name;
return (
<PreviewCard {...props} imageUrl={imageUrl} title={title}>
{children}
</PreviewCard>
);
}
DataSourcePreviewCard.propTypes = {

View File

@@ -1,8 +1,8 @@
import { find, isArray, map, intersection, isEqual } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Select from 'antd/lib/select';
import { find, isArray, map, intersection, isEqual } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
import Select from "antd/lib/select";
const { Option } = Select;
@@ -10,7 +10,7 @@ export 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']),
mode: PropTypes.oneOf(["default", "multiple"]),
queryId: PropTypes.number,
onSelect: PropTypes.func,
className: PropTypes.string,
@@ -18,11 +18,11 @@ export class QueryBasedParameterInput extends React.Component {
static defaultProps = {
value: null,
mode: 'default',
mode: "default",
parameter: null,
queryId: null,
onSelect: () => {},
className: '',
className: "",
};
constructor(props) {
@@ -49,7 +49,7 @@ export class QueryBasedParameterInput extends React.Component {
setValue(value) {
const { options } = this.state;
if (this.props.mode === 'multiple') {
if (this.props.mode === "multiple") {
value = isArray(value) ? value : [value];
const optionValues = map(options, option => option.value);
const validValues = intersection(value, optionValues);
@@ -63,7 +63,7 @@ export class QueryBasedParameterInput extends React.Component {
}
async _loadOptions(queryId) {
if (queryId && (queryId !== this.state.queryId)) {
if (queryId && queryId !== this.state.queryId) {
this.setState({ loading: true });
const options = await this.props.parameter.loadDropdownValues();
@@ -86,7 +86,7 @@ export class QueryBasedParameterInput extends React.Component {
<span>
<Select
className={className}
disabled={loading || (options.length === 0)}
disabled={loading || options.length === 0}
loading={loading}
mode={mode}
value={this.state.value}
@@ -96,9 +96,12 @@ export class QueryBasedParameterInput extends React.Component {
showSearch
showArrow
notFoundContent={null}
{...otherProps}
>
{options.map(option => (<Option value={option.value} key={option.value}>{option.name}</Option>))}
{...otherProps}>
{options.map(option => (
<Option value={option.value} key={option.value}>
{option.name}
</Option>
))}
</Select>
</span>
);
@@ -106,7 +109,7 @@ export class QueryBasedParameterInput extends React.Component {
}
export default function init(ngModule) {
ngModule.component('queryBasedParameterInput', react2angular(QueryBasedParameterInput));
ngModule.component("queryBasedParameterInput", react2angular(QueryBasedParameterInput));
}
init.init = true;

View File

@@ -1,47 +1,47 @@
import React from 'react';
import PropTypes from 'prop-types';
import Tooltip from 'antd/lib/tooltip';
import { react2angular } from 'react2angular';
import React from "react";
import PropTypes from "prop-types";
import Tooltip from "antd/lib/tooltip";
import { react2angular } from "react2angular";
import AceEditor from 'react-ace';
import ace from 'brace';
import notification from '@/services/notification';
import AceEditor from "react-ace";
import ace from "brace";
import notification from "@/services/notification";
import 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/mode/python';
import 'brace/mode/sql';
import 'brace/mode/yaml';
import 'brace/theme/textmate';
import 'brace/ext/searchbox';
import "brace/ext/language_tools";
import "brace/mode/json";
import "brace/mode/python";
import "brace/mode/sql";
import "brace/mode/yaml";
import "brace/theme/textmate";
import "brace/ext/searchbox";
import { Query } from '@/services/query';
import { QuerySnippet } from '@/services/query-snippet';
import { KeyboardShortcuts } from '@/services/keyboard-shortcuts';
import { Query } from "@/services/query";
import { QuerySnippet } from "@/services/query-snippet";
import { KeyboardShortcuts } from "@/services/keyboard-shortcuts";
import localOptions from '@/lib/localOptions';
import AutocompleteToggle from '@/components/AutocompleteToggle';
import keywordBuilder from './keywordBuilder';
import { DataSource, Schema } from './proptypes';
import localOptions from "@/lib/localOptions";
import AutocompleteToggle from "@/components/AutocompleteToggle";
import keywordBuilder from "./keywordBuilder";
import { DataSource, Schema } from "./proptypes";
import './QueryEditor.css';
import "./QueryEditor.css";
const langTools = ace.acequire('ace/ext/language_tools');
const snippetsModule = ace.acequire('ace/snippets');
const langTools = ace.acequire("ace/ext/language_tools");
const snippetsModule = ace.acequire("ace/snippets");
// By default Ace will try to load snippet files for the different modes and fail.
// We don't need them, so we use these placeholders until we define our own.
function defineDummySnippets(mode) {
ace.define(`ace/snippets/${mode}`, ['require', 'exports', 'module'], (require, exports) => {
exports.snippetText = '';
ace.define(`ace/snippets/${mode}`, ["require", "exports", "module"], (require, exports) => {
exports.snippetText = "";
exports.scope = mode;
});
}
defineDummySnippets('python');
defineDummySnippets('sql');
defineDummySnippets('json');
defineDummySnippets('yaml');
defineDummySnippets("python");
defineDummySnippets("sql");
defineDummySnippets("json");
defineDummySnippets("yaml");
class QueryEditor extends React.Component {
static propTypes = {
@@ -82,7 +82,7 @@ class QueryEditor extends React.Component {
column: [],
tableColumn: [],
},
autocompleteQuery: localOptions.get('liveAutocomplete', true),
autocompleteQuery: localOptions.get("liveAutocomplete", true),
liveAutocompleteDisabled: false,
// XXX temporary while interfacing with angular
queryText: props.queryText,
@@ -101,7 +101,7 @@ class QueryEditor extends React.Component {
return;
}
if (prefix[prefix.length - 1] === '.') {
if (prefix[prefix.length - 1] === ".") {
const tableName = prefix.substring(0, prefix.length - 1);
callback(null, tableKeywords.concat(tableColumnKeywords[tableName]));
return;
@@ -139,33 +139,32 @@ class QueryEditor extends React.Component {
return null;
}
onLoad = (editor) => {
onLoad = editor => {
// Release Cmd/Ctrl+L to the browser
editor.commands.bindKey('Cmd+L', null);
editor.commands.bindKey('Ctrl+P', null);
editor.commands.bindKey('Ctrl+L', null);
editor.commands.bindKey("Cmd+L", null);
editor.commands.bindKey("Ctrl+P", null);
editor.commands.bindKey("Ctrl+L", null);
// Ignore Ctrl+P to open new parameter dialog
editor.commands.bindKey({ win: 'Ctrl+P', mac: null }, null);
editor.commands.bindKey({ win: "Ctrl+P", mac: null }, null);
// Lineup only mac
editor.commands.bindKey({ win: null, mac: 'Ctrl+P' }, 'golineup');
editor.commands.bindKey({ win: 'Ctrl+Shift+F', mac: 'Cmd+Shift+F' }, this.formatQuery);
editor.commands.bindKey({ win: null, mac: "Ctrl+P" }, "golineup");
editor.commands.bindKey({ win: "Ctrl+Shift+F", mac: "Cmd+Shift+F" }, this.formatQuery);
// Reset Completer in case dot is pressed
editor.commands.on('afterExec', (e) => {
if (e.command.name === 'insertstring' && e.args === '.'
&& editor.completer) {
editor.commands.on("afterExec", e => {
if (e.command.name === "insertstring" && e.args === "." && editor.completer) {
editor.completer.showPopup(editor);
}
});
QuerySnippet.query((snippets) => {
QuerySnippet.query(snippets => {
const snippetManager = snippetsModule.snippetManager;
const m = {
snippetText: '',
snippetText: "",
};
m.snippets = snippetManager.parseSnippetFile(m.snippetText);
snippets.forEach((snippet) => {
snippets.forEach(snippet => {
m.snippets.push(snippet.getSnippet());
});
snippetManager.register(m.snippets || [], m.scope);
@@ -175,11 +174,11 @@ class QueryEditor extends React.Component {
this.props.listenForResize(() => editor.resize());
this.props.listenForEditorCommand((e, command, ...args) => {
switch (command) {
case 'focus': {
case "focus": {
editor.focus();
break;
}
case 'paste': {
case "paste": {
const [text] = args;
editor.session.doc.replace(editor.selection.getRange(), text);
const range = editor.selection.getRange();
@@ -193,29 +192,29 @@ class QueryEditor extends React.Component {
});
};
updateSelectedQuery = (selection) => {
updateSelectedQuery = selection => {
const { editor } = this.refEditor.current;
const doc = editor.getSession().doc;
const rawSelectedQueryText = doc.getTextRange(selection.getRange());
const selectedQueryText = (rawSelectedQueryText.length > 1) ? rawSelectedQueryText : null;
const selectedQueryText = rawSelectedQueryText.length > 1 ? rawSelectedQueryText : null;
this.setState({ selectedQueryText });
this.props.updateSelectedQuery(selectedQueryText);
};
updateQuery = (queryText) => {
updateQuery = queryText => {
this.props.updateQuery(queryText);
this.setState({ queryText });
};
formatQuery = () => {
Query.format(this.props.dataSource.syntax || 'sql', this.props.queryText)
Query.format(this.props.dataSource.syntax || "sql", this.props.queryText)
.then(this.updateQuery)
.catch(error => notification.error(error));
};
toggleAutocomplete = (state) => {
toggleAutocomplete = state => {
this.setState({ autocompleteQuery: state });
localOptions.set('liveAutocomplete', state);
localOptions.set("liveAutocomplete", state);
};
componentDidUpdate = () => {
@@ -230,13 +229,16 @@ class QueryEditor extends React.Component {
const isExecuteDisabled = this.props.queryExecuting || !this.props.canExecuteQuery;
return (
<section style={{ height: '100%' }} data-test="QueryEditor">
<div className="container p-15 m-b-10" style={{ height: '100%' }}>
<div data-executing={this.props.queryExecuting} style={{ height: 'calc(100% - 40px)', marginBottom: '0px' }} className="editor__container">
<section style={{ height: "100%" }} data-test="QueryEditor">
<div className="container p-15 m-b-10" style={{ height: "100%" }}>
<div
data-executing={this.props.queryExecuting}
style={{ height: "calc(100% - 40px)", marginBottom: "0px" }}
className="editor__container">
<AceEditor
ref={this.refEditor}
theme="textmate"
mode={this.props.dataSource.syntax || 'sql'}
mode={this.props.dataSource.syntax || "sql"}
value={this.state.queryText}
editorProps={{ $blockScrolling: Infinity }}
width="100%"
@@ -261,13 +263,22 @@ class QueryEditor extends React.Component {
<div className="form-inline d-flex">
<Tooltip
placement="top"
title={<span>Add New Parameter (<i>{modKey} + P</i>)</span>}
>
title={
<span>
Add New Parameter (<i>{modKey} + P</i>)
</span>
}>
<button type="button" className="btn btn-default m-r-5" onClick={this.props.addNewParameter}>
&#123;&#123;&nbsp;&#125;&#125;
</button>
</Tooltip>
<Tooltip placement="top" title={<>Format Query (<i>{modKey} + Shift + F</i>)</>}>
<Tooltip
placement="top"
title={
<>
Format Query (<i>{modKey} + Shift + F</i>)
</>
}>
<button type="button" className="btn btn-default m-r-5" onClick={this.formatQuery}>
<span className="zmdi zmdi-format-indent-increase" />
</button>
@@ -280,8 +291,7 @@ class QueryEditor extends React.Component {
<select
className="form-control datasource-small flex-fill w-100"
onChange={this.props.updateDataSource}
disabled={!this.props.isQueryOwner}
>
disabled={!this.props.isQueryOwner}>
{this.props.dataSources.map(ds => (
<option label={ds.name} value={ds.id} key={`ds-option-${ds.id}`}>
{ds.name}
@@ -289,21 +299,20 @@ class QueryEditor extends React.Component {
))}
</select>
{this.props.canEdit ? (
<Tooltip placement="top" title={modKey + ' + S'}>
<Tooltip placement="top" title={modKey + " + S"}>
<button
type="button"
className="btn btn-default m-l-5"
onClick={this.props.saveQuery}
data-test="SaveButton"
title="Save"
>
title="Save">
<span className="fa fa-floppy-o" />
<span className="hidden-xs m-l-5">Save</span>
{this.props.isDirty ? '*' : null}
{this.props.isDirty ? "*" : null}
</button>
</Tooltip>
) : null}
<Tooltip placement="top" title={modKey + ' + Enter'}>
<Tooltip placement="top" title={modKey + " + Enter"}>
{/*
Tooltip wraps disabled buttons with `<span>` and moves all styles
and classes to that `<span>`. There is a piece of CSS that fixes
@@ -313,13 +322,14 @@ class QueryEditor extends React.Component {
*/}
<button
type="button"
className={'btn btn-primary m-l-5' + (isExecuteDisabled ? ' disabled' : '')}
className={"btn btn-primary m-l-5" + (isExecuteDisabled ? " disabled" : "")}
disabled={isExecuteDisabled}
onClick={this.props.executeQuery}
data-test="ExecuteButton"
>
data-test="ExecuteButton">
<span className="zmdi zmdi-play" />
<span className="hidden-xs m-l-5">{ (this.state.selectedQueryText == null) ? 'Execute' : 'Execute Selected' }</span>
<span className="hidden-xs m-l-5">
{this.state.selectedQueryText == null ? "Execute" : "Execute Selected"}
</span>
</button>
</Tooltip>
</div>
@@ -331,7 +341,7 @@ class QueryEditor extends React.Component {
}
export default function init(ngModule) {
ngModule.component('queryEditor', react2angular(QueryEditor));
ngModule.component("queryEditor", react2angular(QueryEditor));
}
init.init = true;

View File

@@ -1,15 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { VisualizationType } from '@/visualizations';
import { VisualizationName } from '@/visualizations/VisualizationName';
import React from "react";
import PropTypes from "prop-types";
import { VisualizationType } from "@/visualizations";
import { VisualizationName } from "@/visualizations/VisualizationName";
function QueryLink({ query, visualization, readOnly }) {
const getUrl = () => {
let hash = null;
if (visualization) {
if (visualization.type === 'TABLE') {
if (visualization.type === "TABLE") {
// link to hard-coded table tab instead of the (hidden) visualization tab
hash = 'table';
hash = "table";
} else {
hash = visualization.id;
}
@@ -20,8 +20,7 @@ function QueryLink({ query, visualization, readOnly }) {
return (
<a href={readOnly ? null : getUrl()} className="query-link">
<VisualizationName visualization={visualization} />{' '}
<span>{query.name}</span>
<VisualizationName visualization={visualization} /> <span>{query.name}</span>
</a>
);
}

View File

@@ -1,41 +1,41 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { react2angular } from 'react2angular';
import { find } from 'lodash';
import Input from 'antd/lib/input';
import Select from 'antd/lib/select';
import { Query } from '@/services/query';
import notification from '@/services/notification';
import { QueryTagsControl } from '@/components/tags-control/TagsControl';
import useSearchResults from '@/lib/hooks/useSearchResults';
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { react2angular } from "react2angular";
import { find } from "lodash";
import Input from "antd/lib/input";
import Select from "antd/lib/select";
import { Query } from "@/services/query";
import notification from "@/services/notification";
import { QueryTagsControl } from "@/components/tags-control/TagsControl";
import useSearchResults from "@/lib/hooks/useSearchResults";
const { Option } = Select;
function search(term) {
// get recent
if (!term) {
return Query.recent().$promise
.then((results) => {
return Query.recent().$promise.then(results => {
const filteredResults = results.filter(item => !item.is_draft); // filter out draft
return Promise.resolve(filteredResults);
});
}
// search by query
return Query.query({ q: term }).$promise
.then(({ results }) => Promise.resolve(results));
return Query.query({ q: term }).$promise.then(({ results }) => Promise.resolve(results));
}
export function QuerySelector(props) {
const [searchTerm, setSearchTerm] = useState('');
const [searchTerm, setSearchTerm] = useState("");
const [selectedQuery, setSelectedQuery] = useState();
const [doSearch, searchResults, searching] = useSearchResults(search, { initialResults: [] });
const placeholder = 'Search a query by name';
const placeholder = "Search a query by name";
const clearIcon = <i className="fa fa-times hide-in-percy" onClick={() => selectQuery(null)} />;
const spinIcon = <i className={cx('fa fa-spinner fa-pulse hide-in-percy', { hidden: !searching })} />;
const spinIcon = <i className={cx("fa fa-spinner fa-pulse hide-in-percy", { hidden: !searching })} />;
useEffect(() => { doSearch(searchTerm); }, [doSearch, searchTerm]);
useEffect(() => {
doSearch(searchTerm);
}, [doSearch, searchTerm]);
// set selected from prop
useEffect(() => {
@@ -48,12 +48,13 @@ export function QuerySelector(props) {
let query = null;
if (queryId) {
query = find(searchResults, { id: queryId });
if (!query) { // shouldn't happen
notification.error('Something went wrong...', 'Couldn\'t select query');
if (!query) {
// shouldn't happen
notification.error("Something went wrong...", "Couldn't select query");
}
}
setSearchTerm(query ? null : ''); // empty string triggers recent fetch
setSearchTerm(query ? null : ""); // empty string triggers recent fetch
setSelectedQuery(query);
props.onChange(query);
}
@@ -67,14 +68,11 @@ export function QuerySelector(props) {
<div className="list-group">
{searchResults.map(q => (
<a
className={cx('query-selector-result', 'list-group-item', { inactive: q.is_draft })}
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" />
data-test={`QueryId${q.id}`}>
{q.name} <QueryTagsControl isDraft={q.is_draft} tags={q.tags} className="inline-tags-control" />
</a>
))}
</div>
@@ -85,7 +83,7 @@ export function QuerySelector(props) {
return <Input value={selectedQuery && selectedQuery.name} placeholder={placeholder} disabled />;
}
if (props.type === 'select') {
if (props.type === "select") {
const suffixIcon = selectedQuery ? clearIcon : null;
const value = selectedQuery ? selectedQuery.name : searchTerm;
@@ -102,14 +100,23 @@ export function QuerySelector(props) {
filterOption={false}
defaultActiveFirstOption={false}
className={props.className}
data-test="QuerySelector"
>
{searchResults && searchResults.map((q) => {
data-test="QuerySelector">
{searchResults &&
searchResults.map(q => {
const disabled = q.is_draft;
return (
<Option value={q.id} key={q.id} disabled={disabled} className="query-selector-result" data-test={`QueryId${q.id}`}>
{q.name}{' '}
<QueryTagsControl isDraft={q.is_draft} tags={q.tags} className={cx('inline-tags-control', { disabled })} />
<Option
value={q.id}
key={q.id}
disabled={disabled}
className="query-selector-result"
data-test={`QueryId${q.id}`}>
{q.name}{" "}
<QueryTagsControl
isDraft={q.is_draft}
tags={q.tags}
className={cx("inline-tags-control", { disabled })}
/>
</Option>
);
})}
@@ -129,7 +136,7 @@ export function QuerySelector(props) {
suffix={spinIcon}
/>
)}
<div className="scrollbox" style={{ maxHeight: '50vh', marginTop: 15 }}>
<div className="scrollbox" style={{ maxHeight: "50vh", marginTop: 15 }}>
{searchResults && renderResults()}
</div>
</span>
@@ -139,20 +146,20 @@ export function QuerySelector(props) {
QuerySelector.propTypes = {
onChange: PropTypes.func.isRequired,
selectedQuery: PropTypes.object, // eslint-disable-line react/forbid-prop-types
type: PropTypes.oneOf(['select', 'default']),
type: PropTypes.oneOf(["select", "default"]),
className: PropTypes.string,
disabled: PropTypes.bool,
};
QuerySelector.defaultProps = {
selectedQuery: null,
type: 'default',
type: "default",
className: null,
disabled: false,
};
export default function init(ngModule) {
ngModule.component('querySelector', react2angular(QuerySelector));
ngModule.component("querySelector", react2angular(QuerySelector));
}
init.init = true;

View File

@@ -1,16 +1,16 @@
import { filter, debounce, find, isEmpty, size } from 'lodash';
import React 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';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { BigMessage } from '@/components/BigMessage';
import { filter, debounce, find, isEmpty, size } from "lodash";
import React 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";
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 LoadingState from "@/components/items-list/components/LoadingState";
import notification from "@/services/notification";
class SelectItemsDialog extends React.Component {
static propTypes = {
@@ -36,20 +36,20 @@ class SelectItemsDialog extends React.Component {
};
static defaultProps = {
dialogTitle: 'Add Items',
inputPlaceholder: 'Search...',
selectedItemsTitle: 'Selected items',
dialogTitle: "Add Items",
inputPlaceholder: "Search...",
selectedItemsTitle: "Selected items",
itemKey: item => item.id,
renderItem: () => '',
renderItem: () => "",
renderStagedItem: null, // hidden by default
save: items => items,
width: '80%',
width: "80%",
extraFooterContent: null,
showCount: false,
};
state = {
searchTerm: '',
searchTerm: "",
loading: false,
items: [],
selected: [],
@@ -57,10 +57,11 @@ class SelectItemsDialog extends React.Component {
};
// eslint-disable-next-line react/sort-comp
loadItems = (searchTerm = '') => {
loadItems = (searchTerm = "") => {
this.setState({ searchTerm, loading: true }, () => {
this.props.searchItems(searchTerm)
.then((items) => {
this.props
.searchItems(searchTerm)
.then(items => {
// If another search appeared while loading data - just reject this set
if (this.state.searchTerm === searchTerm) {
this.setState({ items, loading: false });
@@ -107,7 +108,7 @@ class SelectItemsDialog extends React.Component {
})
.catch(() => {
this.setState({ saveInProgress: false });
notification.error('Failed to save some of selected items.');
notification.error("Failed to save some of selected items.");
});
});
}
@@ -121,9 +122,8 @@ class SelectItemsDialog extends React.Component {
return (
<List.Item
className={classNames('p-l-10', 'p-r-10', { clickable: !isDisabled, disabled: isDisabled }, className)}
onClick={isDisabled ? null : () => this.toggleItem(item)}
>
className={classNames("p-l-10", "p-r-10", { clickable: !isDisabled, disabled: isDisabled }, className)}
onClick={isDisabled ? null : () => this.toggleItem(item)}>
{content}
</List.Item>
);
@@ -140,17 +140,22 @@ class SelectItemsDialog extends React.Component {
className="select-items-dialog"
width={width}
title={dialogTitle}
footer={(
footer={
<div className="d-flex align-items-center">
<span className="flex-fill m-r-5" style={{ textAlign: 'left', color: 'rgba(0, 0, 0, 0.5)' }}>{this.props.extraFooterContent}</span>
<span className="flex-fill m-r-5" style={{ textAlign: "left", color: "rgba(0, 0, 0, 0.5)" }}>
{this.props.extraFooterContent}
</span>
<Button onClick={dialog.dismiss}>Cancel</Button>
<Button onClick={() => this.save()} loading={saveInProgress} disabled={selected.length === 0} type="primary">
<Button
onClick={() => this.save()}
loading={saveInProgress}
disabled={selected.length === 0}
type="primary">
Save
{showCount && !isEmpty(selected) ? ` (${size(selected)})` : null}
</Button>
</div>
)}
>
}>
<div className="d-flex align-items-center m-b-10">
<div className="flex-fill">
<Input.Search
@@ -167,28 +172,20 @@ class SelectItemsDialog extends React.Component {
)}
</div>
<div className="d-flex align-items-stretch" style={{ minHeight: '30vh', maxHeight: '50vh' }}>
<div className="d-flex align-items-stretch" style={{ minHeight: "30vh", maxHeight: "50vh" }}>
<div className="flex-fill scrollbox">
{loading && <LoadingState className="" />}
{!loading && !hasResults && (
<BigMessage icon="fa-search" message="No items match your search." className="" />
)}
{!loading && hasResults && (
<List
size="small"
dataSource={items}
renderItem={item => this.renderItem(item, false)}
/>
<List size="small" dataSource={items} renderItem={item => this.renderItem(item, false)} />
)}
</div>
{renderStagedItem && (
<div className="w-50 m-l-20 scrollbox">
{(selected.length > 0) && (
<List
size="small"
dataSource={selected}
renderItem={item => this.renderItem(item, true)}
/>
{selected.length > 0 && (
<List size="small" dataSource={selected} renderItem={item => this.renderItem(item, true)} />
)}
</div>
)}

View File

@@ -1,9 +1,8 @@
import React from 'react';
import Menu from 'antd/lib/menu';
import { PageHeader } from '@/components/PageHeader';
import { $location } from '@/services/ng';
import settingsMenu from '@/services/settingsMenu';
import React from "react";
import Menu from "antd/lib/menu";
import { PageHeader } from "@/components/PageHeader";
import { $location } from "@/services/ng";
import settingsMenu from "@/services/settingsMenu";
function wrapSettingsTab(options, WrappedComponent) {
if (options) {
@@ -19,7 +18,9 @@ function wrapSettingsTab(options, WrappedComponent) {
<div className="bg-white tiled">
<Menu selectedKeys={[activeItem && activeItem.title]} selectable={false} mode="horizontal">
{settingsMenu.items.map(item => (
<Menu.Item key={item.title}><a href={item.path}>{item.title}</a></Menu.Item>
<Menu.Item key={item.title}>
<a href={item.path}>{item.title}</a>
</Menu.Item>
))}
</Menu>
<div className="p-15">

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import React from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
export function SortIcon({ column, sortColumn, reverse }) {
if (column !== sortColumn) {
@@ -8,7 +8,9 @@ export function SortIcon({ column, sortColumn, reverse }) {
}
return (
<span><i className={'fa fa-sort-' + (reverse ? 'desc' : 'asc')} /></span>
<span>
<i className={"fa fa-sort-" + (reverse ? "desc" : "asc")} />
</span>
);
}
@@ -25,7 +27,7 @@ SortIcon.defaultProps = {
};
export default function init(ngModule) {
ngModule.component('sortIcon', react2angular(SortIcon));
ngModule.component("sortIcon", react2angular(SortIcon));
}
init.init = true;

View File

@@ -1,12 +1,12 @@
import { map } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Badge from 'antd/lib/badge';
import Menu from 'antd/lib/menu';
import getTags from '@/services/getTags';
import { map } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
import Badge from "antd/lib/badge";
import Menu from "antd/lib/menu";
import getTags from "@/services/getTags";
import './TagsList.less';
import "./TagsList.less";
export class TagsList extends React.Component {
static propTypes = {
@@ -30,7 +30,7 @@ export class TagsList extends React.Component {
}
componentDidMount() {
getTags(this.props.tagsUrl).then((allTags) => {
getTags(this.props.tagsUrl).then(allTags => {
this.setState({ allTags });
});
}
@@ -46,7 +46,7 @@ export class TagsList extends React.Component {
}
} else {
// if the tag is the only selected, deselect it, otherwise select only it
if (selectedTags.has(tag) && (selectedTags.size === 1)) {
if (selectedTags.has(tag) && selectedTags.size === 1) {
selectedTags.clear();
} else {
selectedTags.clear();
@@ -66,7 +66,9 @@ export class TagsList extends React.Component {
<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)}>
<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>
@@ -81,7 +83,7 @@ export class TagsList extends React.Component {
}
export default function init(ngModule) {
ngModule.component('tagsList', react2angular(TagsList));
ngModule.component("tagsList", react2angular(TagsList));
}
init.init = true;

View File

@@ -1,21 +1,18 @@
import { pickBy, startsWith } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Radio from 'antd/lib/radio';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import { pickBy, startsWith } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Radio from "antd/lib/radio";
import Icon from "antd/lib/icon";
import Tooltip from "antd/lib/tooltip";
import './index.less';
import "./index.less";
export default function TextAlignmentSelect({ className, ...props }) {
return (
// Antd RadioGroup does not use any custom attributes
<div {...pickBy(props, (v, k) => startsWith(k, 'data-'))}>
<Radio.Group
className={cx('text-alignment-select', className)}
{...props}
>
<div {...pickBy(props, (v, k) => startsWith(k, "data-"))}>
<Radio.Group className={cx("text-alignment-select", className)} {...props}>
<Tooltip title="Align left" mouseEnterDelay={0} mouseLeaveDelay={0}>
<Radio.Button value="left" data-test="TextAlignmentSelect.Left">
<Icon type="align-left" />

View File

@@ -1,12 +1,12 @@
import moment from 'moment';
import { isNil } from 'lodash';
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { Moment } from '@/components/proptypes';
import { clientConfig } from '@/services/auth';
import useForceUpdate from '@/lib/hooks/useForceUpdate';
import Tooltip from 'antd/lib/tooltip';
import moment from "moment";
import { isNil } from "lodash";
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import { Moment } from "@/components/proptypes";
import { clientConfig } from "@/services/auth";
import useForceUpdate from "@/lib/hooks/useForceUpdate";
import Tooltip from "antd/lib/tooltip";
function toMoment(value) {
value = !isNil(value) ? moment(value) : null;
@@ -17,7 +17,7 @@ export function TimeAgo({ date, placeholder, autoUpdate }) {
const startDate = toMoment(date);
const value = startDate ? startDate.fromNow() : placeholder;
const title = startDate ? startDate.format(clientConfig.dateTimeFormat) : '';
const title = startDate ? startDate.format(clientConfig.dateTimeFormat) : "";
const forceUpdate = useForceUpdate();
@@ -38,12 +38,7 @@ export function TimeAgo({ date, placeholder, autoUpdate }) {
TimeAgo.propTypes = {
// `date` and `placeholder` used in `getDerivedStateFromProps`
// eslint-disable-next-line react/no-unused-prop-types
date: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Date),
Moment,
]),
date: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date), Moment]),
// eslint-disable-next-line react/no-unused-prop-types
placeholder: PropTypes.string,
autoUpdate: PropTypes.bool,
@@ -51,35 +46,35 @@ TimeAgo.propTypes = {
TimeAgo.defaultProps = {
date: null,
placeholder: '',
placeholder: "",
autoUpdate: true,
};
export default function init(ngModule) {
ngModule.directive('amTimeAgo', () => ({
ngModule.directive("amTimeAgo", () => ({
link($scope, $element, attr) {
const modelName = attr.amTimeAgo;
$scope.$watch(modelName, (value) => {
$scope.$watch(modelName, value => {
ReactDOM.render(<TimeAgo date={value} />, $element[0]);
});
$scope.$on('$destroy', () => {
$scope.$on("$destroy", () => {
ReactDOM.unmountComponentAtNode($element[0]);
});
},
}));
ngModule.component('rdTimeAgo', {
ngModule.component("rdTimeAgo", {
bindings: {
value: '=',
value: "=",
},
controller($scope, $element) {
$scope.$watch('$ctrl.value', () => {
$scope.$watch("$ctrl.value", () => {
// Initial render will occur here as well
ReactDOM.render(<TimeAgo date={this.value} placeholder="-" />, $element[0]);
});
$scope.$on('$destroy', () => {
$scope.$on("$destroy", () => {
ReactDOM.unmountComponentAtNode($element[0]);
});
},

View File

@@ -1,9 +1,9 @@
import React, { useMemo, useEffect } from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { Moment } from '@/components/proptypes';
import useForceUpdate from '@/lib/hooks/useForceUpdate';
import React, { useMemo, useEffect } from "react";
import moment from "moment";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
import { Moment } from "@/components/proptypes";
import useForceUpdate from "@/lib/hooks/useForceUpdate";
export function Timer({ from }) {
const startTime = useMemo(() => moment(from).valueOf(), [from]);
@@ -15,18 +15,13 @@ export function Timer({ from }) {
}, [forceUpdate]);
const diff = moment.now() - startTime;
const format = diff > 1000 * 60 * 60 ? 'HH:mm:ss' : 'mm:ss'; // no HH under an hour
const format = diff > 1000 * 60 * 60 ? "HH:mm:ss" : "mm:ss"; // no HH under an hour
return (<span className="rd-timer">{moment.utc(diff).format(format)}</span>);
return <span className="rd-timer">{moment.utc(diff).format(format)}</span>;
}
Timer.propTypes = {
from: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Date),
Moment,
]),
from: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date), Moment]),
};
Timer.defaultProps = {
@@ -34,7 +29,7 @@ Timer.defaultProps = {
};
export default function init(ngModule) {
ngModule.component('rdTimer', react2angular(Timer));
ngModule.component("rdTimer", react2angular(Timer));
}
init.init = true;

View File

@@ -1,12 +1,12 @@
import { map } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { map } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Table from 'antd/lib/table';
import Card from 'antd/lib/card';
import Spin from 'antd/lib/spin';
import Badge from 'antd/lib/badge';
import { Columns } from '@/components/items-list/components/ItemsTable';
import Table from "antd/lib/table";
import Card from "antd/lib/card";
import Spin from "antd/lib/spin";
import Badge from "antd/lib/badge";
import { Columns } from "@/components/items-list/components/ItemsTable";
// CounterCard
@@ -28,40 +28,48 @@ CounterCard.propTypes = {
};
CounterCard.defaultProps = {
value: '',
value: "",
};
// Tables
const commonColumns = [
{ title: 'Worker Name', dataIndex: 'worker' },
{ title: 'PID', dataIndex: 'worker_pid' },
{ title: 'Queue', dataIndex: 'queue' },
Columns.custom((value) => {
if (value === 'active') {
return <span><Badge status="processing" /> Active</span>;
{ title: "Worker Name", dataIndex: "worker" },
{ title: "PID", dataIndex: "worker_pid" },
{ title: "Queue", dataIndex: "queue" },
Columns.custom(
value => {
if (value === "active") {
return (
<span>
<Badge status="processing" /> Active
</span>
);
}
return <span><Badge status="warning" /> {value}</span>;
}, {
title: 'State',
dataIndex: 'state',
}),
Columns.timeAgo({ title: 'Start Time', dataIndex: 'start_time' }),
return (
<span>
<Badge status="warning" /> {value}
</span>
);
},
{
title: "State",
dataIndex: "state",
}
),
Columns.timeAgo({ title: "Start Time", dataIndex: "start_time" }),
];
const queryColumns = commonColumns.concat([
Columns.timeAgo({ title: 'Enqueue Time', dataIndex: 'enqueue_time' }),
{ title: 'Query ID', dataIndex: 'query_id' },
{ title: 'Org ID', dataIndex: 'org_id' },
{ title: 'Data Source ID', dataIndex: 'data_source_id' },
{ title: 'User ID', dataIndex: 'user_id' },
{ title: 'Scheduled', dataIndex: 'scheduled' },
Columns.timeAgo({ title: "Enqueue Time", dataIndex: "enqueue_time" }),
{ title: "Query ID", dataIndex: "query_id" },
{ title: "Org ID", dataIndex: "org_id" },
{ title: "Data Source ID", dataIndex: "data_source_id" },
{ title: "User ID", dataIndex: "user_id" },
{ title: "Scheduled", dataIndex: "scheduled" },
]);
const queuesColumns = map(
['Name', 'Active', 'Reserved', 'Waiting'],
c => ({ title: c, dataIndex: c.toLowerCase() }),
);
const queuesColumns = map(["Name", "Active", "Reserved", "Waiting"], c => ({ title: c, dataIndex: c.toLowerCase() }));
const TablePropTypes = {
loading: PropTypes.bool.isRequired,
@@ -69,27 +77,13 @@ const TablePropTypes = {
};
export function QueuesTable({ loading, items }) {
return (
<Table
loading={loading}
columns={queuesColumns}
rowKey="name"
dataSource={items}
/>
);
return <Table loading={loading} columns={queuesColumns} rowKey="name" dataSource={items} />;
}
QueuesTable.propTypes = TablePropTypes;
export function QueriesTable({ loading, items }) {
return (
<Table
loading={loading}
columns={queryColumns}
rowKey="task_id"
dataSource={items}
/>
);
return <Table loading={loading} columns={queryColumns} rowKey="task_id" dataSource={items} />;
}
QueriesTable.propTypes = TablePropTypes;

View File

@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import Tabs from 'antd/lib/tabs';
import { PageHeader } from '@/components/PageHeader';
import React from "react";
import PropTypes from "prop-types";
import Tabs from "antd/lib/tabs";
import { PageHeader } from "@/components/PageHeader";
import './layout.less';
import "./layout.less";
export default function Layout({ activeTab, children }) {
return (
@@ -13,16 +13,16 @@ export default function Layout({ activeTab, children }) {
<div className="bg-white tiled">
<Tabs className="admin-page-layout-tabs" defaultActiveKey={activeTab} animated={false}>
<Tabs.TabPane key="system_status" tab={<a href="admin/status">System Status</a>}>
{(activeTab === 'system_status') ? children : null}
{activeTab === "system_status" ? children : null}
</Tabs.TabPane>
<Tabs.TabPane key="tasks" tab={<a href="admin/queries/tasks">Celery Status</a>}>
{(activeTab === 'tasks') ? children : null}
{activeTab === "tasks" ? children : null}
</Tabs.TabPane>
<Tabs.TabPane key="jobs" tab={<a href="admin/queries/jobs">RQ Status</a>}>
{(activeTab === 'jobs') ? children : null}
{activeTab === "jobs" ? children : null}
</Tabs.TabPane>
<Tabs.TabPane key="outdated_queries" tab={<a href="admin/queries/outdated">Outdated Queries</a>}>
{(activeTab === 'outdated_queries') ? children : null}
{activeTab === "outdated_queries" ? children : null}
</Tabs.TabPane>
</Tabs>
</div>
@@ -36,6 +36,6 @@ Layout.propTypes = {
};
Layout.defaultProps = {
activeTab: 'system_status',
activeTab: "system_status",
children: null,
};

View File

@@ -1,39 +1,43 @@
import { map } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { map } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Badge from 'antd/lib/badge';
import Table from 'antd/lib/table';
import { Columns } from '@/components/items-list/components/ItemsTable';
import Badge from "antd/lib/badge";
import Table from "antd/lib/table";
import { Columns } from "@/components/items-list/components/ItemsTable";
// Tables
const otherJobsColumns = [
{ title: 'Queue', dataIndex: 'queue' },
{ title: 'Job Name', dataIndex: 'name' },
Columns.timeAgo({ title: 'Start Time', dataIndex: 'started_at' }),
Columns.timeAgo({ title: 'Enqueue Time', dataIndex: 'enqueued_at' }),
{ title: "Queue", dataIndex: "queue" },
{ title: "Job Name", dataIndex: "name" },
Columns.timeAgo({ title: "Start Time", dataIndex: "started_at" }),
Columns.timeAgo({ title: "Enqueue Time", dataIndex: "enqueued_at" }),
];
const workersColumns = [Columns.custom(
const workersColumns = [
Columns.custom(
value => (
<span><Badge status={{ busy: 'processing',
idle: 'default',
started: 'success',
suspended: 'warning' }[value]}
/> {value}
<span>
<Badge status={{ busy: "processing", idle: "default", started: "success", suspended: "warning" }[value]} />{" "}
{value}
</span>
), { title: 'State', dataIndex: 'state' },
)].concat(map(['Hostname', 'PID', 'Name', 'Queues', 'Current Job', 'Successful Jobs', 'Failed Jobs'],
c => ({ title: c, dataIndex: c.toLowerCase().replace(/\s/g, '_') }))).concat([
Columns.dateTime({ title: 'Birth Date', dataIndex: 'birth_date' }),
Columns.duration({ title: 'Total Working Time', dataIndex: 'total_working_time' }),
),
{ title: "State", dataIndex: "state" }
),
]
.concat(
map(["Hostname", "PID", "Name", "Queues", "Current Job", "Successful Jobs", "Failed Jobs"], c => ({
title: c,
dataIndex: c.toLowerCase().replace(/\s/g, "_"),
}))
)
.concat([
Columns.dateTime({ title: "Birth Date", dataIndex: "birth_date" }),
Columns.duration({ title: "Total Working Time", dataIndex: "total_working_time" }),
]);
const queuesColumns = map(
['Name', 'Started', 'Queued'],
c => ({ title: c, dataIndex: c.toLowerCase() }),
);
const queuesColumns = map(["Name", "Started", "Queued"], c => ({ title: c, dataIndex: c.toLowerCase() }));
const TablePropTypes = {
loading: PropTypes.bool.isRequired,
@@ -49,7 +53,7 @@ export function WorkersTable({ loading, items }) {
dataSource={items}
pagination={{
defaultPageSize: 25,
pageSizeOptions: ['10', '25', '50'],
pageSizeOptions: ["10", "25", "50"],
showSizeChanger: true,
}}
/>
@@ -59,27 +63,13 @@ export function WorkersTable({ loading, items }) {
WorkersTable.propTypes = TablePropTypes;
export function QueuesTable({ loading, items }) {
return (
<Table
loading={loading}
columns={queuesColumns}
rowKey="name"
dataSource={items}
/>
);
return <Table loading={loading} columns={queuesColumns} rowKey="name" dataSource={items} />;
}
QueuesTable.propTypes = TablePropTypes;
export function OtherJobsTable({ loading, items }) {
return (
<Table
loading={loading}
columns={otherJobsColumns}
rowKey="id"
dataSource={items}
/>
);
return <Table loading={loading} columns={otherJobsColumns} rowKey="id" dataSource={items} />;
}
OtherJobsTable.propTypes = TablePropTypes;

View File

@@ -1,30 +1,26 @@
/* eslint-disable react/prop-types */
import { toPairs } from 'lodash';
import React from 'react';
import { toPairs } from "lodash";
import React from "react";
import List from 'antd/lib/list';
import Card from 'antd/lib/card';
import { TimeAgo } from '@/components/TimeAgo';
import List from "antd/lib/list";
import Card from "antd/lib/card";
import { TimeAgo } from "@/components/TimeAgo";
import { toHuman, prettySize } from '@/filters';
import { toHuman, prettySize } from "@/filters";
export function General({ info }) {
info = toPairs(info);
return (
<Card title="General" size="small">
{(info.length === 0) && (
<div className="text-muted text-center">No data</div>
)}
{(info.length > 0) && (
{info.length === 0 && <div className="text-muted text-center">No data</div>}
{info.length > 0 && (
<List
size="small"
itemLayout="vertical"
dataSource={info}
renderItem={([name, value]) => (
<List.Item extra={<span className="badge">{value}</span>}>
{toHuman(name)}
</List.Item>
<List.Item extra={<span className="badge">{value}</span>}>{toHuman(name)}</List.Item>
)}
/>
)}
@@ -35,18 +31,14 @@ export function General({ info }) {
export function DatabaseMetrics({ info }) {
return (
<Card title="Redash Database" size="small">
{(info.length === 0) && (
<div className="text-muted text-center">No data</div>
)}
{(info.length > 0) && (
{info.length === 0 && <div className="text-muted text-center">No data</div>}
{info.length > 0 && (
<List
size="small"
itemLayout="vertical"
dataSource={info}
renderItem={([name, size]) => (
<List.Item extra={<span className="badge">{prettySize(size)}</span>}>
{name}
</List.Item>
<List.Item extra={<span className="badge">{prettySize(size)}</span>}>{name}</List.Item>
)}
/>
)}
@@ -58,18 +50,14 @@ export function Queues({ info }) {
info = toPairs(info);
return (
<Card title="Queues" size="small">
{(info.length === 0) && (
<div className="text-muted text-center">No data</div>
)}
{(info.length > 0) && (
{info.length === 0 && <div className="text-muted text-center">No data</div>}
{info.length > 0 && (
<List
size="small"
itemLayout="vertical"
dataSource={info}
renderItem={([name, queue]) => (
<List.Item extra={<span className="badge">{queue.size}</span>}>
{name}
</List.Item>
<List.Item extra={<span className="badge">{queue.size}</span>}>{name}</List.Item>
)}
/>
)}
@@ -78,33 +66,34 @@ export function Queues({ info }) {
}
export function Manager({ info }) {
const items = info ? [(
<List.Item extra={<span className="badge"><TimeAgo date={info.lastRefreshAt} placeholder="n/a" /></span>}>
const items = info
? [
<List.Item
extra={
<span className="badge">
<TimeAgo date={info.lastRefreshAt} placeholder="n/a" />
</span>
}>
Last Refresh
</List.Item>
), (
<List.Item extra={<span className="badge"><TimeAgo date={info.startedAt} placeholder="n/a" /></span>}>
</List.Item>,
<List.Item
extra={
<span className="badge">
<TimeAgo date={info.startedAt} placeholder="n/a" />
</span>
}>
Started
</List.Item>
), (
</List.Item>,
<List.Item extra={<span className="badge">{info.outdatedQueriesCount}</span>}>
Outdated Queries Count
</List.Item>
)] : [];
</List.Item>,
]
: [];
return (
<Card title="Manager" size="small">
{!info && (
<div className="text-muted text-center">No data</div>
)}
{info && (
<List
size="small"
itemLayout="vertical"
dataSource={items}
renderItem={item => item}
/>
)}
{!info && <div className="text-muted text-center">No data</div>}
{info && <List size="small" itemLayout="vertical" dataSource={items} renderItem={item => item} />}
</Card>
);
}

View File

@@ -1,30 +1,30 @@
/* eslint-disable no-template-curly-in-string */
import React, { useRef } from 'react';
import { react2angular } from 'react2angular';
import React, { useRef } from "react";
import { react2angular } from "react2angular";
import Dropdown from 'antd/lib/dropdown';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import Menu from 'antd/lib/menu';
import Input from 'antd/lib/input';
import Tooltip from 'antd/lib/tooltip';
import Dropdown from "antd/lib/dropdown";
import Button from "antd/lib/button";
import Icon from "antd/lib/icon";
import Menu from "antd/lib/menu";
import Input from "antd/lib/input";
import Tooltip from "antd/lib/tooltip";
import FavoritesDropdown from './components/FavoritesDropdown';
import HelpTrigger from '@/components/HelpTrigger';
import CreateDashboardDialog from '@/components/dashboards/CreateDashboardDialog';
import FavoritesDropdown from "./components/FavoritesDropdown";
import HelpTrigger from "@/components/HelpTrigger";
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
import { currentUser, Auth, clientConfig } from '@/services/auth';
import { $location, $route } from '@/services/ng';
import { Dashboard } from '@/services/dashboard';
import { Query } from '@/services/query';
import frontendVersion from '@/version.json';
import logoUrl from '@/assets/images/redash_icon_small.png';
import { currentUser, Auth, clientConfig } from "@/services/auth";
import { $location, $route } from "@/services/ng";
import { Dashboard } from "@/services/dashboard";
import { Query } from "@/services/query";
import frontendVersion from "@/version.json";
import logoUrl from "@/assets/images/redash_icon_small.png";
import './AppHeader.less';
import "./AppHeader.less";
function onSearch(q) {
$location.path('/queries').search({ q });
$location.path("/queries").search({ q });
$route.reload();
}
@@ -33,34 +33,34 @@ function DesktopNavbar() {
<div className="app-header" data-platform="desktop">
<div>
<Menu mode="horizontal" selectable={false}>
{currentUser.hasPermission('list_dashboards') && (
{currentUser.hasPermission("list_dashboards") && (
<Menu.Item key="dashboards" className="dropdown-menu-item">
<Button href="dashboards">Dashboards</Button>
<FavoritesDropdown fetch={Dashboard.favorites} urlTemplate="dashboard/${slug}" />
</Menu.Item>
)}
{currentUser.hasPermission('view_query') && (
{currentUser.hasPermission("view_query") && (
<Menu.Item key="queries" className="dropdown-menu-item">
<Button href="queries">Queries</Button>
<FavoritesDropdown fetch={Query.favorites} urlTemplate="queries/${id}" />
</Menu.Item>
)}
{currentUser.hasPermission('list_alerts') && (
{currentUser.hasPermission("list_alerts") && (
<Menu.Item key="alerts">
<Button href="alerts">Alerts</Button>
</Menu.Item>
)}
</Menu>
<Dropdown
trigger={['click']}
overlay={(
trigger={["click"]}
overlay={
<Menu>
{currentUser.hasPermission('create_query') && (
{currentUser.hasPermission("create_query") && (
<Menu.Item key="new-query">
<a href="queries/new">New Query</a>
</Menu.Item>
)}
{currentUser.hasPermission('create_dashboard') && (
{currentUser.hasPermission("create_dashboard") && (
<Menu.Item key="new-dashboard">
<a onMouseUp={() => CreateDashboardDialog.showModal()}>New Dashboard</a>
</Menu.Item>
@@ -69,8 +69,7 @@ function DesktopNavbar() {
<a href="alerts/new">New Alert</a>
</Menu.Item>
</Menu>
)}
>
}>
<Button type="primary" data-test="CreateButton">
Create <Icon type="down" />
</Button>
@@ -105,26 +104,24 @@ function DesktopNavbar() {
<Dropdown
overlayStyle={{ minWidth: 200 }}
placement="bottomRight"
trigger={['click']}
overlay={(
trigger={["click"]}
overlay={
<Menu>
<Menu.Item key="profile">
<a href="users/me">Edit Profile</a>
</Menu.Item>
{currentUser.hasPermission('super_admin') && (
<Menu.Divider />
)}
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
{currentUser.isAdmin && (
<Menu.Item key="datasources">
<a href="data_sources">Data Sources</a>
</Menu.Item>
)}
{currentUser.hasPermission('list_users') && (
{currentUser.hasPermission("list_users") && (
<Menu.Item key="groups">
<a href="groups">Groups</a>
</Menu.Item>
)}
{currentUser.hasPermission('list_users') && (
{currentUser.hasPermission("list_users") && (
<Menu.Item key="users">
<a href="users">Users</a>
</Menu.Item>
@@ -132,38 +129,41 @@ function DesktopNavbar() {
<Menu.Item key="snippets">
<a href="query_snippets">Query Snippets</a>
</Menu.Item>
{currentUser.hasPermission('list_users') && (
{currentUser.hasPermission("list_users") && (
<Menu.Item key="destinations">
<a href="destinations">Alert Destinations</a>
</Menu.Item>
)}
{currentUser.hasPermission('super_admin') && (
<Menu.Divider />
)}
{currentUser.hasPermission('super_admin') && (
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
{currentUser.hasPermission("super_admin") && (
<Menu.Item key="status">
<a href="admin/status">System Status</a>
</Menu.Item>
)}
<Menu.Divider />
<Menu.Item key="logout" onClick={() => Auth.logout()}>Log out</Menu.Item>
<Menu.Item key="logout" onClick={() => Auth.logout()}>
Log out
</Menu.Item>
<Menu.Divider />
<Menu.Item key="version" disabled>
Version: {clientConfig.version}
{frontendVersion !== clientConfig.version && ` (${frontendVersion.substring(0, 8)})`}
{clientConfig.newVersionAvailable && currentUser.hasPermission('super_admin') && (
{clientConfig.newVersionAvailable && currentUser.hasPermission("super_admin") && (
<Tooltip title="Update Available" placement="rightTop">
{' '}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a href="https://version.redash.io/" className="update-available" target="_blank" rel="noopener">
{" "}
{/* eslint-disable react/jsx-no-target-blank */}
<a
href="https://version.redash.io/"
className="update-available"
target="_blank"
rel="noopener">
<i className="fa fa-arrow-circle-down" />
</a>
</Tooltip>
)}
</Menu.Item>
</Menu>
)}
>
}>
<Button data-test="ProfileDropdown" className="profile-dropdown">
<img src={currentUser.profile_image_url} alt={currentUser.name} />
<span>{currentUser.name}</span>
@@ -190,21 +190,21 @@ function MobileNavbar() {
<div>
<Dropdown
overlayStyle={{ minWidth: 200 }}
trigger={['click']}
trigger={["click"]}
getPopupContainer={() => ref.current} // so the overlay menu stays with the fixed header when page scrolls
overlay={(
overlay={
<Menu mode="vertical" selectable={false}>
{currentUser.hasPermission('list_dashboards') && (
{currentUser.hasPermission("list_dashboards") && (
<Menu.Item key="dashboards">
<a href="dashboards">Dashboards</a>
</Menu.Item>
)}
{currentUser.hasPermission('view_query') && (
{currentUser.hasPermission("view_query") && (
<Menu.Item key="queries">
<a href="queries">Queries</a>
</Menu.Item>
)}
{currentUser.hasPermission('list_alerts') && (
{currentUser.hasPermission("list_alerts") && (
<Menu.Item key="alerts">
<a href="alerts">Alerts</a>
</Menu.Item>
@@ -218,23 +218,26 @@ function MobileNavbar() {
<a href="data_sources">Settings</a>
</Menu.Item>
)}
{currentUser.hasPermission('super_admin') && (
{currentUser.hasPermission("super_admin") && (
<Menu.Item key="status">
<a href="admin/status">System Status</a>
</Menu.Item>
)}
{currentUser.hasPermission('super_admin') && (
<Menu.Divider />
)}
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
<Menu.Item key="help">
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a href="https://redash.io/help" target="_blank" rel="noopener">Help</a>
<a href="https://redash.io/help" target="_blank" rel="noopener">
Help
</a>
</Menu.Item>
<Menu.Item key="logout" onClick={() => Auth.logout()}>
Log out
</Menu.Item>
<Menu.Item key="logout" onClick={() => Auth.logout()}>Log out</Menu.Item>
</Menu>
)}
>
<Button><Icon type="menu" /></Button>
}>
<Button>
<Icon type="menu" />
</Button>
</Dropdown>
</div>
</div>
@@ -251,7 +254,7 @@ export function AppHeader() {
}
export default function init(ngModule) {
ngModule.component('appHeader', react2angular(AppHeader));
ngModule.component("appHeader", react2angular(AppHeader));
}
init.init = true;

View File

@@ -1,12 +1,12 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { isEmpty, template } from 'lodash';
import React, { useState, useMemo, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import { isEmpty, template } from "lodash";
import Dropdown from 'antd/lib/dropdown';
import Icon from 'antd/lib/icon';
import Menu from 'antd/lib/menu';
import Dropdown from "antd/lib/dropdown";
import Icon from "antd/lib/icon";
import Menu from "antd/lib/menu";
import HelpTrigger from '@/components/HelpTrigger';
import HelpTrigger from "@/components/HelpTrigger";
export default function FavoritesDropdown({ fetch, urlTemplate }) {
const [items, setItems] = useState();
@@ -15,16 +15,19 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
const noItems = isEmpty(items);
const urlCompiled = useMemo(() => template(urlTemplate), [urlTemplate]);
const fetchItems = useCallback((showLoadingState = true) => {
const fetchItems = useCallback(
(showLoadingState = true) => {
setLoading(showLoadingState);
fetch().$promise
.then(({ results }) => {
fetch()
.$promise.then(({ results }) => {
setItems(results);
})
.finally(() => {
setLoading(false);
});
}, [fetch]);
},
[fetch]
);
// fetch items on init
useEffect(() => {
@@ -59,7 +62,12 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
);
return (
<Dropdown disabled={loading} trigger={['click']} placement="bottomLeft" onVisibleChange={onVisibleChange} overlay={menu}>
<Dropdown
disabled={loading}
trigger={["click"]}
placement="bottomLeft"
onVisibleChange={onVisibleChange}
overlay={menu}>
{loading ? <Icon type="loading" spin /> : <Icon type="down" />}
</Dropdown>
);

View File

@@ -1,4 +1,4 @@
import PromiseRejectionError from '@/lib/promise-rejection-error';
import PromiseRejectionError from "@/lib/promise-rejection-error";
// eslint-disable-next-line import/prefer-default-export
export class ErrorHandler {
@@ -18,10 +18,7 @@ export class ErrorHandler {
// eslint-disable-next-line no-console
console.error(error);
}
if (
(error === null) ||
(error instanceof PromiseRejectionError)
) {
if (error === null || error instanceof PromiseRejectionError) {
this.error = error;
}
}

View File

@@ -1,9 +1,9 @@
import debug from 'debug';
import PromiseRejectionError from '@/lib/promise-rejection-error';
import { ErrorHandler } from './error-handler';
import template from './template.html';
import debug from "debug";
import PromiseRejectionError from "@/lib/promise-rejection-error";
import { ErrorHandler } from "./error-handler";
import template from "./template.html";
const logger = debug('redash:app-view');
const logger = debug("redash:app-view");
const handler = new ErrorHandler();
@@ -14,7 +14,7 @@ const layouts = {
},
fixed: {
showHeader: true,
bodyClass: 'fixed-layout',
bodyClass: "fixed-layout",
},
defaultSignedOut: {
showHeader: false,
@@ -37,7 +37,7 @@ class AppViewComponent {
this.layout = layouts.defaultSignedOut;
this.handler = handler;
$rootScope.$on('$routeChangeStart', (event, route) => {
$rootScope.$on("$routeChangeStart", (event, route) => {
this.handler.reset();
// In case we're handling $routeProvider.otherwise call, there will be no
@@ -47,7 +47,7 @@ class AppViewComponent {
if ($$route.authenticated) {
// For routes that need authentication, check if session is already
// loaded, and load it if not.
logger('Requested authenticated route: ', route);
logger("Requested authenticated route: ", route);
if (!Auth.isAuthenticated()) {
event.preventDefault();
// Auth.requireSession resolves only if session loaded
@@ -59,12 +59,12 @@ class AppViewComponent {
}
});
$rootScope.$on('$routeChangeSuccess', (event, route) => {
$rootScope.$on("$routeChangeSuccess", (event, route) => {
const $$route = route.$$route || { authenticated: true };
this.applyLayout($$route);
});
$rootScope.$on('$routeChangeError', (event, current, previous, rejection) => {
$rootScope.$on("$routeChangeError", (event, current, previous, rejection) => {
const $$route = current.$$route || { authenticated: true };
this.applyLayout($$route);
throw new PromiseRejectionError(rejection);
@@ -79,13 +79,14 @@ class AppViewComponent {
export default function init(ngModule) {
ngModule.factory(
'$exceptionHandler',
() => function exceptionHandler(exception) {
"$exceptionHandler",
() =>
function exceptionHandler(exception) {
handler.process(exception);
},
}
);
ngModule.component('appView', {
ngModule.component("appView", {
template,
controller: AppViewComponent,
});

View File

@@ -1,9 +1,9 @@
function cancelQueryButton() {
return {
restrict: 'E',
restrict: "E",
scope: {
queryId: '=',
taskId: '=',
queryId: "=",
taskId: "=",
},
transclude: true,
template:
@@ -16,11 +16,11 @@ function cancelQueryButton() {
$http.delete(`api/jobs/${$scope.taskId}`).success(() => {});
let queryId = $scope.queryId;
if ($scope.queryId === 'adhoc') {
if ($scope.queryId === "adhoc") {
queryId = null;
}
Events.record('cancel_execute', 'query', queryId, { admin: true });
Events.record("cancel_execute", "query", queryId, { admin: true });
$scope.inProgress = true;
};
},
@@ -28,7 +28,7 @@ function cancelQueryButton() {
}
export default function init(ngModule) {
ngModule.directive('cancelQueryButton', cancelQueryButton);
ngModule.directive("cancelQueryButton", cancelQueryButton);
}
init.init = true;

View File

@@ -1,10 +1,10 @@
import Input from 'antd/lib/input';
import { includes, isEmpty } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import EmptyState from '@/components/items-list/components/EmptyState';
import Input from "antd/lib/input";
import { includes, isEmpty } from "lodash";
import PropTypes from "prop-types";
import React from "react";
import EmptyState from "@/components/items-list/components/EmptyState";
import './CardsList.less';
import "./CardsList.less";
const { Search } = Input;
@@ -16,7 +16,7 @@ export default class CardsList extends React.Component {
imgSrc: PropTypes.string.isRequired,
onClick: PropTypes.func,
href: PropTypes.string,
}),
})
),
showSearch: PropTypes.bool,
};
@@ -27,7 +27,7 @@ export default class CardsList extends React.Component {
};
state = {
searchText: '',
searchText: "",
};
constructor(props) {
@@ -35,7 +35,7 @@ export default class CardsList extends React.Component {
this.items = [];
let itemId = 1;
props.items.forEach((item) => {
props.items.forEach(item => {
this.items.push({ id: itemId, ...item });
itemId += 1;
});
@@ -55,23 +55,22 @@ export default class CardsList extends React.Component {
const { showSearch } = this.props;
const { searchText } = this.state;
const filteredItems = this.items.filter(item => isEmpty(searchText) ||
includes(item.title.toLowerCase(), searchText.toLowerCase()));
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
/>
<Search placeholder="Search..." onChange={e => this.setState({ searchText: e.target.value })} autoFocus />
</div>
</div>
)}
{isEmpty(filteredItems) ? (<EmptyState className="" />) : (
{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))}

View File

@@ -1,18 +1,15 @@
import { each, values, map, includes, first } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'antd/lib/select';
import Modal from 'antd/lib/modal';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import {
MappingType,
ParameterMappingListInput,
} from '@/components/ParameterMappingInput';
import { QuerySelector } from '@/components/QuerySelector';
import { each, values, map, includes, first } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
import Modal from "antd/lib/modal";
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 notification from "@/services/notification";
import { Query } from '@/services/query';
import { Query } from "@/services/query";
const { Option, OptGroup } = Select;
@@ -39,21 +36,19 @@ class AddWidgetDialog extends React.Component {
});
if (selectedQuery) {
Query.get({ id: selectedQuery.id }, (query) => {
Query.get({ id: selectedQuery.id }, query => {
if (query) {
const existingParamNames = map(
this.props.dashboard.getParametersDefs(),
param => param.name,
);
const existingParamNames = map(this.props.dashboard.getParametersDefs(), param => param.name);
this.setState({
selectedQuery: query,
parameterMappings: map(query.getParametersDefs(), param => ({
name: param.name,
type: includes(existingParamNames, param.name)
? MappingType.DashboardMapToExisting : MappingType.DashboardAddNew,
? MappingType.DashboardMapToExisting
: MappingType.DashboardAddNew,
mapTo: param.name,
value: param.normalizedValue,
title: '',
title: "",
param,
})),
});
@@ -66,7 +61,7 @@ class AddWidgetDialog extends React.Component {
}
selectVisualization(query, visualizationId) {
each(query.visualizations, (visualization) => {
each(query.visualizations, visualization => {
if (visualization.id === visualizationId) {
this.setState({ selectedVis: visualization });
return false;
@@ -79,12 +74,13 @@ class AddWidgetDialog extends React.Component {
this.setState({ saveInProgress: true });
this.props.onConfirm(selectedVis, parameterMappings)
this.props
.onConfirm(selectedVis, parameterMappings)
.then(() => {
this.props.dialog.close();
})
.catch(() => {
notification.error('Widget could not be added');
notification.error("Widget could not be added");
})
.finally(() => {
this.setState({ saveInProgress: false });
@@ -98,7 +94,7 @@ class AddWidgetDialog extends React.Component {
renderVisualizationInput() {
let visualizationGroups = {};
if (this.state.selectedQuery) {
each(this.state.selectedQuery.visualizations, (vis) => {
each(this.state.selectedQuery.visualizations, vis => {
visualizationGroups[vis.type] = visualizationGroups[vis.type] || [];
visualizationGroups[vis.type].push(vis);
});
@@ -112,12 +108,13 @@ class AddWidgetDialog extends React.Component {
id="choose-visualization"
className="w-100"
defaultValue={first(this.state.selectedQuery.visualizations).id}
onChange={visualizationId => this.selectVisualization(this.state.selectedQuery, visualizationId)}
>
onChange={visualizationId => this.selectVisualization(this.state.selectedQuery, visualizationId)}>
{visualizationGroups.map(visualizations => (
<OptGroup label={visualizations[0].type} key={visualizations[0].type}>
{visualizations.map(visualization => (
<Option value={visualization.id} key={visualization.id}>{visualization.name}</Option>
<Option value={visualization.id} key={visualization.id}>
{visualization.name}
</Option>
))}
</OptGroup>
))}
@@ -141,15 +138,15 @@ class AddWidgetDialog extends React.Component {
disabled: !this.state.selectedQuery,
}}
okText="Add to Dashboard"
width={700}
>
width={700}>
<div data-test="AddWidgetDialog">
<QuerySelector onChange={query => this.selectQuery(query)} />
{this.state.selectedQuery && this.renderVisualizationInput()}
{
(this.state.parameterMappings.length > 0) && [
<label key="parameters-title" htmlFor="parameter-mappings">Parameters</label>,
{this.state.parameterMappings.length > 0 && [
<label key="parameters-title" htmlFor="parameter-mappings">
Parameters
</label>,
<ParameterMappingListInput
key="parameters-list"
id="parameter-mappings"
@@ -157,8 +154,7 @@ class AddWidgetDialog extends React.Component {
existingParams={existingParams}
onChange={mappings => this.updateParamMappings(mappings)}
/>,
]
}
]}
</div>
</Modal>
);

View File

@@ -1,15 +1,15 @@
import { includes, reduce, some } from 'lodash';
import { includes, reduce, some } from "lodash";
// TODO: Revisit this implementation when migrating widget component to React
const WIDGET_SELECTOR = '[data-widgetid="{0}"]';
const WIDGET_CONTENT_SELECTOR = [
'.widget-header', // header
'.visualization-renderer', // visualization
'.scrollbox .alert', // error state
'.spinner-container', // loading state
'.tile__bottom-control', // footer
].join(',');
".widget-header", // header
".visualization-renderer", // visualization
".scrollbox .alert", // error state
".spinner-container", // loading state
".tile__bottom-control", // footer
].join(",");
const INTERVAL = 200;
export default class AutoHeightController {
@@ -29,9 +29,7 @@ export default class AutoHeightController {
.map(widget => widget.id.toString());
// added
newWidgetIds
.filter(id => !includes(Object.keys(this.widgets), id))
.forEach(this.add);
newWidgetIds.filter(id => !includes(Object.keys(this.widgets), id)).forEach(this.add);
// removed
Object.keys(this.widgets)
@@ -39,12 +37,12 @@ export default class AutoHeightController {
.forEach(this.remove);
}
add = (id) => {
add = id => {
if (this.isEmpty()) {
this.start();
}
const selector = WIDGET_SELECTOR.replace('{0}', id);
const selector = WIDGET_SELECTOR.replace("{0}", id);
this.widgets[id] = [
function getHeight() {
const widgetEl = document.querySelector(selector);
@@ -56,15 +54,19 @@ export default class AutoHeightController {
const els = widgetEl.querySelectorAll(WIDGET_CONTENT_SELECTOR);
// calculate accumulated height
return reduce(els, (acc, el) => {
return reduce(
els,
(acc, el) => {
const height = el ? el.getBoundingClientRect().height : 0;
return acc + height;
}, 0);
},
0
);
},
];
};
remove = (id) => {
remove = id => {
// ignore if not an active autoHeight widget
if (!this.exists(id)) {
return;
@@ -83,10 +85,9 @@ export default class AutoHeightController {
isEmpty = () => !some(this.widgets);
checkHeightChanges = () => {
Object
.keys(this.widgets)
Object.keys(this.widgets)
.filter(this.exists) // reject already removed items
.forEach((id) => {
.forEach(id => {
const [getHeight, prevHeight] = this.widgets[id];
const height = getHeight();
if (height && height !== prevHeight) {
@@ -114,5 +115,5 @@ export default class AutoHeightController {
destroy = () => {
this.stop();
this.widgets = null;
}
};
}

View File

@@ -1,15 +1,15 @@
import { trim } from 'lodash';
import React, { useRef, useState, useEffect } from 'react';
import Modal from 'antd/lib/modal';
import Input from 'antd/lib/input';
import DynamicComponent from '@/components/DynamicComponent';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { $location, $http } from '@/services/ng';
import recordEvent from '@/services/recordEvent';
import { policy } from '@/services/policy';
import { trim } from "lodash";
import React, { useRef, useState, useEffect } from "react";
import Modal from "antd/lib/modal";
import Input from "antd/lib/input";
import DynamicComponent from "@/components/DynamicComponent";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { $location, $http } from "@/services/ng";
import recordEvent from "@/services/recordEvent";
import { policy } from "@/services/policy";
function CreateDashboardDialog({ dialog }) {
const [name, setName] = useState('');
const [name, setName] = useState("");
const [isValid, setIsValid] = useState(false);
const [saveInProgress, setSaveInProgress] = useState(false);
const inputRef = useRef();
@@ -29,19 +29,21 @@ function CreateDashboardDialog({ dialog }) {
function handleNameChange(event) {
const value = trim(event.target.value);
setName(value);
setIsValid(value !== '');
setIsValid(value !== "");
}
function save() {
if (name !== '') {
if (name !== "") {
setSaveInProgress(true);
$http.post('api/dashboards', { name })
.then(({ data }) => {
$http.post("api/dashboards", { name }).then(({ data }) => {
dialog.close();
$location.path(`/dashboard/${data.slug}`).search('edit').replace();
$location
.path(`/dashboard/${data.slug}`)
.search("edit")
.replace();
});
recordEvent('create', 'dashboard');
recordEvent("create", "dashboard");
}
}
@@ -55,7 +57,7 @@ function CreateDashboardDialog({ dialog }) {
okButtonProps={{
disabled: !isValid || saveInProgress,
loading: saveInProgress,
'data-test': 'DashboardSaveButton',
"data-test": "DashboardSaveButton",
}}
cancelButtonProps={{
disabled: saveInProgress,
@@ -64,9 +66,8 @@ function CreateDashboardDialog({ dialog }) {
closable={!saveInProgress}
maskClosable={!saveInProgress}
wrapProps={{
'data-test': 'CreateDashboardDialog',
}}
>
"data-test": "CreateDashboardDialog",
}}>
<DynamicComponent name="CreateDashboardDialogExtra" disabled={!isCreateDashboardEnabled}>
<Input
ref={inputRef}

View File

@@ -1,17 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { chain, cloneDeep, find } from 'lodash';
import { react2angular } from 'react2angular';
import cx from 'classnames';
import { Responsive, WidthProvider } from 'react-grid-layout';
import { VisualizationWidget, TextboxWidget, RestrictedWidget } from '@/components/dashboards/dashboard-widget';
import { FiltersType } from '@/components/Filters';
import cfg from '@/config/dashboard-grid-options';
import AutoHeightController from './AutoHeightController';
import { WidgetTypeEnum } from '@/services/widget';
import React from "react";
import PropTypes from "prop-types";
import { chain, cloneDeep, find } from "lodash";
import { react2angular } from "react2angular";
import cx from "classnames";
import { Responsive, WidthProvider } from "react-grid-layout";
import { VisualizationWidget, TextboxWidget, RestrictedWidget } from "@/components/dashboards/dashboard-widget";
import { FiltersType } from "@/components/Filters";
import cfg from "@/config/dashboard-grid-options";
import AutoHeightController from "./AutoHeightController";
import { WidgetTypeEnum } from "@/services/widget";
import 'react-grid-layout/css/styles.css';
import './dashboard-grid.less';
import "react-grid-layout/css/styles.css";
import "./dashboard-grid.less";
const ResponsiveGridLayout = WidthProvider(Responsive);
@@ -31,8 +31,8 @@ const WidgetType = PropTypes.shape({
}).isRequired,
});
const SINGLE = 'single-column';
const MULTI = 'multi-column';
const SINGLE = "single-column";
const MULTI = "multi-column";
class DashboardGrid extends React.Component {
static propTypes = {
@@ -61,7 +61,10 @@ class DashboardGrid extends React.Component {
};
static normalizeFrom(widget) {
const { id, options: { position: pos } } = widget;
const {
id,
options: { position: pos },
} = widget;
return {
i: id.toString(),
@@ -130,14 +133,14 @@ class DashboardGrid extends React.Component {
}
const normalized = chain(layouts[MULTI])
.keyBy('i')
.keyBy("i")
.mapValues(this.normalizeTo)
.value();
this.props.onLayoutChange(normalized);
};
onBreakpointChange = (mode) => {
onBreakpointChange = mode => {
this.mode = mode;
this.props.onBreakpointChange(mode === SINGLE);
};
@@ -174,14 +177,22 @@ class DashboardGrid extends React.Component {
});
render() {
const className = cx('dashboard-wrapper', this.props.isEditing ? 'editing-mode' : 'preview-mode');
const { onLoadWidget, onRefreshWidget, onRemoveWidget,
onParameterMappingsChange, filters, dashboard, isPublic, widgets } = this.props;
const className = cx("dashboard-wrapper", this.props.isEditing ? "editing-mode" : "preview-mode");
const {
onLoadWidget,
onRefreshWidget,
onRemoveWidget,
onParameterMappingsChange,
filters,
dashboard,
isPublic,
widgets,
} = this.props;
return (
<div className={className}>
<ResponsiveGridLayout
className={cx('layout', { 'disable-animations': this.state.disableAnimations })}
className={cx("layout", { "disable-animations": this.state.disableAnimations })}
cols={{ [MULTI]: cfg.columns, [SINGLE]: 1 }}
rowHeight={cfg.rowHeight - cfg.margins}
margin={[cfg.margins, cfg.margins]}
@@ -192,9 +203,8 @@ class DashboardGrid extends React.Component {
layouts={this.state.layouts}
onLayoutChange={this.onLayoutChange}
onBreakpointChange={this.onBreakpointChange}
breakpoints={{ [MULTI]: cfg.mobileBreakPoint, [SINGLE]: 0 }}
>
{widgets.map((widget) => {
breakpoints={{ [MULTI]: cfg.mobileBreakPoint, [SINGLE]: 0 }}>
{widgets.map(widget => {
const widgetProps = {
widget,
filters,
@@ -209,8 +219,9 @@ class DashboardGrid extends React.Component {
data-grid={DashboardGrid.normalizeFrom(widget)}
data-widgetid={widget.id}
data-test={`WidgetId${widget.id}`}
className={cx('dashboard-widget-wrapper', { 'widget-auto-height-enabled': this.autoHeightCtrl.exists(widget.id) })}
>
className={cx("dashboard-widget-wrapper", {
"widget-auto-height-enabled": this.autoHeightCtrl.exists(widget.id),
})}>
{type === WidgetTypeEnum.VISUALIZATION && (
<VisualizationWidget
{...widgetProps}
@@ -232,7 +243,7 @@ class DashboardGrid extends React.Component {
}
export default function init(ngModule) {
ngModule.component('dashboardGrid', react2angular(DashboardGrid));
ngModule.component("dashboardGrid", react2angular(DashboardGrid));
}
init.init = true;

View File

@@ -1,21 +1,21 @@
import { isMatch, map, find, sortBy } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Modal from 'antd/lib/modal';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { isMatch, map, find, sortBy } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Modal from "antd/lib/modal";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import {
MappingType,
ParameterMappingListInput,
parameterMappingsToEditableMappings,
editableMappingsToParameterMappings,
synchronizeWidgetTitles,
} from '@/components/ParameterMappingInput';
import notification from '@/services/notification';
} from "@/components/ParameterMappingInput";
import notification from "@/services/notification";
export function getParamValuesSnapshot(mappings, dashboardParameters) {
return map(
sortBy(mappings, m => m.name),
(m) => {
m => {
let param;
switch (m.type) {
case MappingType.StaticValue:
@@ -28,7 +28,7 @@ export function getParamValuesSnapshot(mappings, dashboardParameters) {
return [m.name, param ? param.value : null];
// no default
}
},
}
);
}
@@ -39,7 +39,7 @@ class EditParameterMappingsDialog extends React.Component {
dialog: DialogPropType.isRequired,
};
originalParamValuesSnapshot = null
originalParamValuesSnapshot = null;
constructor(props) {
super(props);
@@ -47,12 +47,12 @@ class EditParameterMappingsDialog extends React.Component {
const parameterMappings = parameterMappingsToEditableMappings(
props.widget.options.parameterMappings,
props.widget.query.getParametersDefs(),
map(this.props.dashboard.getParametersDefs(), p => p.name),
map(this.props.dashboard.getParametersDefs(), p => p.name)
);
this.originalParamValuesSnapshot = getParamValuesSnapshot(
parameterMappings,
this.props.dashboard.getParametersDefs(),
this.props.dashboard.getParametersDefs()
);
this.state = {
@@ -71,7 +71,7 @@ class EditParameterMappingsDialog extends React.Component {
const valuesChanged = !isMatch(
this.originalParamValuesSnapshot,
getParamValuesSnapshot(this.state.parameterMappings, this.props.dashboard.getParametersDefs()),
getParamValuesSnapshot(this.state.parameterMappings, this.props.dashboard.getParametersDefs())
);
const widgetsToSave = [
@@ -84,7 +84,7 @@ class EditParameterMappingsDialog extends React.Component {
this.props.dialog.close(valuesChanged);
})
.catch(() => {
notification.error('Widget cannot be updated');
notification.error("Widget cannot be updated");
})
.finally(() => {
this.setState({ saveInProgress: false });
@@ -103,9 +103,8 @@ class EditParameterMappingsDialog extends React.Component {
title="Parameters"
onOk={() => this.saveWidget()}
okButtonProps={{ loading: this.state.saveInProgress }}
width={700}
>
{(this.state.parameterMappings.length > 0) && (
width={700}>
{this.state.parameterMappings.length > 0 && (
<ParameterMappingListInput
mappings={this.state.parameterMappings}
existingParams={this.props.dashboard.getParametersDefs()}

View File

@@ -1,24 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Modal from 'antd/lib/modal';
import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { VisualizationName } from '@/visualizations/VisualizationName';
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Modal from "antd/lib/modal";
import { VisualizationRenderer } from "@/visualizations/VisualizationRenderer";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { VisualizationName } from "@/visualizations/VisualizationName";
function ExpandedWidgetDialog({ dialog, widget }) {
return (
<Modal
{...dialog.props}
title={(
title={
<>
<VisualizationName visualization={widget.visualization} />{' '}
<span>{widget.getQuery().name}</span>
<VisualizationName visualization={widget.visualization} /> <span>{widget.getQuery().name}</span>
</>
)}
}
width="95%"
footer={(<Button onClick={dialog.dismiss}>Close</Button>)}
>
footer={<Button onClick={dialog.dismiss}>Close</Button>}>
<VisualizationRenderer
visualization={widget.visualization}
queryResult={widget.getQueryResult()}

View File

@@ -1,16 +1,16 @@
import { markdown } from 'markdown';
import { debounce } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Modal from 'antd/lib/modal';
import Input from 'antd/lib/input';
import Tooltip from 'antd/lib/tooltip';
import Divider from 'antd/lib/divider';
import HtmlContent from '@/components/HtmlContent';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import notification from '@/services/notification';
import { markdown } from "markdown";
import { debounce } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Modal from "antd/lib/modal";
import Input from "antd/lib/input";
import Tooltip from "antd/lib/tooltip";
import Divider from "antd/lib/divider";
import HtmlContent from "@/components/HtmlContent";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import notification from "@/services/notification";
import './TextboxDialog.less';
import "./TextboxDialog.less";
class TextboxDialog extends React.Component {
static propTypes = {
@@ -20,7 +20,7 @@ class TextboxDialog extends React.Component {
};
static defaultProps = {
text: '',
text: "",
};
updatePreview = debounce(() => {
@@ -40,7 +40,7 @@ class TextboxDialog extends React.Component {
};
}
onTextChanged = (event) => {
onTextChanged = event => {
this.setState({ text: event.target.value });
this.updatePreview();
};
@@ -48,12 +48,13 @@ class TextboxDialog extends React.Component {
saveWidget() {
this.setState({ saveInProgress: true });
this.props.onConfirm(this.state.text)
this.props
.onConfirm(this.state.text)
.then(() => {
this.props.dialog.close();
})
.catch(() => {
notification.error('Widget could not be added');
notification.error("Widget could not be added");
})
.finally(() => {
this.setState({ saveInProgress: false });
@@ -67,16 +68,15 @@ class TextboxDialog extends React.Component {
return (
<Modal
{...dialog.props}
title={isNew ? 'Add Textbox' : 'Edit Textbox'}
title={isNew ? "Add Textbox" : "Edit Textbox"}
onOk={() => this.saveWidget()}
okButtonProps={{
loading: this.state.saveInProgress,
disabled: !this.state.text,
}}
okText={isNew ? 'Add to Dashboard' : 'Save'}
okText={isNew ? "Add to Dashboard" : "Save"}
width={500}
wrapProps={{ 'data-test': 'TextboxDialog' }}
>
wrapProps={{ "data-test": "TextboxDialog" }}>
<div className="textbox-dialog">
<Input.TextArea
className="resize-vertical"
@@ -87,14 +87,11 @@ class TextboxDialog extends React.Component {
placeholder="This is where you write some text"
/>
<small>
Supports basic{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.markdownguide.org/cheat-sheet/#basic-syntax"
>
Supports basic{" "}
<a target="_blank" rel="noopener noreferrer" href="https://www.markdownguide.org/cheat-sheet/#basic-syntax">
<Tooltip title="Markdown guide opens in new window">Markdown</Tooltip>
</a>.
</a>
.
</small>
{this.state.text && (
<React.Fragment>

View File

@@ -1,15 +1,15 @@
import React from 'react';
import Widget from './Widget';
import React from "react";
import Widget from "./Widget";
function RestrictedWidget(props) {
return (
<Widget {...props} className="d-flex justify-content-center align-items-center widget-restricted">
<div className="t-body scrollbox">
<div className="text-center">
<h1><span className="zmdi zmdi-lock" /></h1>
<p className="text-muted">
This widget requires access to a data source you don&apos;t have access to.
</p>
<h1>
<span className="zmdi zmdi-lock" />
</h1>
<p className="text-muted">This widget requires access to a data source you don&apos;t have access to.</p>
</div>
</div>
</Widget>

View File

@@ -1,10 +1,10 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { markdown } from 'markdown';
import Menu from 'antd/lib/menu';
import HtmlContent from '@/components/HtmlContent';
import TextboxDialog from '@/components/dashboards/TextboxDialog';
import Widget from './Widget';
import React, { useState } from "react";
import PropTypes from "prop-types";
import { markdown } from "markdown";
import Menu from "antd/lib/menu";
import HtmlContent from "@/components/HtmlContent";
import TextboxDialog from "@/components/dashboards/TextboxDialog";
import Widget from "./Widget";
function TextboxWidget(props) {
const { widget, canEdit } = props;
@@ -13,7 +13,7 @@ function TextboxWidget(props) {
const editTextBox = () => {
TextboxDialog.showModal({
text: widget.text,
onConfirm: (newText) => {
onConfirm: newText => {
widget.text = newText;
setText(newText);
return widget.save();
@@ -22,7 +22,9 @@ function TextboxWidget(props) {
};
const TextboxMenuOptions = [
<Menu.Item key="edit" onClick={editTextBox}>Edit</Menu.Item>,
<Menu.Item key="edit" onClick={editTextBox}>
Edit
</Menu.Item>,
];
if (!widget.width) {
@@ -31,9 +33,7 @@ function TextboxWidget(props) {
return (
<Widget {...props} menuOptions={canEdit ? TextboxMenuOptions : null} className="widget-text">
<HtmlContent className="body-row-auto scrollbox t-body p-15 markdown">
{markdown.toHTML(text || '')}
</HtmlContent>
<HtmlContent className="body-row-auto scrollbox t-body p-15 markdown">{markdown.toHTML(text || "")}</HtmlContent>
</Widget>
);
}

View File

@@ -1,27 +1,27 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { compact, isEmpty, invoke } from 'lodash';
import { markdown } from 'markdown';
import cx from 'classnames';
import Menu from 'antd/lib/menu';
import { currentUser } from '@/services/auth';
import recordEvent from '@/services/recordEvent';
import { formatDateTime } from '@/filters/datetime';
import HtmlContent from '@/components/HtmlContent';
import { Parameters } from '@/components/Parameters';
import { TimeAgo } from '@/components/TimeAgo';
import { Timer } from '@/components/Timer';
import { Moment } from '@/components/proptypes';
import QueryLink from '@/components/QueryLink';
import { FiltersType } from '@/components/Filters';
import ExpandedWidgetDialog from '@/components/dashboards/ExpandedWidgetDialog';
import EditParameterMappingsDialog from '@/components/dashboards/EditParameterMappingsDialog';
import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer';
import Widget from './Widget';
import React, { useState } from "react";
import PropTypes from "prop-types";
import { compact, isEmpty, invoke } from "lodash";
import { markdown } from "markdown";
import cx from "classnames";
import Menu from "antd/lib/menu";
import { currentUser } from "@/services/auth";
import recordEvent from "@/services/recordEvent";
import { formatDateTime } from "@/filters/datetime";
import HtmlContent from "@/components/HtmlContent";
import { Parameters } from "@/components/Parameters";
import { TimeAgo } from "@/components/TimeAgo";
import { Timer } from "@/components/Timer";
import { Moment } from "@/components/proptypes";
import QueryLink from "@/components/QueryLink";
import { FiltersType } from "@/components/Filters";
import ExpandedWidgetDialog from "@/components/dashboards/ExpandedWidgetDialog";
import EditParameterMappingsDialog from "@/components/dashboards/EditParameterMappingsDialog";
import { VisualizationRenderer } from "@/visualizations/VisualizationRenderer";
import Widget from "./Widget";
function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParametersEdit }) {
const canViewQuery = currentUser.hasPermission('view_query');
const canEditParameters = canEditDashboard && !isEmpty(invoke(widget, 'query.getParametersDefs'));
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();
@@ -30,32 +30,33 @@ function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParameters
return compact([
<Menu.Item key="download_csv" disabled={isQueryResultEmpty}>
{!isQueryResultEmpty ? (
<a href={downloadLink('csv')} download={downloadName('csv')} target="_self">
<a href={downloadLink("csv")} download={downloadName("csv")} target="_self">
Download as CSV File
</a>
) : 'Download as CSV File'}
) : (
"Download as CSV File"
)}
</Menu.Item>,
<Menu.Item key="download_excel" disabled={isQueryResultEmpty}>
{!isQueryResultEmpty ? (
<a href={downloadLink('xlsx')} download={downloadName('xlsx')} target="_self">
<a href={downloadLink("xlsx")} download={downloadName("xlsx")} target="_self">
Download as Excel File
</a>
) : 'Download as Excel File'}
) : (
"Download as Excel File"
)}
</Menu.Item>,
((canViewQuery || canEditParameters) && <Menu.Divider key="divider" />),
(canViewQuery || canEditParameters) && <Menu.Divider key="divider" />,
canViewQuery && (
<Menu.Item key="view_query">
<a href={widget.getQuery().getUrl(true, widget.visualization.id)}>View Query</a>
</Menu.Item>
),
(canEditParameters && (
<Menu.Item
key="edit_parameters"
onClick={onParametersEdit}
>
canEditParameters && (
<Menu.Item key="edit_parameters" onClick={onParametersEdit}>
Edit Parameters
</Menu.Item>
)),
),
]);
}
@@ -74,7 +75,7 @@ RefreshIndicator.propTypes = { refreshStartedAt: Moment };
RefreshIndicator.defaultProps = { refreshStartedAt: null };
function VisualizationWidgetHeader({ widget, refreshStartedAt, parameters, onParametersUpdate }) {
const canViewQuery = currentUser.hasPermission('view_query');
const canViewQuery = currentUser.hasPermission("view_query");
return (
<>
@@ -85,7 +86,7 @@ function VisualizationWidgetHeader({ widget, refreshStartedAt, parameters, onPar
<QueryLink query={widget.getQuery()} visualization={widget.visualization} readOnly={!canViewQuery} />
</p>
<HtmlContent className="text-muted markdown query--description">
{markdown.toHTML(widget.getQuery().description || '')}
{markdown.toHTML(widget.getQuery().description || "")}
</HtmlContent>
</div>
</div>
@@ -113,10 +114,10 @@ VisualizationWidgetHeader.defaultProps = {
function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) {
const widgetQueryResult = widget.getQueryResult();
const updatedAt = invoke(widgetQueryResult, 'getUpdatedAt');
const updatedAt = invoke(widgetQueryResult, "getUpdatedAt");
const [refreshClickButtonId, setRefreshClickButtonId] = useState();
const refreshWidget = (buttonId) => {
const refreshWidget = buttonId => {
if (!refreshClickButtonId) {
setRefreshClickButtonId(buttonId);
onRefresh().finally(() => setRefreshClickButtonId(null));
@@ -126,22 +127,21 @@ function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) {
return (
<>
<span>
{(!isPublic && !!widgetQueryResult) && (
{!isPublic && !!widgetQueryResult && (
<a
className="refresh-button hidden-print btn btn-sm btn-default btn-transparent"
onClick={() => refreshWidget(1)}
data-test="RefreshButton"
>
<i className={cx('zmdi zmdi-refresh', { 'zmdi-hc-spin': refreshClickButtonId === 1 })} />{' '}
data-test="RefreshButton">
<i className={cx("zmdi zmdi-refresh", { "zmdi-hc-spin": refreshClickButtonId === 1 })} />{" "}
<TimeAgo date={updatedAt} />
</a>
)}
<span className="visible-print">
<i className="zmdi zmdi-time-restore" />{' '}{formatDateTime(updatedAt)}
<i className="zmdi zmdi-time-restore" /> {formatDateTime(updatedAt)}
</span>
{isPublic && (
<span className="small hidden-print">
<i className="zmdi zmdi-time-restore" />{' '}<TimeAgo date={updatedAt} />
<i className="zmdi zmdi-time-restore" /> <TimeAgo date={updatedAt} />
</span>
)}
</span>
@@ -149,15 +149,11 @@ function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) {
{!isPublic && (
<a
className="btn btn-sm btn-default hidden-print btn-transparent btn__refresh"
onClick={() => refreshWidget(2)}
>
<i className={cx('zmdi zmdi-refresh', { 'zmdi-hc-spin': refreshClickButtonId === 2 })} />
onClick={() => refreshWidget(2)}>
<i className={cx("zmdi zmdi-refresh", { "zmdi-hc-spin": refreshClickButtonId === 2 })} />
</a>
)}
<a
className="btn btn-sm btn-default hidden-print btn-transparent btn__refresh"
onClick={onExpand}
>
<a className="btn btn-sm btn-default hidden-print btn-transparent btn__refresh" onClick={onExpand}>
<i className="zmdi zmdi-fullscreen" />
</a>
</span>
@@ -204,8 +200,8 @@ class VisualizationWidget extends React.Component {
componentDidMount() {
const { widget, onLoad } = this.props;
recordEvent('view', 'query', widget.visualization.query.id, { dashboard: true });
recordEvent('view', 'visualization', widget.visualization.id, { dashboard: true });
recordEvent("view", "query", widget.visualization.query.id, { dashboard: true });
recordEvent("view", "visualization", widget.visualization.id, { dashboard: true });
onLoad();
}
@@ -218,7 +214,7 @@ class VisualizationWidget extends React.Component {
EditParameterMappingsDialog.showModal({
dashboard,
widget,
}).result.then((valuesChanged) => {
}).result.then(valuesChanged => {
// refresh widget if any parameter value has been updated
if (valuesChanged) {
onRefresh();
@@ -233,7 +229,7 @@ class VisualizationWidget extends React.Component {
const widgetQueryResult = widget.getQueryResult();
const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus();
switch (widgetStatus) {
case 'failed':
case "failed":
return (
<div className="body-row-auto scrollbox">
{widgetQueryResult.getError() && (
@@ -243,7 +239,7 @@ class VisualizationWidget extends React.Component {
)}
</div>
);
case 'done':
case "done":
return (
<div className="body-row-auto scrollbox">
<VisualizationRenderer
@@ -275,27 +271,28 @@ class VisualizationWidget extends React.Component {
<Widget
{...this.props}
className="widget-visualization"
menuOptions={visualizationWidgetMenuOptions({ widget,
menuOptions={visualizationWidgetMenuOptions({
widget,
canEditDashboard: canEdit,
onParametersEdit: this.editParameterMappings })}
header={(
onParametersEdit: this.editParameterMappings,
})}
header={
<VisualizationWidgetHeader
widget={widget}
refreshStartedAt={isRefreshing ? widget.refreshStartedAt : null}
parameters={localParameters}
onParametersUpdate={onRefresh}
/>
)}
footer={(
}
footer={
<VisualizationWidgetFooter
widget={widget}
isPublic={isPublic}
onRefresh={onRefresh}
onExpand={this.expandWidget}
/>
)}
tileProps={{ 'data-refreshing': isRefreshing }}
>
}
tileProps={{ "data-refreshing": isRefreshing }}>
{this.renderVisualization()}
</Widget>
);

View File

@@ -1,31 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { isEmpty } from 'lodash';
import Dropdown from 'antd/lib/dropdown';
import Modal from 'antd/lib/modal';
import Menu from 'antd/lib/menu';
import recordEvent from '@/services/recordEvent';
import { Moment } from '@/components/proptypes';
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { isEmpty } from "lodash";
import Dropdown from "antd/lib/dropdown";
import Modal from "antd/lib/modal";
import Menu from "antd/lib/menu";
import recordEvent from "@/services/recordEvent";
import { Moment } from "@/components/proptypes";
import './Widget.less';
import "./Widget.less";
function WidgetDropdownButton({ extraOptions, showDeleteOption, onDelete }) {
const WidgetMenu = (
<Menu data-test="WidgetDropdownButtonMenu">
{extraOptions}
{(showDeleteOption && extraOptions) && <Menu.Divider />}
{showDeleteOption && extraOptions && <Menu.Divider />}
{showDeleteOption && <Menu.Item onClick={onDelete}>Remove from Dashboard</Menu.Item>}
</Menu>
);
return (
<div className="widget-menu-regular">
<Dropdown
overlay={WidgetMenu}
placement="bottomRight"
trigger={['click']}
>
<Dropdown overlay={WidgetMenu} placement="bottomRight" trigger={["click"]}>
<a className="action p-l-15 p-r-15" data-test="WidgetDropdownButton">
<i className="zmdi zmdi-more-vert" />
</a>
@@ -75,7 +71,7 @@ class Widget extends React.Component {
};
static defaultProps = {
className: '',
className: "",
children: null,
header: null,
footer: null,
@@ -89,17 +85,17 @@ class Widget extends React.Component {
componentDidMount() {
const { widget } = this.props;
recordEvent('view', 'widget', widget.id);
recordEvent("view", "widget", widget.id);
}
deleteWidget = () => {
const { widget, onDelete } = this.props;
Modal.confirm({
title: 'Delete Widget',
content: 'Are you sure you want to remove this widget from the dashboard?',
okText: 'Delete',
okType: 'danger',
title: "Delete Widget",
content: "Are you sure you want to remove this widget from the dashboard?",
okText: "Delete",
okType: "danger",
onOk: () => widget.delete().then(onDelete),
maskClosable: true,
autoFocusButton: null,
@@ -107,12 +103,11 @@ class Widget extends React.Component {
};
render() {
const { className, children, header, footer, canEdit, isPublic,
menuOptions, tileProps } = this.props;
const { className, children, header, footer, canEdit, isPublic, menuOptions, tileProps } = this.props;
const showDropdownButton = !isPublic && (canEdit || !isEmpty(menuOptions));
return (
<div className="widget-wrapper">
<div className={cx('tile body-container', className)} {...tileProps}>
<div className={cx("tile body-container", className)} {...tileProps}>
<div className="widget-actions">
{showDropdownButton && (
<WidgetDropdownButton
@@ -123,15 +118,9 @@ class Widget extends React.Component {
)}
{canEdit && <WidgetDeleteButton onClick={this.deleteWidget} />}
</div>
<div className="body-row widget-header">
{header}
</div>
<div className="body-row widget-header">{header}</div>
{children}
{footer && (
<div className="body-row tile__bottom-control">
{footer}
</div>
)}
{footer && <div className="body-row tile__bottom-control">{footer}</div>}
</div>
</div>
);

View File

@@ -1,3 +1,3 @@
export { default as VisualizationWidget } from './VisualizationWidget';
export { default as TextboxWidget } from './TextboxWidget';
export { default as RestrictedWidget } from './RestrictedWidget';
export { default as VisualizationWidget } from "./VisualizationWidget";
export { default as TextboxWidget } from "./TextboxWidget";
export { default as RestrictedWidget } from "./RestrictedWidget";

View File

@@ -1,33 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Form from 'antd/lib/form';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
import Checkbox from 'antd/lib/checkbox';
import Button from 'antd/lib/button';
import Upload from 'antd/lib/upload';
import Icon from 'antd/lib/icon';
import { includes, isFunction, filter, difference, isEmpty, some, isNumber, isBoolean } from 'lodash';
import Select from 'antd/lib/select';
import notification from '@/services/notification';
import Collapse from '@/components/Collapse';
import AceEditorInput from '@/components/AceEditorInput';
import { toHuman } from '@/filters';
import { Field, Action, AntdForm } from '../proptypes';
import helper from './dynamicFormHelper';
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Form from "antd/lib/form";
import Input from "antd/lib/input";
import InputNumber from "antd/lib/input-number";
import Checkbox from "antd/lib/checkbox";
import Button from "antd/lib/button";
import Upload from "antd/lib/upload";
import Icon from "antd/lib/icon";
import { includes, isFunction, filter, difference, isEmpty, some, isNumber, isBoolean } from "lodash";
import Select from "antd/lib/select";
import notification from "@/services/notification";
import Collapse from "@/components/Collapse";
import AceEditorInput from "@/components/AceEditorInput";
import { toHuman } from "@/filters";
import { Field, Action, AntdForm } from "../proptypes";
import helper from "./dynamicFormHelper";
import './DynamicForm.less';
import "./DynamicForm.less";
const fieldRules = ({ type, required, minLength }) => {
const requiredRule = required;
const minLengthRule = minLength && includes(['text', 'email', 'password'], type);
const emailTypeRule = type === 'email';
const minLengthRule = minLength && includes(["text", "email", "password"], type);
const emailTypeRule = type === "email";
return [
requiredRule && { required, message: 'This field is required.' },
minLengthRule && { min: minLength, message: 'This field is too short.' },
emailTypeRule && { type: 'email', message: 'This field must be a valid email.' },
requiredRule && { required, message: "This field is required." },
minLengthRule && { min: minLength, message: "This field is too short." },
emailTypeRule && { type: "email", message: "This field must be a valid email." },
].filter(rule => rule);
};
@@ -49,31 +49,34 @@ class DynamicForm extends React.Component {
actions: [],
feedbackIcons: false,
hideSubmitButton: false,
saveText: 'Save',
saveText: "Save",
onSubmit: () => {},
};
constructor(props) {
super(props);
const hasFilledExtraField = some(props.fields, (field) => {
const hasFilledExtraField = some(props.fields, field => {
const { extra, initialValue } = field;
return extra && (!isEmpty(initialValue) || isNumber(initialValue) || (isBoolean(initialValue) && initialValue));
});
const inProgressActions = {};
props.actions.forEach(action => inProgressActions[action.name] = false);
props.actions.forEach(action => (inProgressActions[action.name] = false));
this.state = {
isSubmitting: false,
showExtraFields: hasFilledExtraField,
inProgressActions
inProgressActions,
};
this.actionCallbacks = this.props.actions.reduce((acc, cur) => ({
this.actionCallbacks = this.props.actions.reduce(
(acc, cur) => ({
...acc,
[cur.name]: cur.callback,
}), null);
}),
null
);
}
setActionInProgress = (actionName, inProgress) => {
@@ -85,29 +88,29 @@ class DynamicForm extends React.Component {
}));
};
handleSubmit = (e) => {
handleSubmit = e => {
this.setState({ isSubmitting: true });
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
this.props.onSubmit(
values,
(msg) => {
msg => {
const { setFieldsValue, getFieldsValue } = this.props.form;
this.setState({ isSubmitting: false });
setFieldsValue(getFieldsValue()); // reset form touched state
notification.success(msg);
},
(msg) => {
msg => {
this.setState({ isSubmitting: false });
notification.error(msg);
},
}
);
} else this.setState({ isSubmitting: false });
});
};
handleAction = (e) => {
handleAction = e => {
const actionName = e.target.dataset.action;
this.setActionInProgress(actionName, true);
@@ -118,7 +121,7 @@ class DynamicForm extends React.Component {
base64File = (fieldName, e) => {
if (e && e.fileList[0]) {
helper.getBase64(e.file).then((value) => {
helper.getBase64(e.file).then(value => {
this.props.form.setFieldsValue({ [fieldName]: value });
});
}
@@ -138,7 +141,9 @@ class DynamicForm extends React.Component {
const upload = (
<Upload {...props} beforeUpload={() => false}>
<Button disabled={disabled}><Icon type="upload" /> Click to upload</Button>
<Button disabled={disabled}>
<Icon type="upload" /> Click to upload
</Button>
</Upload>
);
@@ -155,24 +160,23 @@ class DynamicForm extends React.Component {
initialValue,
};
return getFieldDecorator(name, decoratorOptions)(
return getFieldDecorator(
name,
decoratorOptions
)(
<Select
{...props}
optionFilterProp="children"
loading={loading || false}
mode={mode}
getPopupContainer={trigger => trigger.parentNode}
>
{options && options.map(option => (
<Option
key={`${option.value}`}
value={option.value}
disabled={readOnly}
>
getPopupContainer={trigger => trigger.parentNode}>
{options &&
options.map(option => (
<Option key={`${option.value}`} value={option.value} disabled={readOnly}>
{option.name || option.value}
</Option>
))}
</Select>,
</Select>
);
}
@@ -183,50 +187,50 @@ class DynamicForm extends React.Component {
const options = {
rules: fieldRules(field),
valuePropName: type === 'checkbox' ? 'checked' : 'value',
valuePropName: type === "checkbox" ? "checked" : "value",
initialValue,
};
if (type === 'checkbox') {
if (type === "checkbox") {
return getFieldDecorator(name, options)(<Checkbox {...props}>{fieldLabel}</Checkbox>);
} else if (type === 'file') {
} else if (type === "file") {
return this.renderUpload(field, props);
} else if (type === 'select') {
} else if (type === "select") {
return this.renderSelect(field, props);
} else if (type === 'content') {
} else if (type === "content") {
return field.content;
} else if (type === 'number') {
} else if (type === "number") {
return getFieldDecorator(name, options)(<InputNumber {...props} />);
} else if (type === 'textarea') {
} else if (type === "textarea") {
return getFieldDecorator(name, options)(<Input.TextArea {...props} />);
} else if (type === 'ace') {
} else if (type === "ace") {
return getFieldDecorator(name, options)(<AceEditorInput {...props} />);
}
return getFieldDecorator(name, options)(<Input {...props} />);
}
renderFields(fields) {
return fields.map((field) => {
return fields.map(field => {
const FormItem = Form.Item;
const { name, title, type, readOnly, autoFocus, contentAfter } = field;
const fieldLabel = title || toHuman(name);
const { feedbackIcons, form } = this.props;
const formItemProps = {
className: 'm-b-10',
hasFeedback: type !== 'checkbox' && type !== 'file' && feedbackIcons,
label: type === 'checkbox' ? '' : fieldLabel,
className: "m-b-10",
hasFeedback: type !== "checkbox" && type !== "file" && feedbackIcons,
label: type === "checkbox" ? "" : fieldLabel,
};
const fieldProps = {
...field.props,
className: 'w-100',
className: "w-100",
name,
type,
readOnly,
autoFocus,
placeholder: field.placeholder,
'data-test': fieldLabel,
"data-test": fieldLabel,
};
return (
@@ -239,29 +243,33 @@ class DynamicForm extends React.Component {
}
renderActions() {
return this.props.actions.map((action) => {
return this.props.actions.map(action => {
const inProgress = this.state.inProgressActions[action.name];
const { isFieldsTouched } = this.props.form;
const actionProps = {
key: action.name,
htmlType: 'button',
className: action.pullRight ? 'pull-right m-t-10' : 'm-t-10',
htmlType: "button",
className: action.pullRight ? "pull-right m-t-10" : "m-t-10",
type: action.type,
disabled: (isFieldsTouched() && action.disableWhenDirty),
disabled: isFieldsTouched() && action.disableWhenDirty,
loading: inProgress,
onClick: this.handleAction,
};
return (<Button {...actionProps} data-action={action.name}>{action.name}</Button>);
return (
<Button {...actionProps} data-action={action.name}>
{action.name}
</Button>
);
});
}
render() {
const submitProps = {
type: 'primary',
htmlType: 'submit',
className: 'w-100 m-t-20',
type: "primary",
htmlType: "submit",
className: "w-100 m-t-20",
disabled: this.state.isSubmitting,
loading: this.state.isSubmitting,
};
@@ -280,10 +288,9 @@ class DynamicForm extends React.Component {
type="dashed"
block
className="extra-options-button"
onClick={() => this.setState({ showExtraFields: !showExtraFields })}
>
onClick={() => this.setState({ showExtraFields: !showExtraFields })}>
Additional Settings
<i className={cx('fa m-l-5', { 'fa-caret-up': showExtraFields, 'fa-caret-down': !showExtraFields })} />
<i className={cx("fa m-l-5", { "fa-caret-up": showExtraFields, "fa-caret-down": !showExtraFields })} />
</Button>
<Collapse collapsed={!showExtraFields} className="extra-options-content">
{this.renderFields(extraFields)}

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { each, includes, isUndefined, isEmpty, map } from 'lodash';
import React from "react";
import { each, includes, isUndefined, isEmpty, map } from "lodash";
function orderedInputs(properties, order, targetOptions) {
const inputs = new Array(order.length);
Object.keys(properties).forEach((key) => {
Object.keys(properties).forEach(key => {
const position = order.indexOf(key);
const input = {
name: key,
@@ -15,8 +15,8 @@ function orderedInputs(properties, order, targetOptions) {
initialValue: targetOptions[key],
};
if (input.type === 'select') {
input.placeholder = 'Select an option';
if (input.type === "select") {
input.placeholder = "Select an option";
input.options = properties[key].options;
}
@@ -31,29 +31,29 @@ function orderedInputs(properties, order, targetOptions) {
function normalizeSchema(configurationSchema) {
each(configurationSchema.properties, (prop, name) => {
if (name === 'password' || name === 'passwd') {
prop.type = 'password';
if (name === "password" || name === "passwd") {
prop.type = "password";
}
if (name.endsWith('File')) {
prop.type = 'file';
if (name.endsWith("File")) {
prop.type = "file";
}
if (prop.type === 'boolean') {
prop.type = 'checkbox';
if (prop.type === "boolean") {
prop.type = "checkbox";
}
if (prop.type === 'string') {
prop.type = 'text';
if (prop.type === "string") {
prop.type = "text";
}
if (!isEmpty(prop.enum)) {
prop.type = 'select';
prop.type = "select";
prop.options = map(prop.enum, value => ({ value, name: value }));
}
if (!isEmpty(prop.extendedEnum)) {
prop.type = 'select';
prop.type = "select";
prop.options = prop.extendedEnum;
}
@@ -66,14 +66,14 @@ function normalizeSchema(configurationSchema) {
function setDefaultValueToFields(configurationSchema, options = {}) {
const properties = configurationSchema.properties;
Object.keys(properties).forEach((key) => {
Object.keys(properties).forEach(key => {
const property = properties[key];
// set default value for checkboxes
if (!isUndefined(property.default) && property.type === 'checkbox') {
if (!isUndefined(property.default) && property.type === "checkbox") {
options[key] = property.default;
}
// set default or first value when value has predefined options
if (property.type === 'select') {
if (property.type === "select") {
const optionValues = map(property.options, option => option.value);
options[key] = includes(optionValues, property.default) ? property.default : optionValues[0];
}
@@ -91,12 +91,12 @@ function getFields(type = {}, target = { options: {} }) {
const isNewTarget = !target.id;
const inputs = [
{
name: 'name',
title: 'Name',
type: 'text',
name: "name",
title: "Name",
type: "text",
required: true,
initialValue: target.name,
contentAfter: React.createElement('hr'),
contentAfter: React.createElement("hr"),
placeholder: `My ${type.name}`,
autoFocus: isNewTarget,
},
@@ -108,8 +108,8 @@ function getFields(type = {}, target = { options: {} }) {
function updateTargetWithValues(target, values) {
target.name = values.name;
Object.keys(values).forEach((key) => {
if (key !== 'name') {
Object.keys(values).forEach(key => {
if (key !== "name") {
target.options[key] = values[key];
}
});
@@ -119,7 +119,7 @@ function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.substr(reader.result.indexOf(',') + 1));
reader.onload = () => resolve(reader.result.substr(reader.result.indexOf(",") + 1));
reader.onerror = error => reject(error);
});
}

View File

@@ -1,22 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import moment from 'moment';
import { includes } from 'lodash';
import { isDynamicDate, getDynamicDateFromString } from '@/services/parameters/DateParameter';
import DateInput from '@/components/DateInput';
import DateTimeInput from '@/components/DateTimeInput';
import DynamicButton from '@/components/dynamic-parameters/DynamicButton';
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import moment from "moment";
import { includes } from "lodash";
import { isDynamicDate, getDynamicDateFromString } from "@/services/parameters/DateParameter";
import DateInput from "@/components/DateInput";
import DateTimeInput from "@/components/DateTimeInput";
import DynamicButton from "@/components/dynamic-parameters/DynamicButton";
import './DynamicParameters.less';
import "./DynamicParameters.less";
const DYNAMIC_DATE_OPTIONS = [
{ name: 'Today/Now',
value: getDynamicDateFromString('d_now'),
label: () => getDynamicDateFromString('d_now').value().format('MMM D') },
{ name: 'Yesterday',
value: getDynamicDateFromString('d_yesterday'),
label: () => getDynamicDateFromString('d_yesterday').value().format('MMM D') },
{
name: "Today/Now",
value: getDynamicDateFromString("d_now"),
label: () =>
getDynamicDateFromString("d_now")
.value()
.format("MMM D"),
},
{
name: "Yesterday",
value: getDynamicDateFromString("d_yesterday"),
label: () =>
getDynamicDateFromString("d_yesterday")
.value()
.format("MMM D"),
},
];
class DateParameter extends React.Component {
@@ -29,8 +39,8 @@ class DateParameter extends React.Component {
};
static defaultProps = {
type: '',
className: '',
type: "",
className: "",
value: null,
parameter: null,
onSelect: () => {},
@@ -41,9 +51,9 @@ class DateParameter extends React.Component {
this.dateComponentRef = React.createRef();
}
onDynamicValueSelect = (dynamicValue) => {
onDynamicValueSelect = dynamicValue => {
const { onSelect, parameter } = this.props;
if (dynamicValue === 'static') {
if (dynamicValue === "static") {
const parameterValue = parameter.getExecutionValue();
if (parameterValue) {
onSelect(moment(parameterValue));
@@ -60,14 +70,14 @@ class DateParameter extends React.Component {
render() {
const { type, value, className, onSelect } = this.props;
const hasDynamicValue = isDynamicDate(value);
const isDateTime = includes(type, 'datetime');
const isDateTime = includes(type, "datetime");
const additionalAttributes = {};
let DateComponent = DateInput;
if (isDateTime) {
DateComponent = DateTimeInput;
if (includes(type, 'with-seconds')) {
if (includes(type, "with-seconds")) {
additionalAttributes.withSeconds = true;
}
}
@@ -85,16 +95,16 @@ class DateParameter extends React.Component {
return (
<DateComponent
ref={this.dateComponentRef}
className={classNames('redash-datepicker', { 'dynamic-value': hasDynamicValue }, className)}
className={classNames("redash-datepicker", { "dynamic-value": hasDynamicValue }, className)}
onSelect={onSelect}
suffixIcon={(
suffixIcon={
<DynamicButton
options={DYNAMIC_DATE_OPTIONS}
selectedDynamicValue={hasDynamicValue ? value : null}
enabled={hasDynamicValue}
onSelect={this.onDynamicValueSelect}
/>
)}
}
{...additionalAttributes}
/>
);

View File

@@ -1,67 +1,138 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import moment from 'moment';
import { includes, isArray, isObject } from 'lodash';
import { isDynamicDateRange, getDynamicDateRangeFromString } from '@/services/parameters/DateRangeParameter';
import DateRangeInput from '@/components/DateRangeInput';
import DateTimeRangeInput from '@/components/DateTimeRangeInput';
import DynamicButton from '@/components/dynamic-parameters/DynamicButton';
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import moment from "moment";
import { includes, isArray, isObject } from "lodash";
import { isDynamicDateRange, getDynamicDateRangeFromString } from "@/services/parameters/DateRangeParameter";
import DateRangeInput from "@/components/DateRangeInput";
import DateTimeRangeInput from "@/components/DateTimeRangeInput";
import DynamicButton from "@/components/dynamic-parameters/DynamicButton";
import './DynamicParameters.less';
import "./DynamicParameters.less";
const DYNAMIC_DATE_OPTIONS = [
{ name: 'This week',
value: getDynamicDateRangeFromString('d_this_week'),
label: () => getDynamicDateRangeFromString('d_this_week').value()[0].format('MMM D') + ' - ' +
getDynamicDateRangeFromString('d_this_week').value()[1].format('MMM D') },
{ name: 'This month',
value: getDynamicDateRangeFromString('d_this_month'),
label: () => getDynamicDateRangeFromString('d_this_month').value()[0].format('MMMM') },
{ name: 'This year',
value: getDynamicDateRangeFromString('d_this_year'),
label: () => getDynamicDateRangeFromString('d_this_year').value()[0].format('YYYY') },
{ name: 'Last week',
value: getDynamicDateRangeFromString('d_last_week'),
label: () => getDynamicDateRangeFromString('d_last_week').value()[0].format('MMM D') + ' - ' +
getDynamicDateRangeFromString('d_last_week').value()[1].format('MMM D') },
{ name: 'Last month',
value: getDynamicDateRangeFromString('d_last_month'),
label: () => getDynamicDateRangeFromString('d_last_month').value()[0].format('MMMM') },
{ name: 'Last year',
value: getDynamicDateRangeFromString('d_last_year'),
label: () => getDynamicDateRangeFromString('d_last_year').value()[0].format('YYYY') },
{ name: 'Last 7 days',
value: getDynamicDateRangeFromString('d_last_7_days'),
label: () => getDynamicDateRangeFromString('d_last_7_days').value()[0].format('MMM D') + ' - Today' },
{ name: 'Last 14 days',
value: getDynamicDateRangeFromString('d_last_14_days'),
label: () => getDynamicDateRangeFromString('d_last_14_days').value()[0].format('MMM D') + ' - Today' },
{ name: 'Last 30 days',
value: getDynamicDateRangeFromString('d_last_30_days'),
label: () => getDynamicDateRangeFromString('d_last_30_days').value()[0].format('MMM D') + ' - Today' },
{ name: 'Last 60 days',
value: getDynamicDateRangeFromString('d_last_60_days'),
label: () => getDynamicDateRangeFromString('d_last_60_days').value()[0].format('MMM D') + ' - Today' },
{ name: 'Last 90 days',
value: getDynamicDateRangeFromString('d_last_90_days'),
label: () => getDynamicDateRangeFromString('d_last_90_days').value()[0].format('MMM D') + ' - Today' },
{
name: "This week",
value: getDynamicDateRangeFromString("d_this_week"),
label: () =>
getDynamicDateRangeFromString("d_this_week")
.value()[0]
.format("MMM D") +
" - " +
getDynamicDateRangeFromString("d_this_week")
.value()[1]
.format("MMM D"),
},
{
name: "This month",
value: getDynamicDateRangeFromString("d_this_month"),
label: () =>
getDynamicDateRangeFromString("d_this_month")
.value()[0]
.format("MMMM"),
},
{
name: "This year",
value: getDynamicDateRangeFromString("d_this_year"),
label: () =>
getDynamicDateRangeFromString("d_this_year")
.value()[0]
.format("YYYY"),
},
{
name: "Last week",
value: getDynamicDateRangeFromString("d_last_week"),
label: () =>
getDynamicDateRangeFromString("d_last_week")
.value()[0]
.format("MMM D") +
" - " +
getDynamicDateRangeFromString("d_last_week")
.value()[1]
.format("MMM D"),
},
{
name: "Last month",
value: getDynamicDateRangeFromString("d_last_month"),
label: () =>
getDynamicDateRangeFromString("d_last_month")
.value()[0]
.format("MMMM"),
},
{
name: "Last year",
value: getDynamicDateRangeFromString("d_last_year"),
label: () =>
getDynamicDateRangeFromString("d_last_year")
.value()[0]
.format("YYYY"),
},
{
name: "Last 7 days",
value: getDynamicDateRangeFromString("d_last_7_days"),
label: () =>
getDynamicDateRangeFromString("d_last_7_days")
.value()[0]
.format("MMM D") + " - Today",
},
{
name: "Last 14 days",
value: getDynamicDateRangeFromString("d_last_14_days"),
label: () =>
getDynamicDateRangeFromString("d_last_14_days")
.value()[0]
.format("MMM D") + " - Today",
},
{
name: "Last 30 days",
value: getDynamicDateRangeFromString("d_last_30_days"),
label: () =>
getDynamicDateRangeFromString("d_last_30_days")
.value()[0]
.format("MMM D") + " - Today",
},
{
name: "Last 60 days",
value: getDynamicDateRangeFromString("d_last_60_days"),
label: () =>
getDynamicDateRangeFromString("d_last_60_days")
.value()[0]
.format("MMM D") + " - Today",
},
{
name: "Last 90 days",
value: getDynamicDateRangeFromString("d_last_90_days"),
label: () =>
getDynamicDateRangeFromString("d_last_90_days")
.value()[0]
.format("MMM D") + " - Today",
},
];
const DYNAMIC_DATETIME_OPTIONS = [
{ name: 'Today',
value: getDynamicDateRangeFromString('d_today'),
label: () => getDynamicDateRangeFromString('d_today').value()[0].format('MMM D') },
{ name: 'Yesterday',
value: getDynamicDateRangeFromString('d_yesterday'),
label: () => getDynamicDateRangeFromString('d_yesterday').value()[0].format('MMM D') },
{
name: "Today",
value: getDynamicDateRangeFromString("d_today"),
label: () =>
getDynamicDateRangeFromString("d_today")
.value()[0]
.format("MMM D"),
},
{
name: "Yesterday",
value: getDynamicDateRangeFromString("d_yesterday"),
label: () =>
getDynamicDateRangeFromString("d_yesterday")
.value()[0]
.format("MMM D"),
},
...DYNAMIC_DATE_OPTIONS,
];
const widthByType = {
'date-range': 294,
'datetime-range': 352,
'datetime-range-with-seconds': 382,
"date-range": 294,
"datetime-range": 352,
"datetime-range-with-seconds": 382,
};
function isValidDateRangeValue(value) {
@@ -78,8 +149,8 @@ class DateRangeParameter extends React.Component {
};
static defaultProps = {
type: '',
className: '',
type: "",
className: "",
value: null,
parameter: null,
onSelect: () => {},
@@ -90,9 +161,9 @@ class DateRangeParameter extends React.Component {
this.dateRangeComponentRef = React.createRef();
}
onDynamicValueSelect = (dynamicValue) => {
onDynamicValueSelect = dynamicValue => {
const { onSelect, parameter } = this.props;
if (dynamicValue === 'static') {
if (dynamicValue === "static") {
const parameterValue = parameter.getExecutionValue();
if (isObject(parameterValue) && parameterValue.start && parameterValue.end) {
onSelect([moment(parameterValue.start), moment(parameterValue.end)]);
@@ -108,7 +179,7 @@ class DateRangeParameter extends React.Component {
render() {
const { type, value, onSelect, className } = this.props;
const isDateTimeRange = includes(type, 'datetime-range');
const isDateTimeRange = includes(type, "datetime-range");
const hasDynamicValue = isDynamicDateRange(value);
const options = isDateTimeRange ? DYNAMIC_DATETIME_OPTIONS : DYNAMIC_DATE_OPTIONS;
@@ -117,7 +188,7 @@ class DateRangeParameter extends React.Component {
let DateRangeComponent = DateRangeInput;
if (isDateTimeRange) {
DateRangeComponent = DateTimeRangeInput;
if (includes(type, 'with-seconds')) {
if (includes(type, "with-seconds")) {
additionalAttributes.withSeconds = true;
}
}
@@ -134,17 +205,17 @@ class DateRangeParameter extends React.Component {
return (
<DateRangeComponent
ref={this.dateRangeComponentRef}
className={classNames('redash-datepicker date-range-input', { 'dynamic-value': hasDynamicValue }, className)}
className={classNames("redash-datepicker date-range-input", { "dynamic-value": hasDynamicValue }, className)}
onSelect={onSelect}
style={{ width: hasDynamicValue ? 195 : widthByType[type] }}
suffixIcon={(
suffixIcon={
<DynamicButton
options={options}
selectedDynamicValue={hasDynamicValue ? value : null}
enabled={hasDynamicValue}
onSelect={this.onDynamicValueSelect}
/>
)}
}
{...additionalAttributes}
/>
);

View File

@@ -1,14 +1,14 @@
import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { isFunction, get, findIndex } from 'lodash';
import Dropdown from 'antd/lib/dropdown';
import Icon from 'antd/lib/icon';
import Menu from 'antd/lib/menu';
import Typography from 'antd/lib/typography';
import { DynamicDateType } from '@/services/parameters/DateParameter';
import { DynamicDateRangeType } from '@/services/parameters/DateRangeParameter';
import React, { useRef } from "react";
import PropTypes from "prop-types";
import { isFunction, get, findIndex } from "lodash";
import Dropdown from "antd/lib/dropdown";
import Icon from "antd/lib/icon";
import Menu from "antd/lib/menu";
import Typography from "antd/lib/typography";
import { DynamicDateType } from "@/services/parameters/DateParameter";
import { DynamicDateRangeType } from "@/services/parameters/DateRangeParameter";
import './DynamicButton.less';
import "./DynamicButton.less";
const { Text } = Typography;
@@ -16,22 +16,20 @@ function DynamicButton({ options, selectedDynamicValue, onSelect, enabled }) {
const menu = (
<Menu
className="dynamic-menu"
onClick={({ key }) => onSelect(get(options, key, 'static'))}
onClick={({ key }) => onSelect(get(options, key, "static"))}
selectedKeys={[`${findIndex(options, { value: selectedDynamicValue })}`]}
data-test="DynamicButtonMenu"
>
data-test="DynamicButtonMenu">
{options.map((option, index) => (
// eslint-disable-next-line react/no-array-index-key
<Menu.Item key={index}>
{option.name} {option.label && (
<em>{isFunction(option.label) ? option.label() : option.label}</em>
)}
{option.name} {option.label && <em>{isFunction(option.label) ? option.label() : option.label}</em>}
</Menu.Item>
))}
{enabled && <Menu.Divider />}
{enabled && (
<Menu.Item>
<Icon type="arrow-left" /><Text type="secondary">Back to Static Value</Text>
<Icon type="arrow-left" />
<Text type="secondary">Back to Static Value</Text>
</Menu.Item>
)}
</Menu>
@@ -46,14 +44,8 @@ function DynamicButton({ options, selectedDynamicValue, onSelect, enabled }) {
overlay={menu}
className="dynamic-button"
placement="bottomRight"
trigger={['click']}
icon={(
<Icon
type="thunderbolt"
theme={enabled ? 'twoTone' : 'outlined'}
className="dynamic-icon"
/>
)}
trigger={["click"]}
icon={<Icon type="thunderbolt" theme={enabled ? "twoTone" : "outlined"} className="dynamic-icon" />}
getPopupContainer={() => containerRef.current}
data-test="DynamicButton"
/>

View File

@@ -1,11 +1,11 @@
import { keys, some } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import CreateDashboardDialog from '@/components/dashboards/CreateDashboardDialog';
import { currentUser } from '@/services/auth';
import organizationStatus from '@/services/organizationStatus';
import './empty-state.less';
import { keys, some } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
import { currentUser } from "@/services/auth";
import organizationStatus from "@/services/organizationStatus";
import "./empty-state.less";
function Step({ show, completed, text, url, urlText, onClick }) {
if (!show) {
@@ -16,7 +16,7 @@ function Step({ show, completed, text, url, urlText, onClick }) {
<li className={classNames({ done: completed })}>
<a href={url} onClick={onClick}>
{urlText}
</a>{' '}
</a>{" "}
{text}
</li>
);
@@ -80,8 +80,8 @@ function EmptyState({
</h2>
<p>{description}</p>
<img
src={'/static/images/illustrations/' + illustration + '.svg'}
alt={illustration + ' Illustration'}
src={"/static/images/illustrations/" + illustration + ".svg"}
alt={illustration + " Illustration"}
width="75%"
/>
</div>
@@ -134,7 +134,7 @@ function EmptyState({
/>
</ol>
<p>
Need more support?{' '}
Need more support?{" "}
<a href={helpLink} target="_blank" rel="noopener noreferrer">
See our Help
<i className="fa fa-external-link m-l-5" aria-hidden="true" />

View File

@@ -1,8 +1,8 @@
import React from 'react';
import Modal from 'antd/lib/modal';
import Input from 'antd/lib/input';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { Group } from '@/services/group';
import React from "react";
import Modal from "antd/lib/modal";
import Input from "antd/lib/input";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { Group } from "@/services/group";
class CreateGroupDialog extends React.Component {
static propTypes = {
@@ -10,13 +10,15 @@ class CreateGroupDialog extends React.Component {
};
state = {
name: '',
name: "",
};
save = () => {
this.props.dialog.close(new Group({
this.props.dialog.close(
new Group({
name: this.state.name,
}));
})
);
};
render() {

View File

@@ -1,21 +1,21 @@
import { isString } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Modal from 'antd/lib/modal';
import Tooltip from 'antd/lib/tooltip';
import notification from '@/services/notification';
import { isString } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Modal from "antd/lib/modal";
import Tooltip from "antd/lib/tooltip";
import notification from "@/services/notification";
function deleteGroup(event, group, onGroupDeleted) {
Modal.confirm({
title: 'Delete Group',
content: 'Are you sure you want to delete this group?',
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
title: "Delete Group",
content: "Are you sure you want to delete this group?",
okText: "Yes",
okType: "danger",
cancelText: "No",
onOk: () => {
group.$delete(() => {
notification.success('Group deleted successfully.');
notification.success("Group deleted successfully.");
onGroupDeleted();
});
},
@@ -27,11 +27,17 @@ export default function DeleteGroupButton({ group, title, onClick, children, ...
return null;
}
const button = (
<Button {...props} type="danger" onClick={event => deleteGroup(event, group, onClick)}>{children}</Button>
<Button {...props} type="danger" onClick={event => deleteGroup(event, group, onClick)}>
{children}
</Button>
);
if (isString(title) && (title !== '')) {
return <Tooltip placement="top" title={title} mouseLeaveDelay={0}>{button}</Tooltip>;
if (isString(title) && title !== "") {
return (
<Tooltip placement="top" title={title} mouseLeaveDelay={0}>
{button}
</Tooltip>
);
}
return button;

View File

@@ -1,21 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Divider from 'antd/lib/divider';
import React from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Divider from "antd/lib/divider";
import * as Sidebar from '@/components/items-list/components/Sidebar';
import { ControllerType } from '@/components/items-list/ItemsList';
import DeleteGroupButton from './DeleteGroupButton';
import * as Sidebar from "@/components/items-list/components/Sidebar";
import { ControllerType } from "@/components/items-list/ItemsList";
import DeleteGroupButton from "./DeleteGroupButton";
import { currentUser } from '@/services/auth';
import { currentUser } from "@/services/auth";
export default function DetailsPageSidebar({
controller, group, items,
canAddMembers, onAddMembersClick,
canAddDataSources, onAddDataSourcesClick,
controller,
group,
items,
canAddMembers,
onAddMembersClick,
canAddDataSources,
onAddDataSourcesClick,
onGroupDeleted,
}) {
const canRemove = group && currentUser.isAdmin && (group.type !== 'builtin');
const canRemove = group && currentUser.isAdmin && group.type !== "builtin";
return (
<React.Fragment>
@@ -28,18 +32,22 @@ export default function DetailsPageSidebar({
/>
{canAddMembers && (
<Button className="w-100 m-t-5" type="primary" onClick={onAddMembersClick}>
<i className="fa fa-plus m-r-5" />Add Members
<i className="fa fa-plus m-r-5" />
Add Members
</Button>
)}
{canAddDataSources && (
<Button className="w-100 m-t-5" type="primary" onClick={onAddDataSourcesClick}>
<i className="fa fa-plus m-r-5" />Add Data Sources
<i className="fa fa-plus m-r-5" />
Add Data Sources
</Button>
)}
{canRemove && (
<React.Fragment>
<Divider dashed className="m-t-10 m-b-10" />
<DeleteGroupButton className="w-100" group={group} onClick={onGroupDeleted}>Delete Group</DeleteGroupButton>
<DeleteGroupButton className="w-100" group={group} onClick={onGroupDeleted}>
Delete Group
</DeleteGroupButton>
</React.Fragment>
)}
</React.Fragment>

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { EditInPlace } from '@/components/EditInPlace';
import { currentUser } from '@/services/auth';
import React from "react";
import PropTypes from "prop-types";
import { EditInPlace } from "@/components/EditInPlace";
import { currentUser } from "@/services/auth";
function updateGroupName(group, name, onChange) {
group.name = name;
@@ -14,7 +14,7 @@ export default function GroupName({ group, onChange, ...props }) {
return null;
}
const canEdit = currentUser.isAdmin && (group.type !== 'builtin');
const canEdit = currentUser.isAdmin && group.type !== "builtin";
return (
<h3 {...props}>

View File

@@ -1,13 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import Tooltip from 'antd/lib/tooltip';
import React from "react";
import PropTypes from "prop-types";
import Tooltip from "antd/lib/tooltip";
export default function ListItemAddon({ isSelected, isStaged, alreadyInGroup, deselectedIcon }) {
if (isStaged) {
return <i className="fa fa-remove" />;
}
if (alreadyInGroup) {
return <Tooltip title="Already selected"><i className="fa fa-check" /></Tooltip>;
return (
<Tooltip title="Already selected">
<i className="fa fa-check" />
</Tooltip>
);
}
return isSelected ? <i className="fa fa-check" /> : <i className={`fa ${deselectedIcon}`} />;
}
@@ -23,5 +27,5 @@ ListItemAddon.defaultProps = {
isSelected: false,
isStaged: false,
alreadyInGroup: false,
deselectedIcon: 'fa-angle-double-right',
deselectedIcon: "fa-angle-double-right",
};

View File

@@ -1,10 +1,10 @@
import { omit, debounce } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { $route, $routeParams } from '@/services/ng';
import { clientConfig } from '@/services/auth';
import { StateStorage } from './classes/StateStorage';
import { omit, debounce } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import hoistNonReactStatics from "hoist-non-react-statics";
import { $route, $routeParams } from "@/services/ng";
import { clientConfig } from "@/services/auth";
import { StateStorage } from "./classes/StateStorage";
export const ControllerType = PropTypes.shape({
// values of props declared by wrapped component, current route's locals (`resolve: { ... }`) and title
@@ -40,16 +40,18 @@ export const ControllerType = PropTypes.shape({
export function wrap(WrappedComponent, itemsSource, stateStorage) {
class ItemsListWrapper extends React.Component {
static propTypes = {
...omit(WrappedComponent.propTypes, ['controller']),
...omit(WrappedComponent.propTypes, ["controller"]),
onError: PropTypes.func,
children: PropTypes.node,
};
static defaultProps = {
...omit(WrappedComponent.defaultProps, ['controller']),
onError: (error) => {
...omit(WrappedComponent.defaultProps, ["controller"]),
onError: error => {
// Allow calling chain to roll up, and then throw the error in global context
setTimeout(() => { throw error; });
setTimeout(() => {
throw error;
});
},
children: null,
};
@@ -107,10 +109,10 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
// ANGULAR_REMOVE_ME Revisit when some React router will be used
title: $route.current.title,
...$routeParams,
...omit($route.current.locals, ['$scope', '$template']),
...omit($route.current.locals, ["$scope", "$template"]),
// Add to params all props except of own ones
...omit(this.props, ['onError', 'children']),
...omit(this.props, ["onError", "children"]),
};
return {
...rest,
@@ -118,7 +120,7 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
params,
isLoaded,
isEmpty: !isLoaded || (totalCount === 0),
isEmpty: !isLoaded || totalCount === 0,
totalItemsCount: isLoaded ? totalCount : 0,
pageSizeOptions: clientConfig.pageSizeOptions,
pageItems: isLoaded ? pageItems : [],

View File

@@ -1,4 +1,4 @@
import { identity, isFunction, isNil, isString } from 'lodash';
import { identity, isFunction, isNil, isString } from "lodash";
class ItemsFetcher {
_getRequest(state, context) {
@@ -20,8 +20,7 @@ class ItemsFetcher {
fetch(changes, state, context) {
const request = this._getRequest(state, context);
return this._originalDoRequest(request, context)
.then(data => this._processResults(data, state, context));
return this._originalDoRequest(request, context).then(data => this._processResults(data, state, context));
}
}
@@ -31,10 +30,13 @@ export class PlainListFetcher extends ItemsFetcher {
_allItems = [];
_getRequest({ searchTerm, selectedTags }, context) {
return this._originalGetRequest({
q: isString(searchTerm) && (searchTerm !== '') ? searchTerm : undefined,
return this._originalGetRequest(
{
q: isString(searchTerm) && searchTerm !== "" ? searchTerm : undefined,
tags: selectedTags,
}, context);
},
context
);
}
_processResults(data, { paginator, sorter }, context) {
@@ -69,12 +71,15 @@ export class PlainListFetcher extends ItemsFetcher {
// items for current page and total items count)
export class PaginatedListFetcher extends ItemsFetcher {
_getRequest({ paginator, sorter, searchTerm, selectedTags }, context) {
return this._originalGetRequest({
return this._originalGetRequest(
{
page: paginator.page,
page_size: paginator.itemsPerPage,
order: sorter.compiled,
q: isString(searchTerm) && (searchTerm !== '') ? searchTerm : undefined,
q: isString(searchTerm) && searchTerm !== "" ? searchTerm : undefined,
tags: selectedTags,
}, context);
},
context
);
}
}

View File

@@ -1,8 +1,8 @@
import { isFunction, identity, map, extend } from 'lodash';
import Paginator from './Paginator';
import Sorter from './Sorter';
import PromiseRejectionError from '@/lib/promise-rejection-error';
import { PlainListFetcher, PaginatedListFetcher } from './ItemsFetcher';
import { isFunction, identity, map, extend } from "lodash";
import Paginator from "./Paginator";
import Sorter from "./Sorter";
import PromiseRejectionError from "@/lib/promise-rejection-error";
import { PlainListFetcher, PaginatedListFetcher } from "./ItemsFetcher";
export class ItemsSource {
onBeforeUpdate = null;
@@ -38,12 +38,13 @@ export class ItemsSource {
const customParams = {};
const context = {
...this.getCallbackContext(),
setCustomParams: (params) => {
setCustomParams: params => {
extend(customParams, params);
},
};
return this._beforeUpdate().then(() => (
this._fetcher.fetch(changes, state, context)
return this._beforeUpdate().then(() =>
this._fetcher
.fetch(changes, state, context)
.then(({ results, count, allResults }) => {
this._pageItems = results;
this._allItems = allResults || null;
@@ -51,10 +52,10 @@ export class ItemsSource {
this._params = { ...this._params, ...customParams };
return this._afterUpdate();
})
.catch((error) => {
.catch(error => {
this.handleError(error);
})
));
);
}
constructor({ getRequest, doRequest, processResults, isPlainList = false, ...defaultState }) {
@@ -62,9 +63,9 @@ export class ItemsSource {
getRequest = identity;
}
this._fetcher = isPlainList ?
new PlainListFetcher({ getRequest, doRequest, processResults }) :
new PaginatedListFetcher({ getRequest, doRequest, processResults });
this._fetcher = isPlainList
? new PlainListFetcher({ getRequest, doRequest, processResults })
: new PaginatedListFetcher({ getRequest, doRequest, processResults });
this.setState(defaultState);
this._pageItems = [];
@@ -91,7 +92,7 @@ export class ItemsSource {
this._paginator = new Paginator(state);
this._sorter = new Sorter(state);
this._searchTerm = state.searchTerm || '';
this._searchTerm = state.searchTerm || "";
this._selectedTags = state.selectedTags || [];
this._savedOrderByField = this._sorter.field;
@@ -109,19 +110,19 @@ export class ItemsSource {
});
};
toggleSorting = (orderByField) => {
toggleSorting = orderByField => {
this._sorter.toggleField(orderByField);
this._savedOrderByField = this._sorter.field;
this._changed({ sorting: true });
};
updateSearch = (searchTerm) => {
updateSearch = searchTerm => {
// here we update state directly, but later `fetchData` will update it properly
this._searchTerm = searchTerm;
// in search mode ignore the ordering and use the ranking order
// provided by the server-side FTS backend instead, unless it was
// requested by the user by actively ordering in search mode
if (searchTerm === '') {
if (searchTerm === "") {
this._sorter.setField(this._savedOrderByField); // restore ordering
} else {
this._sorter.setField(null);
@@ -130,7 +131,7 @@ export class ItemsSource {
this._changed({ search: true, pagination: { page: true } });
};
updateSelectedTags = (selectedTags) => {
updateSelectedTags = selectedTags => {
this._selectedTags = selectedTags;
this._paginator.setPage(1);
this._changed({ tags: true, pagination: { page: true } });
@@ -138,7 +139,7 @@ export class ItemsSource {
update = () => this._changed();
handleError = (error) => {
handleError = error => {
if (isFunction(this.onError)) {
// ANGULAR_REMOVE_ME This code is related to Angular's HTTP services
if (error.status && error.data) {

View File

@@ -1,4 +1,4 @@
import { isUndefined } from 'lodash';
import { isUndefined } from "lodash";
export default class Paginator {
page = 1;
@@ -17,9 +17,9 @@ export default class Paginator {
}
value = parseInt(value, 10) || 1;
if (validate) {
this.page = ((value >= 1) && (value <= this.totalPages)) ? value : 1;
this.page = value >= 1 && value <= this.totalPages ? value : 1;
} else {
this.page = (value >= 1) ? value : 1;
this.page = value >= 1 ? value : 1;
}
}
@@ -28,7 +28,7 @@ export default class Paginator {
return;
}
value = parseInt(value, 10) || 20;
this.itemsPerPage = (value >= 1) ? value : 1;
this.itemsPerPage = value >= 1 ? value : 1;
if (validate) {
this.setPage(this.page, validate);
}

View File

@@ -1,6 +1,6 @@
import { isString, sortBy } from 'lodash';
import { isString, sortBy } from "lodash";
const ORDER_BY_REVERSE = '-';
const ORDER_BY_REVERSE = "-";
export function compile(field, reverse) {
if (!field) {
@@ -10,12 +10,12 @@ export function compile(field, reverse) {
}
export function parse(compiled) {
compiled = isString(compiled) ? compiled : '';
compiled = isString(compiled) ? compiled : "";
const reverse = compiled.startsWith(ORDER_BY_REVERSE);
if (reverse) {
compiled = compiled.substring(1);
}
const field = compiled !== '' ? compiled : null;
const field = compiled !== "" ? compiled : null;
return { field, reverse };
}
@@ -35,7 +35,7 @@ export default class Sorter {
}
setField(value) {
this.field = isString(value) && (value !== '') ? value : null;
this.field = isString(value) && value !== "" ? value : null;
}
setReverse(value) {
@@ -48,7 +48,7 @@ export default class Sorter {
}
toggleField(field) {
if (!isString(field) || (field === '')) {
if (!isString(field) || field === "") {
return;
}
if (field === this.field) {

View File

@@ -1,7 +1,7 @@
import { defaults } from 'lodash';
import { clientConfig } from '@/services/auth';
import { $location } from '@/services/ng';
import { parse as parseOrderBy, compile as compileOrderBy } from './Sorter';
import { defaults } from "lodash";
import { clientConfig } from "@/services/auth";
import { $location } from "@/services/ng";
import { parse as parseOrderBy, compile as compileOrderBy } from "./Sorter";
export class StateStorage {
constructor(state = {}) {
@@ -12,16 +12,15 @@ export class StateStorage {
return defaults(this._state, {
page: 1,
itemsPerPage: clientConfig.pageSize,
orderByField: 'created_at',
orderByField: "created_at",
orderByReverse: false,
searchTerm: '',
searchTerm: "",
tags: [],
});
}
// eslint-disable-next-line class-methods-use-this
setState() {
}
setState() {}
}
export class UrlStateStorage extends StateStorage {
@@ -29,15 +28,13 @@ export class UrlStateStorage extends StateStorage {
const defaultState = super.getState();
const params = $location.search();
const searchTerm = params.q || '';
const searchTerm = params.q || "";
// in search mode order by should be explicitly specified in url, otherwise use default
const defaultOrderBy = searchTerm !== '' ? '' : compileOrderBy(defaultState.orderByField, defaultState.orderByReverse);
const defaultOrderBy =
searchTerm !== "" ? "" : compileOrderBy(defaultState.orderByField, defaultState.orderByReverse);
const {
field: orderByField,
reverse: orderByReverse,
} = parseOrderBy(params.order || defaultOrderBy);
const { field: orderByField, reverse: orderByReverse } = parseOrderBy(params.order || defaultOrderBy);
return {
page: parseInt(params.page, 10) || defaultState.page,
@@ -54,7 +51,7 @@ export class UrlStateStorage extends StateStorage {
page,
page_size: itemsPerPage,
order: compileOrderBy(orderByField, orderByReverse),
q: searchTerm !== '' ? searchTerm : null,
q: searchTerm !== "" ? searchTerm : null,
});
}
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { BigMessage } from '@/components/BigMessage';
import React from "react";
import { BigMessage } from "@/components/BigMessage";
// Default "list empty" message for list pages
export default function EmptyState(props) {

View File

@@ -1,12 +1,12 @@
import { isFunction, map, filter, extend, omit, identity } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Table from 'antd/lib/table';
import { FavoritesControl } from '@/components/FavoritesControl';
import { TimeAgo } from '@/components/TimeAgo';
import { durationHumanize } from '@/filters';
import { formatDate, formatDateTime } from '@/filters/datetime';
import { isFunction, map, filter, extend, omit, identity } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Table from "antd/lib/table";
import { FavoritesControl } from "@/components/FavoritesControl";
import { TimeAgo } from "@/components/TimeAgo";
import { durationHumanize } from "@/filters";
import { formatDate, formatDateTime } from "@/filters/datetime";
// `this` refers to previous function in the chain (`Columns.***`).
// Adds `sorter: true` field to column definition
@@ -16,15 +16,19 @@ function sortable(...args) {
export const Columns = {
favorites(overrides) {
return extend({
width: '1%',
return extend(
{
width: "1%",
render: (text, item) => <FavoritesControl item={item} />,
}, overrides);
},
overrides
);
},
avatar(overrides, formatTitle) {
formatTitle = isFunction(formatTitle) ? formatTitle : identity;
return extend({
width: '1%',
return extend(
{
width: "1%",
render: (user, item) => (
<img
src={item.user.profile_image_url}
@@ -33,34 +37,51 @@ export const Columns = {
title={formatTitle(user.name, item)}
/>
),
}, overrides);
},
overrides
);
},
date(overrides) {
return extend({
return extend(
{
render: text => formatDate(text),
}, overrides);
},
overrides
);
},
dateTime(overrides) {
return extend({
return extend(
{
render: text => formatDateTime(text),
}, overrides);
},
overrides
);
},
duration(overrides) {
return extend({
width: '1%',
className: 'text-nowrap',
return extend(
{
width: "1%",
className: "text-nowrap",
render: text => durationHumanize(text),
}, overrides);
},
overrides
);
},
timeAgo(overrides) {
return extend({
return extend(
{
render: value => <TimeAgo date={value} />,
}, overrides);
},
overrides
);
},
custom(render, overrides) {
return extend({
return extend(
{
render,
}, overrides);
},
overrides
);
},
};
@@ -75,12 +96,14 @@ export default class ItemsTable extends React.Component {
loading: PropTypes.bool,
// eslint-disable-next-line react/forbid-prop-types
items: PropTypes.arrayOf(PropTypes.object),
columns: PropTypes.arrayOf(PropTypes.shape({
columns: PropTypes.arrayOf(
PropTypes.shape({
field: PropTypes.string, // data field
orderByField: PropTypes.string, // field to order by (defaults to `field`)
render: PropTypes.func, // (prop, item) => text | node; `prop` is `item[field]`
isAvailable: PropTypes.func, // return `true` to show column and `false` to hide; if omitted: show column
})),
})
),
showHeader: PropTypes.bool,
onRowClick: PropTypes.func, // (event, item) => void
@@ -103,55 +126,49 @@ export default class ItemsTable extends React.Component {
prepareColumns() {
const { orderByField, orderByReverse, toggleSorting } = this.props;
const orderByDirection = orderByReverse ? 'descend' : 'ascend';
const orderByDirection = orderByReverse ? "descend" : "ascend";
return map(
map(
filter(this.props.columns, column => (isFunction(column.isAvailable) ? column.isAvailable() : true)),
column => extend(column, { orderByField: column.orderByField || column.field }),
column => extend(column, { orderByField: column.orderByField || column.field })
),
(column, index) => {
// Bind click events only to sortable columns
const onHeaderCell = column.sorter ? (
() => ({ onClick: () => toggleSorting(column.orderByField) })
) : null;
const onHeaderCell = column.sorter ? () => ({ onClick: () => toggleSorting(column.orderByField) }) : null;
// Wrap render function to pass correct arguments
const render = isFunction(column.render) ? (text, row) => column.render(text, row.item) : identity;
return extend(
omit(column, ['field', 'orderByField', 'render']),
{
key: 'column' + index,
dataIndex: 'item[' + JSON.stringify(column.field) + ']',
return extend(omit(column, ["field", "orderByField", "render"]), {
key: "column" + index,
dataIndex: "item[" + JSON.stringify(column.field) + "]",
defaultSortOrder: column.orderByField === orderByField ? orderByDirection : null,
onHeaderCell,
render,
},
);
},
});
}
);
}
render() {
const columns = this.prepareColumns();
const rows = map(
this.props.items,
(item, index) => ({ key: 'row' + index, item }),
);
const rows = map(this.props.items, (item, index) => ({ key: "row" + index, item }));
// Bind events only if `onRowClick` specified
const onTableRow = isFunction(this.props.onRowClick) ? (
row => ({
onClick: (event) => { this.props.onRowClick(event, row.item); },
const onTableRow = isFunction(this.props.onRowClick)
? row => ({
onClick: event => {
this.props.onRowClick(event, row.item);
},
})
) : null;
: null;
const { showHeader } = this.props;
return (
<Table
className={classNames('table-data', { 'ant-table-headerless': !showHeader })}
className={classNames("table-data", { "ant-table-headerless": !showHeader })}
loading={this.props.loading}
columns={columns}
showHeader={showHeader}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { BigMessage } from '@/components/BigMessage';
import React from "react";
import { BigMessage } from "@/components/BigMessage";
// Default "loading" message for list pages
export default function LoadingState(props) {

View File

@@ -1,10 +1,10 @@
import { isFunction, isString, filter, map } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Input from 'antd/lib/input';
import AntdMenu from 'antd/lib/menu';
import Select from 'antd/lib/select';
import { TagsList } from '@/components/TagsList';
import { isFunction, isString, filter, map } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Input from "antd/lib/input";
import AntdMenu from "antd/lib/menu";
import Select from "antd/lib/select";
import { TagsList } from "@/components/TagsList";
/*
SearchInput
@@ -33,7 +33,7 @@ SearchInput.propTypes = {
};
SearchInput.defaultProps = {
placeholder: 'Search...',
placeholder: "Search...",
showIcon: false,
};
@@ -42,10 +42,7 @@ SearchInput.defaultProps = {
*/
export function Menu({ items, selected }) {
items = filter(
items,
item => (isFunction(item.isAvailable) ? item.isAvailable() : true),
);
items = filter(items, item => (isFunction(item.isAvailable) ? item.isAvailable() : true));
if (items.length === 0) {
return null;
}
@@ -55,10 +52,11 @@ export function Menu({ items, selected }) {
{map(items, item => (
<AntdMenu.Item key={item.key} className="m-0">
<a href={item.href}>
{
isString(item.icon) && (item.icon !== '') &&
<span className="btn-favourite m-r-5"><i className={item.icon} aria-hidden="true" /></span>
}
{isString(item.icon) && item.icon !== "" && (
<span className="btn-favourite m-r-5">
<i className={item.icon} aria-hidden="true" />
</span>
)}
{isFunction(item.icon) && (item.icon(item) || null)}
{item.title}
</a>
@@ -70,13 +68,15 @@ export function Menu({ items, selected }) {
}
Menu.propTypes = {
items: PropTypes.arrayOf(PropTypes.shape({
items: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string.isRequired,
href: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
icon: PropTypes.func, // function to render icon
isAvailable: PropTypes.func, // return `true` to show item and `false` to hide; if omitted: show item
})),
})
),
selected: PropTypes.string,
};
@@ -90,7 +90,11 @@ Menu.defaultProps = {
*/
export function MenuIcon({ icon }) {
return <span className="btn-favourite m-r-5"><i className={icon} aria-hidden="true" /></span>;
return (
<span className="btn-favourite m-r-5">
<i className={icon} aria-hidden="true" />
</span>
);
}
MenuIcon.propTypes = {
@@ -102,7 +106,7 @@ MenuIcon.propTypes = {
*/
export function ProfileImage({ user }) {
if (!isString(user.profile_image_url) || (user.profile_image_url === '')) {
if (!isString(user.profile_image_url) || user.profile_image_url === "") {
return null;
}
return <img src={user.profile_image_url} className="profile__image--sidebar m-r-5" width="13" alt={user.name} />;
@@ -120,7 +124,7 @@ ProfileImage.propTypes = {
*/
export function Tags({ url, onChange }) {
if (url === '') {
if (url === "") {
return null;
}
return (
@@ -142,13 +146,11 @@ Tags.propTypes = {
export function PageSizeSelect({ options, value, onChange, ...props }) {
return (
<div {...props}>
<Select
className="w-100"
defaultValue={value}
onChange={onChange}
>
<Select className="w-100" defaultValue={value} onChange={onChange}>
{map(options, option => (
<Select.Option key={option} value={option}>{ option } results</Select.Option>
<Select.Option key={option} value={option}>
{option} results
</Select.Option>
))}
</Select>
</div>

View File

@@ -1,11 +1,11 @@
/* eslint-disable react/prop-types */
import { isFinite, isString, isArray, isObject, keys, map } from 'lodash';
import React, { useState } from 'react';
import cx from 'classnames';
import PropTypes from 'prop-types';
import { isFinite, isString, isArray, isObject, keys, map } from "lodash";
import React, { useState } from "react";
import cx from "classnames";
import PropTypes from "prop-types";
import './json-view-interactive.less';
import "./json-view-interactive.less";
function JsonBlock({ value, children, openingBrace, closingBrace, withKeys }) {
const [isExpanded, setIsExpanded] = useState(false);
@@ -15,14 +15,16 @@ function JsonBlock({ value, children, openingBrace, closingBrace, withKeys }) {
return (
<React.Fragment>
{(count > 0) && (
{count > 0 && (
<span className="jvi-toggle" onClick={() => setIsExpanded(!isExpanded)}>
<i className={cx('fa', { 'fa-caret-right': !isExpanded, 'fa-caret-down': isExpanded })} />
<i className={cx("fa", { "fa-caret-right": !isExpanded, "fa-caret-down": isExpanded })} />
</span>
)}
<span className="jvi-punctuation jvi-braces">{openingBrace}</span>
{!isExpanded && (count > 0) && (
<span className="jvi-punctuation jvi-ellipsis" onClick={() => setIsExpanded(true)}>&hellip;</span>
{!isExpanded && count > 0 && (
<span className="jvi-punctuation jvi-ellipsis" onClick={() => setIsExpanded(true)}>
&hellip;
</span>
)}
{isExpanded && (
<span className="jvi-block">
@@ -32,9 +34,8 @@ function JsonBlock({ value, children, openingBrace, closingBrace, withKeys }) {
const comma = isLast ? null : <span className="jvi-punctuation jvi-comma">,</span>;
return (
<span
key={'item-' + key}
className={cx('jvi-item', { 'jvi-nested-first': isFirst, 'jvi-nested-last': isLast })}
>
key={"item-" + key}
className={cx("jvi-item", { "jvi-nested-first": isFirst, "jvi-nested-last": isLast })}>
{withKeys && (
<span className="jvi-object-key">
<JsonValue value={key}>
@@ -50,18 +51,16 @@ function JsonBlock({ value, children, openingBrace, closingBrace, withKeys }) {
)}
<span className="jvi-punctuation jvi-braces">{closingBrace}</span>
{children}
{!isExpanded && (
<span className="jvi-comment">{' // ' + count + ' ' + (count === 1 ? 'item' : 'items')}</span>
)}
{!isExpanded && <span className="jvi-comment">{" // " + count + " " + (count === 1 ? "item" : "items")}</span>}
</React.Fragment>
);
}
function JsonValue({ value, children }) {
if ((value === null) || (value === false) || (value === true) || isFinite(value)) {
if (value === null || value === false || value === true || isFinite(value)) {
return (
<span className="jvi-value jvi-primitive">
{'' + value}
{"" + value}
{children}
</span>
);
@@ -77,10 +76,18 @@ function JsonValue({ value, children }) {
);
}
if (isArray(value)) {
return <JsonBlock value={value} openingBrace="[" closingBrace="]">{children}</JsonBlock>;
return (
<JsonBlock value={value} openingBrace="[" closingBrace="]">
{children}
</JsonBlock>
);
}
if (isObject(value)) {
return <JsonBlock value={value} openingBrace="{" closingBrace="}" withKeys>{children}</JsonBlock>;
return (
<JsonBlock value={value} openingBrace="{" closingBrace="}" withKeys>
{children}
</JsonBlock>
);
}
return null;
}

View File

@@ -1,15 +1,15 @@
import { map } from 'lodash';
import { map } from "lodash";
function buildTableColumnKeywords(table) {
const keywords = [];
table.columns.forEach((column) => {
table.columns.forEach(column => {
keywords.push({
caption: column,
name: `${table.name}.${column}`,
value: `${table.name}.${column}`,
score: 100,
meta: 'Column',
className: 'completion',
meta: "Column",
className: "completion",
});
});
return keywords;
@@ -20,16 +20,16 @@ function buildKeywordsFromSchema(schema) {
const columnKeywords = {};
const tableColumnKeywords = {};
schema.forEach((table) => {
schema.forEach(table => {
tableKeywords.push({
name: table.name,
value: table.name,
score: 100,
meta: 'Table',
meta: "Table",
});
tableColumnKeywords[table.name] = buildTableColumnKeywords(table);
table.columns.forEach((c) => {
columnKeywords[c] = 'Column';
table.columns.forEach(c => {
columnKeywords[c] = "Column";
});
});

View File

@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import './content-with-sidebar.less';
import "./content-with-sidebar.less";
const propTypes = {
className: PropTypes.string,
@@ -18,7 +18,7 @@ const defaultProps = {
function Sidebar({ className, children, ...props }) {
return (
<div className={classNames('layout-sidebar', className)} {...props}>
<div className={classNames("layout-sidebar", className)} {...props}>
<div>{children}</div>
</div>
);
@@ -31,7 +31,7 @@ Sidebar.defaultProps = defaultProps;
function Content({ className, children, ...props }) {
return (
<div className={classNames('layout-content', className)} {...props}>
<div className={classNames("layout-content", className)} {...props}>
<div>{children}</div>
</div>
);
@@ -43,7 +43,11 @@ Content.defaultProps = defaultProps;
// Layout
export default function Layout({ className, children, ...props }) {
return <div className={classNames('layout-with-sidebar', className)} {...props}>{children}</div>;
return (
<div className={classNames("layout-with-sidebar", className)} {...props}>
{children}
</div>
);
}
Layout.propTypes = propTypes;

View File

@@ -12,7 +12,7 @@ const Overlay = {
};
export default function init(ngModule) {
ngModule.component('overlay', Overlay);
ngModule.component("overlay", Overlay);
}
init.init = true;

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