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

@@ -7,4 +7,4 @@ module.exports = {
rules: { rules: {
"jest/no-focused-tests": "off", "jest/no-focused-tests": "off",
}, },
}; };

View File

@@ -1,4 +1,4 @@
import { configure } from 'enzyme'; import { configure } from "enzyme";
import Adapter from 'enzyme-adapter-react-16'; import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() }); 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); MockDate.set(date);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
// ANGULAR_REMOVE_ME // 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) { export default function init(ngModule) {
ngModule.component('colorBox', react2angular(ColorPicker.Swatch)); ngModule.component("colorBox", react2angular(ColorPicker.Swatch));
} }
init.init = true; init.init = true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,25 @@
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { react2angular } from 'react2angular'; import { react2angular } from "react2angular";
import { BigMessage } from '@/components/BigMessage'; import { BigMessage } from "@/components/BigMessage";
import { TagsControl } from '@/components/tags-control/TagsControl'; import { TagsControl } from "@/components/tags-control/TagsControl";
export function NoTaggedObjectsFound({ objectType, tags }) { export function NoTaggedObjectsFound({ objectType, tags }) {
return ( return (
<BigMessage icon="fa-tags"> <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> </BigMessage>
); );
} }
NoTaggedObjectsFound.propTypes = { NoTaggedObjectsFound.propTypes = {
objectType: PropTypes.string.isRequired, objectType: PropTypes.string.isRequired,
tags: PropTypes.oneOfType([ tags: PropTypes.oneOfType([PropTypes.array, PropTypes.objectOf(Set)]).isRequired,
PropTypes.array,
PropTypes.objectOf(Set),
]).isRequired,
}; };
export default function init(ngModule) { export default function init(ngModule) {
ngModule.component('noTaggedObjectsFound', react2angular(NoTaggedObjectsFound)); ngModule.component("noTaggedObjectsFound", react2angular(NoTaggedObjectsFound));
} }
init.init = true; init.init = true;

View File

@@ -1,12 +1,12 @@
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { react2angular } from 'react2angular'; import { react2angular } from "react2angular";
export function PageHeader({ title }) { export function PageHeader({ title }) {
return ( return (
<div className="page-header-wrapper row p-l-15 p-r-15 m-b-10 m-l-0 m-r-0"> <div className="page-header-wrapper row p-l-15 p-r-15 m-b-10 m-l-0 m-r-0">
<div className="col-sm-9 p-l-0 p-r-0"> <div className="col-sm-9 p-l-0 p-r-0">
<h3>{ title }</h3> <h3>{title}</h3>
</div> </div>
</div> </div>
); );
@@ -17,7 +17,7 @@ PageHeader.propTypes = {
}; };
export default function init(ngModule) { export default function init(ngModule) {
ngModule.component('pageHeader', react2angular(PageHeader)); ngModule.component("pageHeader", react2angular(PageHeader));
} }
init.init = true; init.init = true;

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import Select from 'antd/lib/select'; import Select from "antd/lib/select";
import Input from 'antd/lib/input'; import Input from "antd/lib/input";
import InputNumber from 'antd/lib/input-number'; import InputNumber from "antd/lib/input-number";
import DateParameter from '@/components/dynamic-parameters/DateParameter'; import DateParameter from "@/components/dynamic-parameters/DateParameter";
import DateRangeParameter from '@/components/dynamic-parameters/DateRangeParameter'; import DateRangeParameter from "@/components/dynamic-parameters/DateRangeParameter";
import { isEqual } from 'lodash'; import { isEqual } from "lodash";
import { QueryBasedParameterInput } from './QueryBasedParameterInput'; import { QueryBasedParameterInput } from "./QueryBasedParameterInput";
import './ParameterValueInput.less'; import "./ParameterValueInput.less";
const { Option } = Select; const { Option } = Select;
@@ -30,13 +30,13 @@ class ParameterValueInput extends React.Component {
}; };
static defaultProps = { static defaultProps = {
type: 'text', type: "text",
value: null, value: null,
enumOptions: '', enumOptions: "",
queryId: null, queryId: null,
parameter: null, parameter: null,
onSelect: () => {}, onSelect: () => {},
className: '', className: "",
}; };
constructor(props) { constructor(props) {
@@ -47,7 +47,7 @@ class ParameterValueInput extends React.Component {
}; };
} }
componentDidUpdate = (prevProps) => { componentDidUpdate = prevProps => {
const { value, parameter } = this.props; const { value, parameter } = this.props;
// if value prop updated, reset dirty state // if value prop updated, reset dirty state
if (prevProps.value !== value || prevProps.parameter !== parameter) { if (prevProps.value !== value || prevProps.parameter !== parameter) {
@@ -56,13 +56,13 @@ class ParameterValueInput extends React.Component {
isDirty: parameter.hasPendingValue, isDirty: parameter.hasPendingValue,
}); });
} }
} };
onSelect = (value) => { onSelect = value => {
const isDirty = !isEqual(value, this.props.value); const isDirty = !isEqual(value, this.props.value);
this.setState({ value, isDirty }); this.setState({ value, isDirty });
this.props.onSelect(value, isDirty); this.props.onSelect(value, isDirty);
} };
renderDateParameter() { renderDateParameter() {
const { type, parameter } = this.props; const { type, parameter } = this.props;
@@ -95,13 +95,13 @@ class ParameterValueInput extends React.Component {
renderEnumInput() { renderEnumInput() {
const { enumOptions, parameter } = this.props; const { enumOptions, parameter } = this.props;
const { value } = this.state; const { value } = this.state;
const enumOptionsArray = enumOptions.split('\n').filter(v => v !== ''); const enumOptionsArray = enumOptions.split("\n").filter(v => v !== "");
// Antd Select doesn't handle null in multiple mode // Antd Select doesn't handle null in multiple mode
const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val); const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val);
return ( return (
<Select <Select
className={this.props.className} className={this.props.className}
mode={parameter.multiValuesOptions ? 'multiple' : 'default'} mode={parameter.multiValuesOptions ? "multiple" : "default"}
optionFilterProp="children" optionFilterProp="children"
disabled={enumOptionsArray.length === 0} disabled={enumOptionsArray.length === 0}
value={normalize(value)} value={normalize(value)}
@@ -111,9 +111,12 @@ class ParameterValueInput extends React.Component {
showArrow showArrow
style={{ minWidth: 60 }} style={{ minWidth: 60 }}
notFoundContent={null} notFoundContent={null}
{...multipleValuesProps} {...multipleValuesProps}>
> {enumOptionsArray.map(option => (
{enumOptionsArray.map(option => (<Option key={option} value={option}>{ option }</Option>))} <Option key={option} value={option}>
{option}
</Option>
))}
</Select> </Select>
); );
} }
@@ -124,7 +127,7 @@ class ParameterValueInput extends React.Component {
return ( return (
<QueryBasedParameterInput <QueryBasedParameterInput
className={this.props.className} className={this.props.className}
mode={parameter.multiValuesOptions ? 'multiple' : 'default'} mode={parameter.multiValuesOptions ? "multiple" : "default"}
optionFilterProp="children" optionFilterProp="children"
parameter={parameter} parameter={parameter}
value={value} value={value}
@@ -143,11 +146,7 @@ class ParameterValueInput extends React.Component {
const normalize = val => (isNaN(val) ? undefined : val); const normalize = val => (isNaN(val) ? undefined : val);
return ( return (
<InputNumber <InputNumber className={className} value={normalize(value)} onChange={val => this.onSelect(normalize(val))} />
className={className}
value={normalize(value)}
onChange={val => this.onSelect(normalize(val))}
/>
); );
} }
@@ -168,16 +167,22 @@ class ParameterValueInput extends React.Component {
renderInput() { renderInput() {
const { type } = this.props; const { type } = this.props;
switch (type) { switch (type) {
case 'datetime-with-seconds': case "datetime-with-seconds":
case 'datetime-local': case "datetime-local":
case 'date': return this.renderDateParameter(); case "date":
case 'datetime-range-with-seconds': return this.renderDateParameter();
case 'datetime-range': case "datetime-range-with-seconds":
case 'date-range': return this.renderDateRangeParameter(); case "datetime-range":
case 'enum': return this.renderEnumInput(); case "date-range":
case 'query': return this.renderQueryBasedInput(); return this.renderDateRangeParameter();
case 'number': return this.renderNumberInput(); case "enum":
default: return this.renderTextInput(); 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 React from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { size, filter, forEach, extend } from 'lodash'; import { size, filter, forEach, extend } from "lodash";
import { react2angular } from 'react2angular'; import { react2angular } from "react2angular";
import { SortableContainer, SortableElement, DragHandle } from '@/components/sortable'; import { SortableContainer, SortableElement, DragHandle } from "@/components/sortable";
import { $location } from '@/services/ng'; import { $location } from "@/services/ng";
import { Parameter } from '@/services/parameters'; import { Parameter } from "@/services/parameters";
import ParameterApplyButton from '@/components/ParameterApplyButton'; import ParameterApplyButton from "@/components/ParameterApplyButton";
import ParameterValueInput from '@/components/ParameterValueInput'; import ParameterValueInput from "@/components/ParameterValueInput";
import EditParameterSettingsDialog from './EditParameterSettingsDialog'; import EditParameterSettingsDialog from "./EditParameterSettingsDialog";
import { toHuman } from '@/filters'; import { toHuman } from "@/filters";
import './Parameters.less'; import "./Parameters.less";
function updateUrl(parameters) { function updateUrl(parameters) {
const params = extend({}, $location.search()); const params = extend({}, $location.search());
parameters.forEach((param) => { parameters.forEach(param => {
extend(params, param.toUrlParams()); extend(params, param.toUrlParams());
}); });
Object.keys(params).forEach(key => params[key] == null && delete params[key]); 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; const { parameters, disableUrlUpdate } = this.props;
if (prevProps.parameters !== parameters) { if (prevProps.parameters !== parameters) {
this.setState({ parameters }); this.setState({ parameters });
@@ -59,7 +59,7 @@ export class Parameters extends React.Component {
} }
}; };
handleKeyDown = (e) => { handleKeyDown = e => {
// Cmd/Ctrl/Alt + Enter // Cmd/Ctrl/Alt + Enter
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) {
e.stopPropagation(); e.stopPropagation();
@@ -106,26 +106,20 @@ export class Parameters extends React.Component {
showParameterSettings = (parameter, index) => { showParameterSettings = (parameter, index) => {
const { onParametersEdit } = this.props; const { onParametersEdit } = this.props;
EditParameterSettingsDialog EditParameterSettingsDialog.showModal({ parameter }).result.then(updated => {
.showModal({ parameter }) this.setState(({ parameters }) => {
.result.then((updated) => { const updatedParameter = extend(parameter, updated);
this.setState(({ parameters }) => { parameters[index] = Parameter.create(updatedParameter, updatedParameter.parentQueryId);
const updatedParameter = extend(parameter, updated); onParametersEdit();
parameters[index] = Parameter.create(updatedParameter, updatedParameter.parentQueryId); return { parameters };
onParametersEdit();
return { parameters };
});
}); });
});
}; };
renderParameter(param, index) { renderParameter(param, index) {
const { editable } = this.props; const { editable } = this.props;
return ( return (
<div <div key={param.name} className="di-block" data-test={`ParameterName-${param.name}`}>
key={param.name}
className="di-block"
data-test={`ParameterName-${param.name}`}
>
<div className="parameter-heading"> <div className="parameter-heading">
<label>{param.title || toHuman(param.name)}</label> <label>{param.title || toHuman(param.name)}</label>
{editable && ( {editable && (
@@ -133,8 +127,7 @@ export class Parameters extends React.Component {
className="btn btn-default btn-xs m-l-5" className="btn btn-default btn-xs m-l-5"
onClick={() => this.showParameterSettings(param, index)} onClick={() => this.showParameterSettings(param, index)}
data-test={`ParameterSettings-${param.name}`} data-test={`ParameterSettings-${param.name}`}
type="button" type="button">
>
<i className="fa fa-cog" /> <i className="fa fa-cog" />
</button> </button>
)} )}
@@ -154,7 +147,7 @@ export class Parameters extends React.Component {
render() { render() {
const { parameters } = this.state; const { parameters } = this.state;
const { editable } = this.props; const { editable } = this.props;
const dirtyParamCount = size(filter(parameters, 'hasPendingValue')); const dirtyParamCount = size(filter(parameters, "hasPendingValue"));
return ( return (
<SortableContainer <SortableContainer
disabled={!editable} disabled={!editable}
@@ -165,10 +158,9 @@ export class Parameters extends React.Component {
updateBeforeSortStart={this.onBeforeSortStart} updateBeforeSortStart={this.onBeforeSortStart}
onSortEnd={this.moveParameter} onSortEnd={this.moveParameter}
containerProps={{ containerProps={{
className: 'parameter-container', className: "parameter-container",
onKeyDown: dirtyParamCount ? this.handleKeyDown : null, onKeyDown: dirtyParamCount ? this.handleKeyDown : null,
}} }}>
>
{parameters.map((param, index) => ( {parameters.map((param, index) => (
<SortableElement key={param.name} index={index}> <SortableElement key={param.name} index={index}>
<div className="parameter-block" data-editable={editable || null}> <div className="parameter-block" data-editable={editable || null}>
@@ -184,7 +176,7 @@ export class Parameters extends React.Component {
} }
export default function init(ngModule) { export default function init(ngModule) {
ngModule.component('parameters', react2angular(Parameters)); ngModule.component("parameters", react2angular(Parameters));
} }
init.init = true; init.init = true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { react2angular } from 'react2angular'; import { react2angular } from "react2angular";
export function SortIcon({ column, sortColumn, reverse }) { export function SortIcon({ column, sortColumn, reverse }) {
if (column !== sortColumn) { if (column !== sortColumn) {
@@ -8,7 +8,9 @@ export function SortIcon({ column, sortColumn, reverse }) {
} }
return ( 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) { export default function init(ngModule) {
ngModule.component('sortIcon', react2angular(SortIcon)); ngModule.component("sortIcon", react2angular(SortIcon));
} }
init.init = true; init.init = true;

View File

@@ -1,12 +1,12 @@
import { map } from 'lodash'; import { map } from "lodash";
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { react2angular } from 'react2angular'; import { react2angular } from "react2angular";
import Badge from 'antd/lib/badge'; import Badge from "antd/lib/badge";
import Menu from 'antd/lib/menu'; import Menu from "antd/lib/menu";
import getTags from '@/services/getTags'; import getTags from "@/services/getTags";
import './TagsList.less'; import "./TagsList.less";
export class TagsList extends React.Component { export class TagsList extends React.Component {
static propTypes = { static propTypes = {
@@ -30,7 +30,7 @@ export class TagsList extends React.Component {
} }
componentDidMount() { componentDidMount() {
getTags(this.props.tagsUrl).then((allTags) => { getTags(this.props.tagsUrl).then(allTags => {
this.setState({ allTags }); this.setState({ allTags });
}); });
} }
@@ -46,7 +46,7 @@ export class TagsList extends React.Component {
} }
} else { } else {
// if the tag is the only selected, deselect it, otherwise select only it // 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(); selectedTags.clear();
} else { } else {
selectedTags.clear(); selectedTags.clear();
@@ -66,7 +66,9 @@ export class TagsList extends React.Component {
<Menu className="invert-stripe-position" mode="inline" selectedKeys={[...selectedTags]}> <Menu className="invert-stripe-position" mode="inline" selectedKeys={[...selectedTags]}>
{map(allTags, tag => ( {map(allTags, tag => (
<Menu.Item key={tag.name} className="m-0"> <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> <span className="max-character col-xs-11">{tag.name}</span>
<Badge count={tag.count} /> <Badge count={tag.count} />
</a> </a>
@@ -81,7 +83,7 @@ export class TagsList extends React.Component {
} }
export default function init(ngModule) { export default function init(ngModule) {
ngModule.component('tagsList', react2angular(TagsList)); ngModule.component("tagsList", react2angular(TagsList));
} }
init.init = true; init.init = true;

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import React, { useMemo, useEffect } from 'react'; import React, { useMemo, useEffect } from "react";
import moment from 'moment'; import moment from "moment";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { react2angular } from 'react2angular'; import { react2angular } from "react2angular";
import { Moment } from '@/components/proptypes'; import { Moment } from "@/components/proptypes";
import useForceUpdate from '@/lib/hooks/useForceUpdate'; import useForceUpdate from "@/lib/hooks/useForceUpdate";
export function Timer({ from }) { export function Timer({ from }) {
const startTime = useMemo(() => moment(from).valueOf(), [from]); const startTime = useMemo(() => moment(from).valueOf(), [from]);
@@ -15,18 +15,13 @@ export function Timer({ from }) {
}, [forceUpdate]); }, [forceUpdate]);
const diff = moment.now() - startTime; 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 = { Timer.propTypes = {
from: PropTypes.oneOfType([ from: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date), Moment]),
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Date),
Moment,
]),
}; };
Timer.defaultProps = { Timer.defaultProps = {
@@ -34,7 +29,7 @@ Timer.defaultProps = {
}; };
export default function init(ngModule) { export default function init(ngModule) {
ngModule.component('rdTimer', react2angular(Timer)); ngModule.component("rdTimer", react2angular(Timer));
} }
init.init = true; init.init = true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react'; import React, { useState, useMemo, useCallback, useEffect } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import { isEmpty, template } from 'lodash'; import { isEmpty, template } from "lodash";
import Dropdown from 'antd/lib/dropdown'; import Dropdown from "antd/lib/dropdown";
import Icon from 'antd/lib/icon'; import Icon from "antd/lib/icon";
import Menu from 'antd/lib/menu'; import Menu from "antd/lib/menu";
import HelpTrigger from '@/components/HelpTrigger'; import HelpTrigger from "@/components/HelpTrigger";
export default function FavoritesDropdown({ fetch, urlTemplate }) { export default function FavoritesDropdown({ fetch, urlTemplate }) {
const [items, setItems] = useState(); const [items, setItems] = useState();
@@ -15,16 +15,19 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
const noItems = isEmpty(items); const noItems = isEmpty(items);
const urlCompiled = useMemo(() => template(urlTemplate), [urlTemplate]); const urlCompiled = useMemo(() => template(urlTemplate), [urlTemplate]);
const fetchItems = useCallback((showLoadingState = true) => { const fetchItems = useCallback(
setLoading(showLoadingState); (showLoadingState = true) => {
fetch().$promise setLoading(showLoadingState);
.then(({ results }) => { fetch()
setItems(results); .$promise.then(({ results }) => {
}) setItems(results);
.finally(() => { })
setLoading(false); .finally(() => {
}); setLoading(false);
}, [fetch]); });
},
[fetch]
);
// fetch items on init // fetch items on init
useEffect(() => { useEffect(() => {
@@ -59,7 +62,12 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
); );
return ( 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" />} {loading ? <Icon type="loading" spin /> : <Icon type="down" />}
</Dropdown> </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 // eslint-disable-next-line import/prefer-default-export
export class ErrorHandler { export class ErrorHandler {
@@ -18,10 +18,7 @@ export class ErrorHandler {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);
} }
if ( if (error === null || error instanceof PromiseRejectionError) {
(error === null) ||
(error instanceof PromiseRejectionError)
) {
this.error = error; this.error = error;
} }
} }

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import Input from 'antd/lib/input'; import Input from "antd/lib/input";
import { includes, isEmpty } from 'lodash'; import { includes, isEmpty } from "lodash";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import React from 'react'; import React from "react";
import EmptyState from '@/components/items-list/components/EmptyState'; import EmptyState from "@/components/items-list/components/EmptyState";
import './CardsList.less'; import "./CardsList.less";
const { Search } = Input; const { Search } = Input;
@@ -16,7 +16,7 @@ export default class CardsList extends React.Component {
imgSrc: PropTypes.string.isRequired, imgSrc: PropTypes.string.isRequired,
onClick: PropTypes.func, onClick: PropTypes.func,
href: PropTypes.string, href: PropTypes.string,
}), })
), ),
showSearch: PropTypes.bool, showSearch: PropTypes.bool,
}; };
@@ -27,7 +27,7 @@ export default class CardsList extends React.Component {
}; };
state = { state = {
searchText: '', searchText: "",
}; };
constructor(props) { constructor(props) {
@@ -35,7 +35,7 @@ export default class CardsList extends React.Component {
this.items = []; this.items = [];
let itemId = 1; let itemId = 1;
props.items.forEach((item) => { props.items.forEach(item => {
this.items.push({ id: itemId, ...item }); this.items.push({ id: itemId, ...item });
itemId += 1; itemId += 1;
}); });
@@ -55,23 +55,22 @@ export default class CardsList extends React.Component {
const { showSearch } = this.props; const { showSearch } = this.props;
const { searchText } = this.state; const { searchText } = this.state;
const filteredItems = this.items.filter(item => isEmpty(searchText) || const filteredItems = this.items.filter(
includes(item.title.toLowerCase(), searchText.toLowerCase())); item => isEmpty(searchText) || includes(item.title.toLowerCase(), searchText.toLowerCase())
);
return ( return (
<div data-test="CardsList"> <div data-test="CardsList">
{showSearch && ( {showSearch && (
<div className="row p-10"> <div className="row p-10">
<div className="col-md-4 col-md-offset-4"> <div className="col-md-4 col-md-offset-4">
<Search <Search placeholder="Search..." onChange={e => this.setState({ searchText: e.target.value })} autoFocus />
placeholder="Search..."
onChange={e => this.setState({ searchText: e.target.value })}
autoFocus
/>
</div> </div>
</div> </div>
)} )}
{isEmpty(filteredItems) ? (<EmptyState className="" />) : ( {isEmpty(filteredItems) ? (
<EmptyState className="" />
) : (
<div className="row"> <div className="row">
<div className="col-lg-12 d-inline-flex flex-wrap visual-card-list"> <div className="col-lg-12 d-inline-flex flex-wrap visual-card-list">
{filteredItems.map(item => this.renderListItem(item))} {filteredItems.map(item => this.renderListItem(item))}

View File

@@ -1,18 +1,15 @@
import { each, values, map, includes, first } from 'lodash'; import { each, values, map, includes, first } from "lodash";
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import Select from 'antd/lib/select'; import Select from "antd/lib/select";
import Modal from 'antd/lib/modal'; import Modal from "antd/lib/modal";
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { import { MappingType, ParameterMappingListInput } from "@/components/ParameterMappingInput";
MappingType, import { QuerySelector } from "@/components/QuerySelector";
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; const { Option, OptGroup } = Select;
@@ -39,21 +36,19 @@ class AddWidgetDialog extends React.Component {
}); });
if (selectedQuery) { if (selectedQuery) {
Query.get({ id: selectedQuery.id }, (query) => { Query.get({ id: selectedQuery.id }, query => {
if (query) { if (query) {
const existingParamNames = map( const existingParamNames = map(this.props.dashboard.getParametersDefs(), param => param.name);
this.props.dashboard.getParametersDefs(),
param => param.name,
);
this.setState({ this.setState({
selectedQuery: query, selectedQuery: query,
parameterMappings: map(query.getParametersDefs(), param => ({ parameterMappings: map(query.getParametersDefs(), param => ({
name: param.name, name: param.name,
type: includes(existingParamNames, param.name) type: includes(existingParamNames, param.name)
? MappingType.DashboardMapToExisting : MappingType.DashboardAddNew, ? MappingType.DashboardMapToExisting
: MappingType.DashboardAddNew,
mapTo: param.name, mapTo: param.name,
value: param.normalizedValue, value: param.normalizedValue,
title: '', title: "",
param, param,
})), })),
}); });
@@ -66,7 +61,7 @@ class AddWidgetDialog extends React.Component {
} }
selectVisualization(query, visualizationId) { selectVisualization(query, visualizationId) {
each(query.visualizations, (visualization) => { each(query.visualizations, visualization => {
if (visualization.id === visualizationId) { if (visualization.id === visualizationId) {
this.setState({ selectedVis: visualization }); this.setState({ selectedVis: visualization });
return false; return false;
@@ -79,12 +74,13 @@ class AddWidgetDialog extends React.Component {
this.setState({ saveInProgress: true }); this.setState({ saveInProgress: true });
this.props.onConfirm(selectedVis, parameterMappings) this.props
.onConfirm(selectedVis, parameterMappings)
.then(() => { .then(() => {
this.props.dialog.close(); this.props.dialog.close();
}) })
.catch(() => { .catch(() => {
notification.error('Widget could not be added'); notification.error("Widget could not be added");
}) })
.finally(() => { .finally(() => {
this.setState({ saveInProgress: false }); this.setState({ saveInProgress: false });
@@ -98,7 +94,7 @@ class AddWidgetDialog extends React.Component {
renderVisualizationInput() { renderVisualizationInput() {
let visualizationGroups = {}; let visualizationGroups = {};
if (this.state.selectedQuery) { 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] = visualizationGroups[vis.type] || [];
visualizationGroups[vis.type].push(vis); visualizationGroups[vis.type].push(vis);
}); });
@@ -112,12 +108,13 @@ class AddWidgetDialog extends React.Component {
id="choose-visualization" id="choose-visualization"
className="w-100" className="w-100"
defaultValue={first(this.state.selectedQuery.visualizations).id} 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 => ( {visualizationGroups.map(visualizations => (
<OptGroup label={visualizations[0].type} key={visualizations[0].type}> <OptGroup label={visualizations[0].type} key={visualizations[0].type}>
{visualizations.map(visualization => ( {visualizations.map(visualization => (
<Option value={visualization.id} key={visualization.id}>{visualization.name}</Option> <Option value={visualization.id} key={visualization.id}>
{visualization.name}
</Option>
))} ))}
</OptGroup> </OptGroup>
))} ))}
@@ -141,24 +138,23 @@ class AddWidgetDialog extends React.Component {
disabled: !this.state.selectedQuery, disabled: !this.state.selectedQuery,
}} }}
okText="Add to Dashboard" okText="Add to Dashboard"
width={700} width={700}>
>
<div data-test="AddWidgetDialog"> <div data-test="AddWidgetDialog">
<QuerySelector onChange={query => this.selectQuery(query)} /> <QuerySelector onChange={query => this.selectQuery(query)} />
{this.state.selectedQuery && this.renderVisualizationInput()} {this.state.selectedQuery && this.renderVisualizationInput()}
{ {this.state.parameterMappings.length > 0 && [
(this.state.parameterMappings.length > 0) && [ <label key="parameters-title" htmlFor="parameter-mappings">
<label key="parameters-title" htmlFor="parameter-mappings">Parameters</label>, Parameters
<ParameterMappingListInput </label>,
key="parameters-list" <ParameterMappingListInput
id="parameter-mappings" key="parameters-list"
mappings={this.state.parameterMappings} id="parameter-mappings"
existingParams={existingParams} mappings={this.state.parameterMappings}
onChange={mappings => this.updateParamMappings(mappings)} existingParams={existingParams}
/>, onChange={mappings => this.updateParamMappings(mappings)}
] />,
} ]}
</div> </div>
</Modal> </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 // TODO: Revisit this implementation when migrating widget component to React
const WIDGET_SELECTOR = '[data-widgetid="{0}"]'; const WIDGET_SELECTOR = '[data-widgetid="{0}"]';
const WIDGET_CONTENT_SELECTOR = [ const WIDGET_CONTENT_SELECTOR = [
'.widget-header', // header ".widget-header", // header
'.visualization-renderer', // visualization ".visualization-renderer", // visualization
'.scrollbox .alert', // error state ".scrollbox .alert", // error state
'.spinner-container', // loading state ".spinner-container", // loading state
'.tile__bottom-control', // footer ".tile__bottom-control", // footer
].join(','); ].join(",");
const INTERVAL = 200; const INTERVAL = 200;
export default class AutoHeightController { export default class AutoHeightController {
@@ -29,9 +29,7 @@ export default class AutoHeightController {
.map(widget => widget.id.toString()); .map(widget => widget.id.toString());
// added // added
newWidgetIds newWidgetIds.filter(id => !includes(Object.keys(this.widgets), id)).forEach(this.add);
.filter(id => !includes(Object.keys(this.widgets), id))
.forEach(this.add);
// removed // removed
Object.keys(this.widgets) Object.keys(this.widgets)
@@ -39,12 +37,12 @@ export default class AutoHeightController {
.forEach(this.remove); .forEach(this.remove);
} }
add = (id) => { add = id => {
if (this.isEmpty()) { if (this.isEmpty()) {
this.start(); this.start();
} }
const selector = WIDGET_SELECTOR.replace('{0}', id); const selector = WIDGET_SELECTOR.replace("{0}", id);
this.widgets[id] = [ this.widgets[id] = [
function getHeight() { function getHeight() {
const widgetEl = document.querySelector(selector); const widgetEl = document.querySelector(selector);
@@ -56,15 +54,19 @@ export default class AutoHeightController {
const els = widgetEl.querySelectorAll(WIDGET_CONTENT_SELECTOR); const els = widgetEl.querySelectorAll(WIDGET_CONTENT_SELECTOR);
// calculate accumulated height // calculate accumulated height
return reduce(els, (acc, el) => { return reduce(
const height = el ? el.getBoundingClientRect().height : 0; els,
return acc + height; (acc, el) => {
}, 0); const height = el ? el.getBoundingClientRect().height : 0;
return acc + height;
},
0
);
}, },
]; ];
}; };
remove = (id) => { remove = id => {
// ignore if not an active autoHeight widget // ignore if not an active autoHeight widget
if (!this.exists(id)) { if (!this.exists(id)) {
return; return;
@@ -83,10 +85,9 @@ export default class AutoHeightController {
isEmpty = () => !some(this.widgets); isEmpty = () => !some(this.widgets);
checkHeightChanges = () => { checkHeightChanges = () => {
Object Object.keys(this.widgets)
.keys(this.widgets)
.filter(this.exists) // reject already removed items .filter(this.exists) // reject already removed items
.forEach((id) => { .forEach(id => {
const [getHeight, prevHeight] = this.widgets[id]; const [getHeight, prevHeight] = this.widgets[id];
const height = getHeight(); const height = getHeight();
if (height && height !== prevHeight) { if (height && height !== prevHeight) {
@@ -114,5 +115,5 @@ export default class AutoHeightController {
destroy = () => { destroy = () => {
this.stop(); this.stop();
this.widgets = null; this.widgets = null;
} };
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,17 @@
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import Tooltip from 'antd/lib/tooltip'; import Tooltip from "antd/lib/tooltip";
export default function ListItemAddon({ isSelected, isStaged, alreadyInGroup, deselectedIcon }) { export default function ListItemAddon({ isSelected, isStaged, alreadyInGroup, deselectedIcon }) {
if (isStaged) { if (isStaged) {
return <i className="fa fa-remove" />; return <i className="fa fa-remove" />;
} }
if (alreadyInGroup) { 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}`} />; return isSelected ? <i className="fa fa-check" /> : <i className={`fa ${deselectedIcon}`} />;
} }
@@ -23,5 +27,5 @@ ListItemAddon.defaultProps = {
isSelected: false, isSelected: false,
isStaged: false, isStaged: false,
alreadyInGroup: 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 { omit, debounce } from "lodash";
import React from 'react'; import React from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import hoistNonReactStatics from 'hoist-non-react-statics'; import hoistNonReactStatics from "hoist-non-react-statics";
import { $route, $routeParams } from '@/services/ng'; import { $route, $routeParams } from "@/services/ng";
import { clientConfig } from '@/services/auth'; import { clientConfig } from "@/services/auth";
import { StateStorage } from './classes/StateStorage'; import { StateStorage } from "./classes/StateStorage";
export const ControllerType = PropTypes.shape({ export const ControllerType = PropTypes.shape({
// values of props declared by wrapped component, current route's locals (`resolve: { ... }`) and title // 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) { export function wrap(WrappedComponent, itemsSource, stateStorage) {
class ItemsListWrapper extends React.Component { class ItemsListWrapper extends React.Component {
static propTypes = { static propTypes = {
...omit(WrappedComponent.propTypes, ['controller']), ...omit(WrappedComponent.propTypes, ["controller"]),
onError: PropTypes.func, onError: PropTypes.func,
children: PropTypes.node, children: PropTypes.node,
}; };
static defaultProps = { static defaultProps = {
...omit(WrappedComponent.defaultProps, ['controller']), ...omit(WrappedComponent.defaultProps, ["controller"]),
onError: (error) => { onError: error => {
// Allow calling chain to roll up, and then throw the error in global context // Allow calling chain to roll up, and then throw the error in global context
setTimeout(() => { throw error; }); setTimeout(() => {
throw error;
});
}, },
children: null, children: null,
}; };
@@ -107,10 +109,10 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
// ANGULAR_REMOVE_ME Revisit when some React router will be used // ANGULAR_REMOVE_ME Revisit when some React router will be used
title: $route.current.title, title: $route.current.title,
...$routeParams, ...$routeParams,
...omit($route.current.locals, ['$scope', '$template']), ...omit($route.current.locals, ["$scope", "$template"]),
// Add to params all props except of own ones // Add to params all props except of own ones
...omit(this.props, ['onError', 'children']), ...omit(this.props, ["onError", "children"]),
}; };
return { return {
...rest, ...rest,
@@ -118,7 +120,7 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
params, params,
isLoaded, isLoaded,
isEmpty: !isLoaded || (totalCount === 0), isEmpty: !isLoaded || totalCount === 0,
totalItemsCount: isLoaded ? totalCount : 0, totalItemsCount: isLoaded ? totalCount : 0,
pageSizeOptions: clientConfig.pageSizeOptions, pageSizeOptions: clientConfig.pageSizeOptions,
pageItems: isLoaded ? pageItems : [], pageItems: isLoaded ? pageItems : [],
@@ -129,7 +131,7 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
// don't pass own props to wrapped component // don't pass own props to wrapped component
const { children, onError, ...props } = this.props; const { children, onError, ...props } = this.props;
props.controller = this.state; props.controller = this.state;
return <WrappedComponent {...props}>{ children }</WrappedComponent>; return <WrappedComponent {...props}>{children}</WrappedComponent>;
} }
} }

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { isUndefined } from 'lodash'; import { isUndefined } from "lodash";
export default class Paginator { export default class Paginator {
page = 1; page = 1;
@@ -17,9 +17,9 @@ export default class Paginator {
} }
value = parseInt(value, 10) || 1; value = parseInt(value, 10) || 1;
if (validate) { if (validate) {
this.page = ((value >= 1) && (value <= this.totalPages)) ? value : 1; this.page = value >= 1 && value <= this.totalPages ? value : 1;
} else { } else {
this.page = (value >= 1) ? value : 1; this.page = value >= 1 ? value : 1;
} }
} }
@@ -28,7 +28,7 @@ export default class Paginator {
return; return;
} }
value = parseInt(value, 10) || 20; value = parseInt(value, 10) || 20;
this.itemsPerPage = (value >= 1) ? value : 1; this.itemsPerPage = value >= 1 ? value : 1;
if (validate) { if (validate) {
this.setPage(this.page, 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) { export function compile(field, reverse) {
if (!field) { if (!field) {
@@ -10,12 +10,12 @@ export function compile(field, reverse) {
} }
export function parse(compiled) { export function parse(compiled) {
compiled = isString(compiled) ? compiled : ''; compiled = isString(compiled) ? compiled : "";
const reverse = compiled.startsWith(ORDER_BY_REVERSE); const reverse = compiled.startsWith(ORDER_BY_REVERSE);
if (reverse) { if (reverse) {
compiled = compiled.substring(1); compiled = compiled.substring(1);
} }
const field = compiled !== '' ? compiled : null; const field = compiled !== "" ? compiled : null;
return { field, reverse }; return { field, reverse };
} }
@@ -35,7 +35,7 @@ export default class Sorter {
} }
setField(value) { setField(value) {
this.field = isString(value) && (value !== '') ? value : null; this.field = isString(value) && value !== "" ? value : null;
} }
setReverse(value) { setReverse(value) {
@@ -48,7 +48,7 @@ export default class Sorter {
} }
toggleField(field) { toggleField(field) {
if (!isString(field) || (field === '')) { if (!isString(field) || field === "") {
return; return;
} }
if (field === this.field) { if (field === this.field) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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