mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
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:
31
.github/workflows/black.yml
vendored
31
.github/workflows/black.yml
vendored
@@ -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
61
.restyled.yaml
Normal 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
|
||||
@@ -1,4 +1,4 @@
|
||||
import { configure } from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-16";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import AceEditor from 'react-ace';
|
||||
import React, { forwardRef } from "react";
|
||||
import AceEditor from "react-ace";
|
||||
|
||||
import './AceEditorInput.less';
|
||||
import "./AceEditorInput.less";
|
||||
|
||||
function AceEditorInput(props, ref) {
|
||||
return (
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import PropTypes from 'prop-types';
|
||||
import '@/redash-font/style.less';
|
||||
import recordEvent from '@/services/recordEvent';
|
||||
import React from "react";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import PropTypes from "prop-types";
|
||||
import "@/redash-font/style.less";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
|
||||
export default function AutocompleteToggle({ state, disabled, onToggle }) {
|
||||
let tooltipMessage = 'Live Autocomplete Enabled';
|
||||
let icon = 'icon-flash';
|
||||
let tooltipMessage = "Live Autocomplete Enabled";
|
||||
let icon = "icon-flash";
|
||||
if (!state) {
|
||||
tooltipMessage = 'Live Autocomplete Disabled';
|
||||
icon = 'icon-flash-off';
|
||||
tooltipMessage = "Live Autocomplete Disabled";
|
||||
icon = "icon-flash-off";
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
tooltipMessage = 'Live Autocomplete Not Available (Use Ctrl+Space to Trigger)';
|
||||
icon = 'icon-flash-off';
|
||||
tooltipMessage = "Live Autocomplete Not Available (Use Ctrl+Space to Trigger)";
|
||||
icon = "icon-flash-off";
|
||||
}
|
||||
|
||||
const toggle = (newState) => {
|
||||
recordEvent('toggle_autocomplete', 'screen', 'query_editor', { state: newState });
|
||||
const toggle = newState => {
|
||||
recordEvent("toggle_autocomplete", "screen", "query_editor", { state: newState });
|
||||
onToggle(newState);
|
||||
};
|
||||
|
||||
@@ -26,11 +26,10 @@ export default function AutocompleteToggle({ state, disabled, onToggle }) {
|
||||
<Tooltip placement="top" title={tooltipMessage}>
|
||||
<button
|
||||
type="button"
|
||||
className={'btn btn-default m-r-5' + (disabled ? ' disabled' : '')}
|
||||
className={"btn btn-default m-r-5" + (disabled ? " disabled" : "")}
|
||||
onClick={() => toggle(!state)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<i className={'icon ' + icon} />
|
||||
disabled={disabled}>
|
||||
<i className={"icon " + icon} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import Card from 'antd/lib/card';
|
||||
import Button from 'antd/lib/button';
|
||||
import Typography from 'antd/lib/typography';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import DynamicComponent from '@/components/DynamicComponent';
|
||||
import OrgSettings from '@/services/organizationSettings';
|
||||
import React, { useState } from "react";
|
||||
import Card from "antd/lib/card";
|
||||
import Button from "antd/lib/button";
|
||||
import Typography from "antd/lib/typography";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
import OrgSettings from "@/services/organizationSettings";
|
||||
|
||||
const Text = Typography.Text;
|
||||
|
||||
@@ -21,11 +21,11 @@ function BeaconConsent() {
|
||||
setHide(true);
|
||||
};
|
||||
|
||||
const confirmConsent = (confirm) => {
|
||||
let message = '🙏 Thank you.';
|
||||
const confirmConsent = confirm => {
|
||||
let message = "🙏 Thank you.";
|
||||
|
||||
if (!confirm) {
|
||||
message = 'Settings Saved.';
|
||||
message = "Settings Saved.";
|
||||
}
|
||||
|
||||
OrgSettings.save({ beacon_consent: confirm }, message)
|
||||
@@ -40,14 +40,13 @@ function BeaconConsent() {
|
||||
<DynamicComponent name="BeaconConsent">
|
||||
<div className="m-t-10 tiled">
|
||||
<Card
|
||||
title={(
|
||||
title={
|
||||
<>
|
||||
Would you be ok with sharing anonymous usage data with the Redash team?{' '}
|
||||
Would you be ok with sharing anonymous usage data with the Redash team?{" "}
|
||||
<HelpTrigger type="USAGE_DATA_SHARING" />
|
||||
</>
|
||||
)}
|
||||
bordered={false}
|
||||
>
|
||||
}
|
||||
bordered={false}>
|
||||
<Text>Help Redash improve by automatically sending anonymous usage data:</Text>
|
||||
<div className="m-t-5">
|
||||
<ul>
|
||||
@@ -66,7 +65,8 @@ function BeaconConsent() {
|
||||
</div>
|
||||
<div className="m-t-15">
|
||||
<Text type="secondary">
|
||||
You can change this setting anytime from the <a href="settings/organization">Organization Settings</a> page.
|
||||
You can change this setting anytime from the <a href="settings/organization">Organization Settings</a>{" "}
|
||||
page.
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
export function BigMessage({ message, icon, children, className }) {
|
||||
return (
|
||||
<div className={'p-15 text-center ' + className}>
|
||||
<div className={"p-15 text-center " + className}>
|
||||
<h3 className="m-t-0 m-b-0">
|
||||
<i className={'fa ' + icon} />
|
||||
<i className={"fa " + icon} />
|
||||
</h3>
|
||||
<br />
|
||||
{message}
|
||||
@@ -23,13 +23,13 @@ BigMessage.propTypes = {
|
||||
};
|
||||
|
||||
BigMessage.defaultProps = {
|
||||
message: '',
|
||||
message: "",
|
||||
children: null,
|
||||
className: 'tiled bg-white',
|
||||
className: "tiled bg-white",
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('bigMessage', react2angular(BigMessage));
|
||||
ngModule.component("bigMessage", react2angular(BigMessage));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'antd/lib/button';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import './CodeBlock.less';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import "./CodeBlock.less";
|
||||
|
||||
export default class CodeBlock extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -20,7 +20,7 @@ export default class CodeBlock extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.ref = React.createRef();
|
||||
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported('copy');
|
||||
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported("copy");
|
||||
this.resetCopyState = null;
|
||||
}
|
||||
|
||||
@@ -36,14 +36,14 @@ export default class CodeBlock extends React.Component {
|
||||
|
||||
// copy
|
||||
try {
|
||||
const success = document.execCommand('copy');
|
||||
const success = document.execCommand("copy");
|
||||
if (!success) {
|
||||
throw new Error();
|
||||
}
|
||||
this.setState({ copied: 'Copied!' });
|
||||
this.setState({ copied: "Copied!" });
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
copied: 'Copy failed',
|
||||
copied: "Copy failed",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,13 +58,8 @@ export default class CodeBlock extends React.Component {
|
||||
const { copyable, children, ...props } = this.props;
|
||||
|
||||
const copyButton = (
|
||||
<Tooltip title={this.state.copied || 'Copy'}>
|
||||
<Button
|
||||
icon="copy"
|
||||
type="dashed"
|
||||
size="small"
|
||||
onClick={this.copy}
|
||||
/>
|
||||
<Tooltip title={this.state.copied || "Copy"}>
|
||||
<Button icon="copy" type="dashed" size="small" onClick={this.copy} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import AntCollapse from 'antd/lib/collapse';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import AntCollapse from "antd/lib/collapse";
|
||||
|
||||
export default function Collapse({ collapsed, children, className, ...props }) {
|
||||
return (
|
||||
<AntCollapse {...props} activeKey={collapsed ? null : 'content'} className={cx(className, 'ant-collapse-headerless')}>
|
||||
<AntCollapse.Panel key="content" header="">{children}</AntCollapse.Panel>
|
||||
<AntCollapse
|
||||
{...props}
|
||||
activeKey={collapsed ? null : "content"}
|
||||
className={cx(className, "ant-collapse-headerless")}>
|
||||
<AntCollapse.Panel key="content" header="">
|
||||
{children}
|
||||
</AntCollapse.Panel>
|
||||
</AntCollapse>
|
||||
);
|
||||
}
|
||||
@@ -20,5 +25,5 @@ Collapse.propTypes = {
|
||||
Collapse.defaultProps = {
|
||||
collapsed: true,
|
||||
children: null,
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// ANGULAR_REMOVE_ME
|
||||
import { react2angular } from 'react2angular';
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import ColorPicker from '@/components/ColorPicker';
|
||||
import ColorPicker from "@/components/ColorPicker";
|
||||
|
||||
import './color-box.less';
|
||||
import "./color-box.less";
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('colorBox', react2angular(ColorPicker.Swatch));
|
||||
ngModule.component("colorBox", react2angular(ColorPicker.Swatch));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { isNil, isArray, chunk, map, filter, toPairs } from 'lodash';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import TextInput from 'antd/lib/input';
|
||||
import Typography from 'antd/lib/typography';
|
||||
import Swatch from './Swatch';
|
||||
import { isNil, isArray, chunk, map, filter, toPairs } from "lodash";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import tinycolor from "tinycolor2";
|
||||
import TextInput from "antd/lib/input";
|
||||
import Typography from "antd/lib/typography";
|
||||
import Swatch from "./Swatch";
|
||||
|
||||
import './input.less';
|
||||
import "./input.less";
|
||||
|
||||
function preparePresets(presetColors, presetColumns) {
|
||||
presetColors = isArray(presetColors) ? map(presetColors, v => [null, v]) : toPairs(presetColors);
|
||||
@@ -16,14 +16,14 @@ function preparePresets(presetColors, presetColumns) {
|
||||
}
|
||||
value = tinycolor(value);
|
||||
if (value.isValid()) {
|
||||
return [title, '#' + value.toHex().toUpperCase()];
|
||||
return [title, "#" + value.toHex().toUpperCase()];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return chunk(filter(presetColors), presetColumns);
|
||||
}
|
||||
|
||||
function validateColor(value, callback, prefix = '#') {
|
||||
function validateColor(value, callback, prefix = "#") {
|
||||
if (isNil(value)) {
|
||||
callback(null);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ function validateColor(value, callback, prefix = '#') {
|
||||
}
|
||||
|
||||
export default function Input({ color, presetColors, presetColumns, onChange, onPressEnter }) {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
|
||||
const presets = preparePresets(presetColors, presetColumns);
|
||||
@@ -46,7 +46,7 @@ export default function Input({ color, presetColors, presetColumns, onChange, on
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInputFocused) {
|
||||
validateColor(color, setInputValue, '');
|
||||
validateColor(color, setInputValue, "");
|
||||
}
|
||||
}, [color, isInputFocused]);
|
||||
|
||||
@@ -86,7 +86,7 @@ Input.propTypes = {
|
||||
};
|
||||
|
||||
Input.defaultProps = {
|
||||
color: '#FFFFFF',
|
||||
color: "#FFFFFF",
|
||||
presetColors: null,
|
||||
presetColumns: 8,
|
||||
onChange: () => {},
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import React, { useMemo } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
|
||||
import { validateColor, getColorName } from './utils';
|
||||
import './label.less';
|
||||
import { validateColor, getColorName } from "./utils";
|
||||
import "./label.less";
|
||||
|
||||
export default function Label({ className, color, presetColors, ...props }) {
|
||||
const name = useMemo(
|
||||
() => getColorName(validateColor(color), presetColors),
|
||||
[color, presetColors],
|
||||
);
|
||||
const name = useMemo(() => getColorName(validateColor(color), presetColors), [color, presetColors]);
|
||||
|
||||
return <span className={cx('color-label', className)} {...props}>{name}</span>;
|
||||
return (
|
||||
<span className={cx("color-label", className)} {...props}>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Label.propTypes = {
|
||||
@@ -25,6 +26,6 @@ Label.propTypes = {
|
||||
|
||||
Label.defaultProps = {
|
||||
className: null,
|
||||
color: '#FFFFFF',
|
||||
color: "#FFFFFF",
|
||||
presetColors: null,
|
||||
};
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { isString } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { isString } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
import './swatch.less';
|
||||
import "./swatch.less";
|
||||
|
||||
export default function Swatch({ className, color, title, size, ...props }) {
|
||||
const result = (
|
||||
<span
|
||||
className={cx('color-swatch', className)}
|
||||
style={{ backgroundColor: color, width: size }}
|
||||
{...props}
|
||||
/>
|
||||
<span className={cx("color-swatch", className)} style={{ backgroundColor: color, width: size }} {...props} />
|
||||
);
|
||||
|
||||
if (isString(title) && (title !== '')) {
|
||||
if (isString(title) && title !== "") {
|
||||
return (
|
||||
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>{result}</Tooltip>
|
||||
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
{result}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return result;
|
||||
@@ -33,6 +31,6 @@ Swatch.propTypes = {
|
||||
Swatch.defaultProps = {
|
||||
className: null,
|
||||
title: null,
|
||||
color: 'transparent',
|
||||
color: "transparent",
|
||||
size: 12,
|
||||
};
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import { toString } from 'lodash';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Card from 'antd/lib/card';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
|
||||
import { toString } from "lodash";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import Popover from "antd/lib/popover";
|
||||
import Card from "antd/lib/card";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import Icon from "antd/lib/icon";
|
||||
import chooseTextColorForBackground from "@/lib/chooseTextColorForBackground";
|
||||
|
||||
import ColorInput from './Input';
|
||||
import Swatch from './Swatch';
|
||||
import Label from './Label';
|
||||
import { validateColor } from './utils';
|
||||
import ColorInput from "./Input";
|
||||
import Swatch from "./Swatch";
|
||||
import Label from "./Label";
|
||||
import { validateColor } from "./utils";
|
||||
|
||||
import './index.less';
|
||||
import "./index.less";
|
||||
|
||||
export default function ColorPicker({
|
||||
color, placement, presetColors, presetColumns, interactive, children, onChange, triggerProps,
|
||||
addonBefore, addonAfter,
|
||||
color,
|
||||
placement,
|
||||
presetColors,
|
||||
presetColumns,
|
||||
interactive,
|
||||
children,
|
||||
onChange,
|
||||
triggerProps,
|
||||
addonBefore,
|
||||
addonAfter,
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const validatedColor = useMemo(() => validateColor(color), [color]);
|
||||
const [currentColor, setCurrentColor] = useState('');
|
||||
const [currentColor, setCurrentColor] = useState("");
|
||||
|
||||
function handleApply() {
|
||||
setVisible(false);
|
||||
@@ -36,16 +44,16 @@ export default function ColorPicker({
|
||||
|
||||
const actions = [];
|
||||
if (!interactive) {
|
||||
actions.push((
|
||||
actions.push(
|
||||
<Tooltip key="cancel" title="Cancel">
|
||||
<Icon type="close" onClick={handleCancel} />
|
||||
</Tooltip>
|
||||
));
|
||||
actions.push((
|
||||
);
|
||||
actions.push(
|
||||
<Tooltip key="apply" title="Apply">
|
||||
<Icon type="check" onClick={handleApply} />
|
||||
</Tooltip>
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
function handleInputChange(newColor) {
|
||||
@@ -66,9 +74,9 @@ export default function ColorPicker({
|
||||
{addonBefore}
|
||||
<Popover
|
||||
arrowPointAtCenter
|
||||
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
|
||||
overlayStyle={{ '--color-picker-selected-color': currentColor }}
|
||||
content={(
|
||||
overlayClassName={`color-picker ${interactive ? "color-picker-interactive" : "color-picker-with-actions"}`}
|
||||
overlayStyle={{ "--color-picker-selected-color": currentColor }}
|
||||
content={
|
||||
<Card
|
||||
data-test="ColorPicker"
|
||||
className="color-picker-panel"
|
||||
@@ -78,8 +86,7 @@ export default function ColorPicker({
|
||||
backgroundColor: currentColor,
|
||||
color: chooseTextColorForBackground(currentColor),
|
||||
}}
|
||||
actions={actions}
|
||||
>
|
||||
actions={actions}>
|
||||
<ColorInput
|
||||
color={currentColor}
|
||||
presetColors={presetColors}
|
||||
@@ -88,18 +95,17 @@ export default function ColorPicker({
|
||||
onPressEnter={handleApply}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
}
|
||||
trigger="click"
|
||||
placement={placement}
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
>
|
||||
onVisibleChange={setVisible}>
|
||||
{children || (
|
||||
<Swatch
|
||||
color={validatedColor}
|
||||
size={30}
|
||||
{...triggerProps}
|
||||
className={cx('color-picker-trigger', triggerProps.className)}
|
||||
className={cx("color-picker-trigger", triggerProps.className)}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
@@ -111,9 +117,18 @@ export default function ColorPicker({
|
||||
ColorPicker.propTypes = {
|
||||
color: PropTypes.string,
|
||||
placement: PropTypes.oneOf([
|
||||
'top', 'left', 'right', 'bottom',
|
||||
'topLeft', 'topRight', 'bottomLeft', 'bottomRight',
|
||||
'leftTop', 'leftBottom', 'rightTop', 'rightBottom',
|
||||
"top",
|
||||
"left",
|
||||
"right",
|
||||
"bottom",
|
||||
"topLeft",
|
||||
"topRight",
|
||||
"bottomLeft",
|
||||
"bottomRight",
|
||||
"leftTop",
|
||||
"leftBottom",
|
||||
"rightTop",
|
||||
"rightBottom",
|
||||
]),
|
||||
presetColors: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
|
||||
@@ -129,8 +144,8 @@ ColorPicker.propTypes = {
|
||||
};
|
||||
|
||||
ColorPicker.defaultProps = {
|
||||
color: '#FFFFFF',
|
||||
placement: 'top',
|
||||
color: "#FFFFFF",
|
||||
placement: "top",
|
||||
presetColors: null,
|
||||
presetColumns: 8,
|
||||
interactive: false,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { isArray, findKey } from 'lodash';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { isArray, findKey } from "lodash";
|
||||
import tinycolor from "tinycolor2";
|
||||
|
||||
export function validateColor(value, fallback = null) {
|
||||
value = tinycolor(value);
|
||||
return value.isValid() ? '#' + value.toHex().toUpperCase() : fallback;
|
||||
return value.isValid() ? "#" + value.toHex().toUpperCase() : fallback;
|
||||
}
|
||||
|
||||
export function getColorName(color, presetColors) {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isEmpty, toUpper, includes } from 'lodash';
|
||||
import Button from 'antd/lib/button';
|
||||
import List from 'antd/lib/list';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Input from 'antd/lib/input';
|
||||
import Steps from 'antd/lib/steps';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { PreviewCard } from '@/components/PreviewCard';
|
||||
import EmptyState from '@/components/items-list/components/EmptyState';
|
||||
import DynamicForm from '@/components/dynamic-form/DynamicForm';
|
||||
import helper from '@/components/dynamic-form/dynamicFormHelper';
|
||||
import HelpTrigger, { TYPES as HELP_TRIGGER_TYPES } from '@/components/HelpTrigger';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { isEmpty, toUpper, includes } from "lodash";
|
||||
import Button from "antd/lib/button";
|
||||
import List from "antd/lib/list";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Input from "antd/lib/input";
|
||||
import Steps from "antd/lib/steps";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import { PreviewCard } from "@/components/PreviewCard";
|
||||
import EmptyState from "@/components/items-list/components/EmptyState";
|
||||
import DynamicForm from "@/components/dynamic-form/DynamicForm";
|
||||
import helper from "@/components/dynamic-form/dynamicFormHelper";
|
||||
import HelpTrigger, { TYPES as HELP_TRIGGER_TYPES } from "@/components/HelpTrigger";
|
||||
|
||||
const { Step } = Steps;
|
||||
const { Search } = Input;
|
||||
@@ -38,19 +38,19 @@ class CreateSourceDialog extends React.Component {
|
||||
};
|
||||
|
||||
state = {
|
||||
searchText: '',
|
||||
searchText: "",
|
||||
selectedType: null,
|
||||
savingSource: false,
|
||||
currentStep: StepEnum.SELECT_TYPE,
|
||||
};
|
||||
|
||||
selectType = (selectedType) => {
|
||||
selectType = selectedType => {
|
||||
this.setState({ selectedType, currentStep: StepEnum.CONFIGURE_IT });
|
||||
};
|
||||
|
||||
resetType = () => {
|
||||
if (this.state.currentStep === StepEnum.CONFIGURE_IT) {
|
||||
this.setState({ searchText: '', selectedType: null, currentStep: StepEnum.SELECT_TYPE });
|
||||
this.setState({ searchText: "", selectedType: null, currentStep: StepEnum.SELECT_TYPE });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,10 +58,13 @@ class CreateSourceDialog extends React.Component {
|
||||
const { selectedType, savingSource } = this.state;
|
||||
if (!savingSource) {
|
||||
this.setState({ savingSource: true, currentStep: StepEnum.DONE });
|
||||
this.props.onCreate(selectedType, values).then((data) => {
|
||||
successCallback('Saved.');
|
||||
this.props
|
||||
.onCreate(selectedType, values)
|
||||
.then(data => {
|
||||
successCallback("Saved.");
|
||||
this.props.dialog.close({ success: true, data });
|
||||
}).catch((error) => {
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({ savingSource: false, currentStep: StepEnum.CONFIGURE_IT });
|
||||
errorCallback(error.message);
|
||||
});
|
||||
@@ -71,8 +74,9 @@ class CreateSourceDialog extends React.Component {
|
||||
renderTypeSelector() {
|
||||
const { types } = this.props;
|
||||
const { searchText } = this.state;
|
||||
const filteredTypes = types.filter(type => isEmpty(searchText) ||
|
||||
includes(type.name.toLowerCase(), searchText.toLowerCase()));
|
||||
const filteredTypes = types.filter(
|
||||
type => isEmpty(searchText) || includes(type.name.toLowerCase(), searchText.toLowerCase())
|
||||
);
|
||||
return (
|
||||
<div className="m-t-10">
|
||||
<Search
|
||||
@@ -81,13 +85,11 @@ class CreateSourceDialog extends React.Component {
|
||||
autoFocus
|
||||
data-test="SearchSource"
|
||||
/>
|
||||
<div className="scrollbox p-5 m-t-10" style={{ minHeight: '30vh', maxHeight: '40vh' }}>
|
||||
{isEmpty(filteredTypes) ? (<EmptyState className="" />) : (
|
||||
<List
|
||||
size="small"
|
||||
dataSource={filteredTypes}
|
||||
renderItem={item => this.renderItem(item)}
|
||||
/>
|
||||
<div className="scrollbox p-5 m-t-10" style={{ minHeight: "30vh", maxHeight: "40vh" }}>
|
||||
{isEmpty(filteredTypes) ? (
|
||||
<EmptyState className="" />
|
||||
) : (
|
||||
<List size="small" dataSource={filteredTypes} renderItem={item => this.renderItem(item)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,12 +104,7 @@ class CreateSourceDialog extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<img
|
||||
className="p-5"
|
||||
src={`${imageFolder}/${selectedType.type}.png`}
|
||||
alt={selectedType.name}
|
||||
width="48"
|
||||
/>
|
||||
<img className="p-5" src={`${imageFolder}/${selectedType.type}.png`} alt={selectedType.name} width="48" />
|
||||
<h4 className="m-0">{selectedType.name}</h4>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
@@ -117,13 +114,7 @@ class CreateSourceDialog extends React.Component {
|
||||
</HelpTrigger>
|
||||
)}
|
||||
</div>
|
||||
<DynamicForm
|
||||
id="sourceForm"
|
||||
fields={fields}
|
||||
onSubmit={this.createSource}
|
||||
feedbackIcons
|
||||
hideSubmitButton
|
||||
/>
|
||||
<DynamicForm id="sourceForm" fields={fields} onSubmit={this.createSource} feedbackIcons hideSubmitButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -131,10 +122,7 @@ class CreateSourceDialog extends React.Component {
|
||||
renderItem(item) {
|
||||
const { imageFolder } = this.props;
|
||||
return (
|
||||
<List.Item
|
||||
className="p-l-10 p-r-10 clickable"
|
||||
onClick={() => this.selectType(item)}
|
||||
>
|
||||
<List.Item className="p-l-10 p-r-10 clickable" onClick={() => this.selectType(item)}>
|
||||
<PreviewCard title={item.name} imageUrl={`${imageFolder}/${item.type}.png`} roundedImage={false}>
|
||||
<i className="fa fa-angle-double-right" />
|
||||
</PreviewCard>
|
||||
@@ -149,34 +137,38 @@ class CreateSourceDialog extends React.Component {
|
||||
<Modal
|
||||
{...dialog.props}
|
||||
title={`Create a New ${sourceType}`}
|
||||
footer={(currentStep === StepEnum.SELECT_TYPE) ? [
|
||||
(<Button key="cancel" onClick={() => dialog.dismiss()}>Cancel</Button>),
|
||||
(<Button key="submit" type="primary" disabled>Create</Button>),
|
||||
] : [
|
||||
(<Button key="previous" onClick={this.resetType}>Previous</Button>),
|
||||
(
|
||||
footer={
|
||||
currentStep === StepEnum.SELECT_TYPE
|
||||
? [
|
||||
<Button key="cancel" onClick={() => dialog.dismiss()}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" disabled>
|
||||
Create
|
||||
</Button>,
|
||||
]
|
||||
: [
|
||||
<Button key="previous" onClick={this.resetType}>
|
||||
Previous
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
htmlType="submit"
|
||||
form="sourceForm"
|
||||
type="primary"
|
||||
loading={savingSource}
|
||||
data-test="CreateSourceButton"
|
||||
>
|
||||
data-test="CreateSourceButton">
|
||||
Create
|
||||
</Button>
|
||||
),
|
||||
]}
|
||||
>
|
||||
</Button>,
|
||||
]
|
||||
}>
|
||||
<div data-test="CreateSourceDialog">
|
||||
<Steps className="hidden-xs m-b-10" size="small" current={currentStep} progressDot>
|
||||
{currentStep === StepEnum.CONFIGURE_IT ? (
|
||||
<Step
|
||||
title={<a>Type Selection</a>}
|
||||
className="clickable"
|
||||
onClick={this.resetType}
|
||||
/>
|
||||
) : (<Step title="Type Selection" />)}
|
||||
<Step title={<a>Type Selection</a>} className="clickable" onClick={this.resetType} />
|
||||
) : (
|
||||
<Step title="Type Selection" />
|
||||
)}
|
||||
<Step title="Configuration" />
|
||||
<Step title="Done" />
|
||||
</Steps>
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DatePicker from 'antd/lib/date-picker';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import DatePicker from "antd/lib/date-picker";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
|
||||
const DateInput = React.forwardRef(({
|
||||
defaultValue,
|
||||
value,
|
||||
onSelect,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const format = clientConfig.dateFormat || 'YYYY-MM-DD';
|
||||
const DateInput = React.forwardRef(({ defaultValue, value, onSelect, className, ...props }, ref) => {
|
||||
const format = clientConfig.dateFormat || "YYYY-MM-DD";
|
||||
const additionalAttributes = {};
|
||||
if (defaultValue && defaultValue.isValid()) {
|
||||
additionalAttributes.defaultValue = defaultValue;
|
||||
@@ -43,7 +37,7 @@ DateInput.defaultProps = {
|
||||
defaultValue: null,
|
||||
value: undefined,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default DateInput;
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { isArray } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DatePicker from 'antd/lib/date-picker';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import { isArray } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import DatePicker from "antd/lib/date-picker";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const DateRangeInput = React.forwardRef(({
|
||||
defaultValue,
|
||||
value,
|
||||
onSelect,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const format = clientConfig.dateFormat || 'YYYY-MM-DD';
|
||||
const DateRangeInput = React.forwardRef(({ defaultValue, value, onSelect, className, ...props }, ref) => {
|
||||
const format = clientConfig.dateFormat || "YYYY-MM-DD";
|
||||
const additionalAttributes = {};
|
||||
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
|
||||
additionalAttributes.defaultValue = defaultValue;
|
||||
@@ -45,7 +39,7 @@ DateRangeInput.defaultProps = {
|
||||
defaultValue: null,
|
||||
value: undefined,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default DateRangeInput;
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DatePicker from 'antd/lib/date-picker';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import DatePicker from "antd/lib/date-picker";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
|
||||
const DateTimeInput = React.forwardRef(({
|
||||
defaultValue,
|
||||
value,
|
||||
withSeconds,
|
||||
onSelect,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const format = (clientConfig.dateFormat || 'YYYY-MM-DD') +
|
||||
(withSeconds ? ' HH:mm:ss' : ' HH:mm');
|
||||
const DateTimeInput = React.forwardRef(({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
|
||||
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
|
||||
const additionalAttributes = {};
|
||||
if (defaultValue && defaultValue.isValid()) {
|
||||
additionalAttributes.defaultValue = defaultValue;
|
||||
@@ -48,7 +40,7 @@ DateTimeInput.defaultProps = {
|
||||
value: undefined,
|
||||
withSeconds: false,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default DateTimeInput;
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import { isArray } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DatePicker from 'antd/lib/date-picker';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import { isArray } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import DatePicker from "antd/lib/date-picker";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const DateTimeRangeInput = React.forwardRef(({
|
||||
defaultValue,
|
||||
value,
|
||||
withSeconds,
|
||||
onSelect,
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const format = (clientConfig.dateFormat || 'YYYY-MM-DD') +
|
||||
(withSeconds ? ' HH:mm:ss' : ' HH:mm');
|
||||
const DateTimeRangeInput = React.forwardRef(
|
||||
({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
|
||||
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
|
||||
const additionalAttributes = {};
|
||||
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
|
||||
additionalAttributes.defaultValue = defaultValue;
|
||||
@@ -35,7 +28,8 @@ const DateTimeRangeInput = React.forwardRef(({
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
DateTimeRangeInput.propTypes = {
|
||||
defaultValue: PropTypes.arrayOf(Moment),
|
||||
@@ -50,7 +44,7 @@ DateTimeRangeInput.defaultProps = {
|
||||
value: undefined,
|
||||
withSeconds: false,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
export default DateTimeRangeInput;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isFunction } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { isFunction } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
/**
|
||||
Wrapper for dialogs based on Ant's <Modal> component.
|
||||
@@ -140,7 +140,7 @@ function openDialog(DialogComponent, props) {
|
||||
reject: () => {},
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
|
||||
function render() {
|
||||
@@ -176,7 +176,7 @@ function openDialog(DialogComponent, props) {
|
||||
const result = {
|
||||
close: closeDialog,
|
||||
dismiss: dismissDialog,
|
||||
update: (newProps) => {
|
||||
update: newProps => {
|
||||
props = { ...props, ...newProps };
|
||||
render();
|
||||
},
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { isFunction, isString } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isFunction, isString } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const componentsRegistry = new Map();
|
||||
const activeInstances = new Set();
|
||||
|
||||
export function registerComponent(name, component) {
|
||||
if (isString(name) && name !== '') {
|
||||
if (isString(name) && name !== "") {
|
||||
componentsRegistry.set(name, isFunction(component) ? component : null);
|
||||
// Refresh active DynamicComponent instances which use this component
|
||||
activeInstances.forEach((dynamicComponent) => {
|
||||
activeInstances.forEach(dynamicComponent => {
|
||||
if (dynamicComponent.props.name === name) {
|
||||
dynamicComponent.forceUpdate();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { trim } from 'lodash';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import { trim } from "lodash";
|
||||
|
||||
export class EditInPlace extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -16,8 +16,8 @@ export class EditInPlace extends React.Component {
|
||||
static defaultProps = {
|
||||
ignoreBlanks: false,
|
||||
isEditable: true,
|
||||
placeholder: '',
|
||||
value: '',
|
||||
placeholder: "",
|
||||
value: "",
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -42,14 +42,14 @@ export class EditInPlace extends React.Component {
|
||||
|
||||
stopEditing = () => {
|
||||
const newValue = trim(this.inputRef.current.value);
|
||||
const ignorableBlank = this.props.ignoreBlanks && newValue === '';
|
||||
const ignorableBlank = this.props.ignoreBlanks && newValue === "";
|
||||
if (!ignorableBlank && newValue !== this.props.value) {
|
||||
this.props.onDone(newValue);
|
||||
}
|
||||
this.setState({ editing: false });
|
||||
};
|
||||
|
||||
keyDown = (event) => {
|
||||
keyDown = event => {
|
||||
if (event.keyCode === 13 && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
this.stopEditing();
|
||||
@@ -63,15 +63,15 @@ export class EditInPlace extends React.Component {
|
||||
role="presentation"
|
||||
onFocus={this.startEditing}
|
||||
onClick={this.startEditing}
|
||||
className={this.props.isEditable ? 'editable' : ''}
|
||||
>
|
||||
className={this.props.isEditable ? "editable" : ""}>
|
||||
{this.props.value || this.props.placeholder}
|
||||
</span>
|
||||
);
|
||||
|
||||
renderEdit = () => React.createElement(this.props.editor, {
|
||||
renderEdit = () =>
|
||||
React.createElement(this.props.editor, {
|
||||
ref: this.inputRef,
|
||||
className: 'rd-form-control',
|
||||
className: "rd-form-control",
|
||||
defaultValue: this.props.value,
|
||||
onBlur: this.stopEditing,
|
||||
onKeyDown: this.keyDown,
|
||||
@@ -79,7 +79,7 @@ export class EditInPlace extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className={'edit-in-place' + (this.state.editing ? ' active' : '')}>
|
||||
<span className={"edit-in-place" + (this.state.editing ? " active" : "")}>
|
||||
{this.state.editing ? this.renderEdit() : this.renderNormal()}
|
||||
</span>
|
||||
);
|
||||
@@ -87,7 +87,7 @@ export class EditInPlace extends React.Component {
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('editInPlace', react2angular(EditInPlace));
|
||||
ngModule.component("editInPlace", react2angular(EditInPlace));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
|
||||
import { includes, words, capitalize, clone, isNull } from 'lodash';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Form from 'antd/lib/form';
|
||||
import Button from 'antd/lib/button';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import Divider from 'antd/lib/divider';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { QuerySelector } from '@/components/QuerySelector';
|
||||
import { Query } from '@/services/query';
|
||||
import { includes, words, capitalize, clone, isNull } from "lodash";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Checkbox from "antd/lib/checkbox";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Form from "antd/lib/form";
|
||||
import Button from "antd/lib/button";
|
||||
import Select from "antd/lib/select";
|
||||
import Input from "antd/lib/input";
|
||||
import Divider from "antd/lib/divider";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import { QuerySelector } from "@/components/QuerySelector";
|
||||
import { Query } from "@/services/query";
|
||||
|
||||
const { Option } = Select;
|
||||
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
|
||||
|
||||
function getDefaultTitle(text) {
|
||||
return capitalize(words(text).join(' ')); // humanize
|
||||
return capitalize(words(text).join(" ")); // humanize
|
||||
}
|
||||
|
||||
function isTypeDateRange(type) {
|
||||
@@ -26,30 +25,26 @@ function isTypeDateRange(type) {
|
||||
|
||||
function joinExampleList(multiValuesOptions) {
|
||||
const { prefix, suffix } = multiValuesOptions;
|
||||
return ['value1', 'value2', 'value3']
|
||||
.map(value => `${prefix}${value}${suffix}`)
|
||||
.join(',');
|
||||
return ["value1", "value2", "value3"].map(value => `${prefix}${value}${suffix}`).join(",");
|
||||
}
|
||||
|
||||
function NameInput({ name, type, onChange, existingNames, setValidation }) {
|
||||
let helpText = '';
|
||||
let validateStatus = '';
|
||||
let helpText = "";
|
||||
let validateStatus = "";
|
||||
|
||||
if (!name) {
|
||||
helpText = 'Choose a keyword for this parameter';
|
||||
helpText = "Choose a keyword for this parameter";
|
||||
setValidation(false);
|
||||
} else if (includes(existingNames, name)) {
|
||||
helpText = 'Parameter with this name already exists';
|
||||
helpText = "Parameter with this name already exists";
|
||||
setValidation(false);
|
||||
validateStatus = 'error';
|
||||
validateStatus = "error";
|
||||
} else {
|
||||
if (isTypeDateRange(type)) {
|
||||
helpText = (
|
||||
<React.Fragment>
|
||||
Appears in query as {' '}
|
||||
<code style={{ display: 'inline-block', color: 'inherit' }}>
|
||||
{`{{${name}.start}} {{${name}.end}}`}
|
||||
</code>
|
||||
Appears in query as{" "}
|
||||
<code style={{ display: "inline-block", color: "inherit" }}>{`{{${name}.start}} {{${name}.end}}`}</code>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -57,13 +52,7 @@ function NameInput({ name, type, onChange, existingNames, setValidation }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
required
|
||||
label="Keyword"
|
||||
help={helpText}
|
||||
validateStatus={validateStatus}
|
||||
{...formItemProps}
|
||||
>
|
||||
<Form.Item required label="Keyword" help={helpText} validateStatus={validateStatus} {...formItemProps}>
|
||||
<Input onChange={e => onChange(e.target.value)} autoFocus />
|
||||
</Form.Item>
|
||||
);
|
||||
@@ -88,7 +77,7 @@ function EditParameterSettingsDialog(props) {
|
||||
useEffect(() => {
|
||||
const queryId = props.parameter.queryId;
|
||||
if (queryId) {
|
||||
Query.get({ id: queryId }, (query) => {
|
||||
Query.get({ id: queryId }, query => {
|
||||
setInitialQuery(query);
|
||||
});
|
||||
}
|
||||
@@ -101,12 +90,12 @@ function EditParameterSettingsDialog(props) {
|
||||
}
|
||||
|
||||
// title
|
||||
if (param.title === '') {
|
||||
if (param.title === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// query
|
||||
if (param.type === 'query' && !param.queryId) {
|
||||
if (param.type === "query" && !param.queryId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -129,16 +118,22 @@ function EditParameterSettingsDialog(props) {
|
||||
return (
|
||||
<Modal
|
||||
{...props.dialog.props}
|
||||
title={isNew ? 'Add Parameter' : param.name}
|
||||
title={isNew ? "Add Parameter" : param.name}
|
||||
width={600}
|
||||
footer={[(
|
||||
<Button key="cancel" onClick={props.dialog.dismiss}>Cancel</Button>
|
||||
), (
|
||||
<Button key="submit" htmlType="submit" disabled={!isFulfilled()} type="primary" form="paramForm" data-test="SaveParameterSettings">
|
||||
{isNew ? 'Add Parameter' : 'OK'}
|
||||
</Button>
|
||||
)]}
|
||||
>
|
||||
footer={[
|
||||
<Button key="cancel" onClick={props.dialog.dismiss}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
htmlType="submit"
|
||||
disabled={!isFulfilled()}
|
||||
type="primary"
|
||||
form="paramForm"
|
||||
data-test="SaveParameterSettings">
|
||||
{isNew ? "Add Parameter" : "OK"}
|
||||
</Button>,
|
||||
]}>
|
||||
<Form layout="horizontal" onSubmit={onConfirm} id="paramForm">
|
||||
{isNew && (
|
||||
<NameInput
|
||||
@@ -158,25 +153,35 @@ function EditParameterSettingsDialog(props) {
|
||||
</Form.Item>
|
||||
<Form.Item label="Type" {...formItemProps}>
|
||||
<Select value={param.type} onChange={type => setParam({ ...param, type })} data-test="ParameterTypeSelect">
|
||||
<Option value="text" data-test="TextParameterTypeOption">Text</Option>
|
||||
<Option value="number" data-test="NumberParameterTypeOption">Number</Option>
|
||||
<Option value="text" data-test="TextParameterTypeOption">
|
||||
Text
|
||||
</Option>
|
||||
<Option value="number" data-test="NumberParameterTypeOption">
|
||||
Number
|
||||
</Option>
|
||||
<Option value="enum">Dropdown List</Option>
|
||||
<Option value="query">Query Based Dropdown List</Option>
|
||||
<Option disabled key="dv1">
|
||||
<Divider className="select-option-divider" />
|
||||
</Option>
|
||||
<Option value="date" data-test="DateParameterTypeOption">Date</Option>
|
||||
<Option value="datetime-local" data-test="DateTimeParameterTypeOption">Date and Time</Option>
|
||||
<Option value="date" data-test="DateParameterTypeOption">
|
||||
Date
|
||||
</Option>
|
||||
<Option value="datetime-local" data-test="DateTimeParameterTypeOption">
|
||||
Date and Time
|
||||
</Option>
|
||||
<Option value="datetime-with-seconds">Date and Time (with seconds)</Option>
|
||||
<Option disabled key="dv2">
|
||||
<Divider className="select-option-divider" />
|
||||
</Option>
|
||||
<Option value="date-range" data-test="DateRangeParameterTypeOption">Date Range</Option>
|
||||
<Option value="date-range" data-test="DateRangeParameterTypeOption">
|
||||
Date Range
|
||||
</Option>
|
||||
<Option value="datetime-range">Date and Time Range</Option>
|
||||
<Option value="datetime-range-with-seconds">Date and Time Range (with seconds)</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{param.type === 'enum' && (
|
||||
{param.type === "enum" && (
|
||||
<Form.Item label="Values" help="Dropdown list values (newline delimited)" {...formItemProps}>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
@@ -185,7 +190,7 @@ function EditParameterSettingsDialog(props) {
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{param.type === 'query' && (
|
||||
{param.type === "query" && (
|
||||
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
|
||||
<QuerySelector
|
||||
selectedQuery={initialQuery}
|
||||
@@ -194,45 +199,54 @@ function EditParameterSettingsDialog(props) {
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{(param.type === 'enum' || param.type === 'query') && (
|
||||
{(param.type === "enum" || param.type === "query") && (
|
||||
<Form.Item className="m-b-0" label=" " colon={false} {...formItemProps}>
|
||||
<Checkbox
|
||||
defaultChecked={!!param.multiValuesOptions}
|
||||
onChange={e => setParam({ ...param,
|
||||
multiValuesOptions: e.target.checked ? {
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
separator: ',',
|
||||
} : null })}
|
||||
data-test="AllowMultipleValuesCheckbox"
|
||||
>
|
||||
onChange={e =>
|
||||
setParam({
|
||||
...param,
|
||||
multiValuesOptions: e.target.checked
|
||||
? {
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
separator: ",",
|
||||
}
|
||||
: null,
|
||||
})
|
||||
}
|
||||
data-test="AllowMultipleValuesCheckbox">
|
||||
Allow multiple values
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
)}
|
||||
{(param.type === 'enum' || param.type === 'query') && param.multiValuesOptions && (
|
||||
{(param.type === "enum" || param.type === "query") && param.multiValuesOptions && (
|
||||
<Form.Item
|
||||
label="Quotation"
|
||||
help={(
|
||||
help={
|
||||
<React.Fragment>
|
||||
Placed in query as: <code>{joinExampleList(param.multiValuesOptions)}</code>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{...formItemProps}
|
||||
>
|
||||
}
|
||||
{...formItemProps}>
|
||||
<Select
|
||||
value={param.multiValuesOptions.prefix}
|
||||
onChange={quoteOption => setParam({ ...param,
|
||||
onChange={quoteOption =>
|
||||
setParam({
|
||||
...param,
|
||||
multiValuesOptions: {
|
||||
...param.multiValuesOptions,
|
||||
prefix: quoteOption,
|
||||
suffix: quoteOption,
|
||||
} })}
|
||||
data-test="QuotationSelect"
|
||||
>
|
||||
},
|
||||
})
|
||||
}
|
||||
data-test="QuotationSelect">
|
||||
<Option value="">None (default)</Option>
|
||||
<Option value="'">Single Quotation Mark</Option>
|
||||
<Option value={'"'} data-test="DoubleQuotationMarkOption">Double Quotation Mark</Option>
|
||||
<Option value={'"'} data-test="DoubleQuotationMarkOption">
|
||||
Double Quotation Mark
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
import QueryResultsLink from './QueryResultsLink';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Menu from "antd/lib/menu";
|
||||
import Button from "antd/lib/button";
|
||||
import Icon from "antd/lib/icon";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import QueryResultsLink from "./QueryResultsLink";
|
||||
|
||||
export function QueryControlDropdown(props) {
|
||||
const menu = (
|
||||
@@ -32,8 +31,7 @@ export function QueryControlDropdown(props) {
|
||||
query={props.query}
|
||||
queryResult={props.queryResult}
|
||||
embed={props.embed}
|
||||
apiKey={props.apiKey}
|
||||
>
|
||||
apiKey={props.apiKey}>
|
||||
<Icon type="file" /> Download as CSV File
|
||||
</QueryResultsLink>
|
||||
</Menu.Item>
|
||||
@@ -44,8 +42,7 @@ export function QueryControlDropdown(props) {
|
||||
query={props.query}
|
||||
queryResult={props.queryResult}
|
||||
embed={props.embed}
|
||||
apiKey={props.apiKey}
|
||||
>
|
||||
apiKey={props.apiKey}>
|
||||
<Icon type="file-excel" /> Download as Excel File
|
||||
</QueryResultsLink>
|
||||
</Menu.Item>
|
||||
@@ -53,11 +50,7 @@ export function QueryControlDropdown(props) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={menu}
|
||||
overlayClassName="query-control-dropdown-overlay"
|
||||
>
|
||||
<Dropdown trigger={["click"]} overlay={menu} overlayClassName="query-control-dropdown-overlay">
|
||||
<Button data-test="QueryControlDropdownButton">
|
||||
<Icon type="ellipsis" rotate={90} />
|
||||
</Button>
|
||||
@@ -72,22 +65,19 @@ QueryControlDropdown.propTypes = {
|
||||
showEmbedDialog: PropTypes.func.isRequired,
|
||||
embed: PropTypes.bool,
|
||||
apiKey: PropTypes.string,
|
||||
selectedTab: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
selectedTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
openAddToDashboardForm: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
QueryControlDropdown.defaultProps = {
|
||||
queryResult: {},
|
||||
embed: false,
|
||||
apiKey: '',
|
||||
selectedTab: '',
|
||||
apiKey: "",
|
||||
selectedTab: "",
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('queryControlDropdown', react2angular(QueryControlDropdown));
|
||||
ngModule.component("queryControlDropdown", react2angular(QueryControlDropdown));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
export default function QueryResultsLink(props) {
|
||||
let href = '';
|
||||
let href = "";
|
||||
|
||||
const { query, queryResult, fileType } = props;
|
||||
const resultId = queryResult.getId && queryResult.getId();
|
||||
@@ -11,9 +10,7 @@ export default function QueryResultsLink(props) {
|
||||
|
||||
if (resultId && resultData && query.name) {
|
||||
if (query.id) {
|
||||
href = `api/queries/${query.id}/results/${resultId}.${fileType}${
|
||||
props.embed ? `?api_key=${props.apiKey}` : ''
|
||||
}`;
|
||||
href = `api/queries/${query.id}/results/${resultId}.${fileType}${props.embed ? `?api_key=${props.apiKey}` : ""}`;
|
||||
} else {
|
||||
href = `api/query_results/${resultId}.${fileType}`;
|
||||
}
|
||||
@@ -33,15 +30,12 @@ QueryResultsLink.propTypes = {
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
embed: PropTypes.bool,
|
||||
apiKey: PropTypes.string,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
|
||||
};
|
||||
|
||||
QueryResultsLink.defaultProps = {
|
||||
queryResult: {},
|
||||
fileType: 'csv',
|
||||
fileType: "csv",
|
||||
embed: false,
|
||||
apiKey: '',
|
||||
apiKey: "",
|
||||
};
|
||||
|
||||
@@ -1,39 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import Icon from "antd/lib/icon";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
export function EditVisualizationButton(props) {
|
||||
return (
|
||||
<Button
|
||||
data-test="EditVisualization"
|
||||
className="edit-visualization"
|
||||
onClick={() => props.openVisualizationEditor(props.selectedTab)}
|
||||
>
|
||||
onClick={() => props.openVisualizationEditor(props.selectedTab)}>
|
||||
<Icon type="form" />
|
||||
<span className="hidden-xs hidden-s hidden-m">
|
||||
Edit Visualization
|
||||
</span>
|
||||
<span className="hidden-xs hidden-s hidden-m">Edit Visualization</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
EditVisualizationButton.propTypes = {
|
||||
openVisualizationEditor: PropTypes.func.isRequired,
|
||||
selectedTab: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
selectedTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
EditVisualizationButton.defaultProps = {
|
||||
selectedTab: '',
|
||||
selectedTab: "",
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('editVisualizationButton', react2angular(EditVisualizationButton));
|
||||
ngModule.component("editVisualizationButton", react2angular(EditVisualizationButton));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { clientConfig, currentUser } from '@/services/auth';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Alert from 'antd/lib/alert';
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import { clientConfig, currentUser } from "@/services/auth";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import Alert from "antd/lib/alert";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
|
||||
export default function EmailSettingsWarning({ featureName, className, mode, adminOnly }) {
|
||||
if (!clientConfig.mailSettingsMissing) {
|
||||
@@ -17,33 +17,31 @@ export default function EmailSettingsWarning({ featureName, className, mode, adm
|
||||
|
||||
const message = (
|
||||
<span>
|
||||
Your mail server isn't configured correctly, and is needed for {featureName} to work.{' '}
|
||||
Your mail server isn't configured correctly, and is needed for {featureName} to work.{" "}
|
||||
<HelpTrigger type="MAIL_CONFIG" className="f-inherit" />
|
||||
</span>
|
||||
);
|
||||
|
||||
if (mode === 'icon') {
|
||||
if (mode === "icon") {
|
||||
return (
|
||||
<Tooltip title={message}>
|
||||
<i className={cx('fa fa-exclamation-triangle', className)} />
|
||||
<i className={cx("fa fa-exclamation-triangle", className)} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert message={message} type="error" className={className} />
|
||||
);
|
||||
return <Alert message={message} type="error" className={className} />;
|
||||
}
|
||||
|
||||
EmailSettingsWarning.propTypes = {
|
||||
featureName: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
mode: PropTypes.oneOf(['alert', 'icon']),
|
||||
mode: PropTypes.oneOf(["alert", "icon"]),
|
||||
adminOnly: PropTypes.bool,
|
||||
};
|
||||
|
||||
EmailSettingsWarning.defaultProps = {
|
||||
className: null,
|
||||
mode: 'alert',
|
||||
mode: "alert",
|
||||
adminOnly: false,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { $rootScope } from '@/services/ng';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import { $rootScope } from "@/services/ng";
|
||||
|
||||
export class FavoritesControl extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -16,7 +16,7 @@ export class FavoritesControl extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
forceUpdate: '',
|
||||
forceUpdate: "",
|
||||
};
|
||||
|
||||
toggleItem(event, item, callback) {
|
||||
@@ -26,21 +26,17 @@ export class FavoritesControl extends React.Component {
|
||||
action().then(() => {
|
||||
item.is_favorite = !savedIsFavorite;
|
||||
this.forceUpdate();
|
||||
$rootScope.$broadcast('reloadFavorites');
|
||||
$rootScope.$broadcast("reloadFavorites");
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { item, onChange } = this.props;
|
||||
const icon = item.is_favorite ? 'fa fa-star' : 'fa fa-star-o';
|
||||
const title = item.is_favorite ? 'Remove from favorites' : 'Add to favorites';
|
||||
const icon = item.is_favorite ? "fa fa-star" : "fa fa-star-o";
|
||||
const title = item.is_favorite ? "Remove from favorites" : "Add to favorites";
|
||||
return (
|
||||
<a
|
||||
title={title}
|
||||
className="btn-favourite"
|
||||
onClick={event => this.toggleItem(event, item, onChange)}
|
||||
>
|
||||
<a title={title} className="btn-favourite" onClick={event => this.toggleItem(event, item, onChange)}>
|
||||
<i className={icon} aria-hidden="true" />
|
||||
</a>
|
||||
);
|
||||
@@ -48,8 +44,8 @@ export class FavoritesControl extends React.Component {
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('favoritesControlImpl', react2angular(FavoritesControl));
|
||||
ngModule.component('favoritesControl', {
|
||||
ngModule.component("favoritesControlImpl", react2angular(FavoritesControl));
|
||||
ngModule.component("favoritesControl", {
|
||||
template: `
|
||||
<favorites-control-impl
|
||||
ng-if="$ctrl.item"
|
||||
@@ -59,13 +55,13 @@ export default function init(ngModule) {
|
||||
></favorites-control-impl>
|
||||
`,
|
||||
bindings: {
|
||||
item: '=',
|
||||
item: "=",
|
||||
},
|
||||
controller($scope) {
|
||||
// See comment for FavoritesControl.propTypes.forceUpdate
|
||||
this.forceUpdateTag = 'force' + Date.now();
|
||||
$scope.$on('reloadFavorites', () => {
|
||||
this.forceUpdateTag = 'force' + Date.now();
|
||||
this.forceUpdateTag = "force" + Date.now();
|
||||
$scope.$on("reloadFavorites", () => {
|
||||
this.forceUpdateTag = "force" + Date.now();
|
||||
});
|
||||
|
||||
this.onChange = () => {
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { isArray, indexOf, get, map, includes, every, some, toNumber } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Select from 'antd/lib/select';
|
||||
import { formatColumnValue } from '@/filters';
|
||||
import { isArray, indexOf, get, map, includes, every, some, toNumber } from "lodash";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import Select from "antd/lib/select";
|
||||
import { formatColumnValue } from "@/filters";
|
||||
|
||||
const ALL_VALUES = '###Redash::Filters::SelectAll###';
|
||||
const NONE_VALUES = '###Redash::Filters::Clear###';
|
||||
const ALL_VALUES = "###Redash::Filters::SelectAll###";
|
||||
const NONE_VALUES = "###Redash::Filters::Clear###";
|
||||
|
||||
export const FilterType = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
friendlyName: PropTypes.string.isRequired,
|
||||
multiple: PropTypes.bool,
|
||||
current: PropTypes.oneOfType([
|
||||
PropTypes.any,
|
||||
PropTypes.arrayOf(PropTypes.any),
|
||||
]),
|
||||
current: PropTypes.oneOfType([PropTypes.any, PropTypes.arrayOf(PropTypes.any)]),
|
||||
values: PropTypes.arrayOf(PropTypes.any).isRequired,
|
||||
});
|
||||
|
||||
@@ -49,23 +46,22 @@ export function filterData(rows, filters = []) {
|
||||
|
||||
let result = rows;
|
||||
|
||||
if (isArray(filters) && (filters.length > 0)) {
|
||||
if (isArray(filters) && filters.length > 0) {
|
||||
// "every" field's value should match "some" of corresponding filter's values
|
||||
result = result.filter(row => every(
|
||||
filters,
|
||||
(filter) => {
|
||||
result = result.filter(row =>
|
||||
every(filters, filter => {
|
||||
const rowValue = row[filter.name];
|
||||
const filterValues = isArray(filter.current) ? filter.current : [filter.current];
|
||||
return some(filterValues, (filterValue) => {
|
||||
return some(filterValues, filterValue => {
|
||||
if (moment.isMoment(rowValue)) {
|
||||
return rowValue.isSame(filterValue);
|
||||
}
|
||||
// We compare with either the value or the String representation of the value,
|
||||
// because Select2 casts true/false to "true"/"false".
|
||||
return (filterValue === rowValue) || (String(rowValue) === filterValue);
|
||||
return filterValue === rowValue || String(rowValue) === filterValue;
|
||||
});
|
||||
},
|
||||
));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -82,36 +78,45 @@ export function Filters({ filters, onChange }) {
|
||||
<div className="filters-wrapper">
|
||||
<div className="container bg-white">
|
||||
<div className="row">
|
||||
{map(filters, (filter) => {
|
||||
{map(filters, filter => {
|
||||
const options = map(filter.values, (value, index) => (
|
||||
<Select.Option key={index}>{formatColumnValue(value, get(filter, 'column.type'))}</Select.Option>
|
||||
<Select.Option key={index}>{formatColumnValue(value, get(filter, "column.type"))}</Select.Option>
|
||||
));
|
||||
|
||||
return (
|
||||
<div key={filter.name} className="col-sm-6 p-l-0 filter-container">
|
||||
<label>{filter.friendlyName}</label>
|
||||
{(options.length === 0) && (
|
||||
<Select className="w-100" disabled value="No values" />
|
||||
)}
|
||||
{(options.length > 0) && (
|
||||
{options.length === 0 && <Select className="w-100" disabled value="No values" />}
|
||||
{options.length > 0 && (
|
||||
<Select
|
||||
labelInValue
|
||||
className="w-100"
|
||||
mode={filter.multiple ? 'multiple' : 'default'}
|
||||
value={isArray(filter.current) ?
|
||||
map(filter.current,
|
||||
value => ({ key: `${indexOf(filter.values, value)}`, label: formatColumnValue(value) })) :
|
||||
({ key: `${indexOf(filter.values, filter.current)}`, label: formatColumnValue(filter.current) })}
|
||||
mode={filter.multiple ? "multiple" : "default"}
|
||||
value={
|
||||
isArray(filter.current)
|
||||
? map(filter.current, value => ({
|
||||
key: `${indexOf(filter.values, value)}`,
|
||||
label: formatColumnValue(value),
|
||||
}))
|
||||
: { key: `${indexOf(filter.values, filter.current)}`, label: formatColumnValue(filter.current) }
|
||||
}
|
||||
allowClear={filter.multiple}
|
||||
optionFilterProp="children"
|
||||
showSearch
|
||||
onChange={values => onChange(filter, values)}
|
||||
>
|
||||
onChange={values => onChange(filter, values)}>
|
||||
{!filter.multiple && options}
|
||||
{filter.multiple && [
|
||||
<Select.Option key={NONE_VALUES}><i className="fa fa-square-o m-r-5" />Clear</Select.Option>,
|
||||
<Select.Option key={ALL_VALUES}><i className="fa fa-check-square-o m-r-5" />Select All</Select.Option>,
|
||||
<Select.OptGroup key="Values" title="Values">{options}</Select.OptGroup>,
|
||||
<Select.Option key={NONE_VALUES}>
|
||||
<i className="fa fa-square-o m-r-5" />
|
||||
Clear
|
||||
</Select.Option>,
|
||||
<Select.Option key={ALL_VALUES}>
|
||||
<i className="fa fa-check-square-o m-r-5" />
|
||||
Select All
|
||||
</Select.Option>,
|
||||
<Select.OptGroup key="Values" title="Values">
|
||||
{options}
|
||||
</Select.OptGroup>,
|
||||
]}
|
||||
</Select>
|
||||
)}
|
||||
@@ -134,7 +139,7 @@ Filters.defaultProps = {
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('filters', react2angular(Filters));
|
||||
ngModule.component("filters", react2angular(Filters));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,97 +1,43 @@
|
||||
import { startsWith } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Drawer from 'antd/lib/drawer';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { BigMessage } from '@/components/BigMessage';
|
||||
import DynamicComponent from '@/components/DynamicComponent';
|
||||
import { startsWith } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import Drawer from "antd/lib/drawer";
|
||||
import Icon from "antd/lib/icon";
|
||||
import { BigMessage } from "@/components/BigMessage";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
|
||||
import './HelpTrigger.less';
|
||||
import "./HelpTrigger.less";
|
||||
|
||||
const DOMAIN = 'https://redash.io';
|
||||
const HELP_PATH = '/help';
|
||||
const DOMAIN = "https://redash.io";
|
||||
const HELP_PATH = "/help";
|
||||
const IFRAME_TIMEOUT = 20000;
|
||||
const IFRAME_URL_UPDATE_MESSAGE = 'iframe_url';
|
||||
const IFRAME_URL_UPDATE_MESSAGE = "iframe_url";
|
||||
|
||||
export const TYPES = {
|
||||
HOME: [
|
||||
'',
|
||||
'Help',
|
||||
],
|
||||
VALUE_SOURCE_OPTIONS: [
|
||||
'/user-guide/querying/query-parameters#Value-Source-Options',
|
||||
'Guide: Value Source Options',
|
||||
],
|
||||
SHARE_DASHBOARD: [
|
||||
'/user-guide/dashboards/sharing-dashboards',
|
||||
'Guide: Sharing and Embedding Dashboards',
|
||||
],
|
||||
AUTHENTICATION_OPTIONS: [
|
||||
'/user-guide/users/authentication-options',
|
||||
'Guide: Authentication Options',
|
||||
],
|
||||
USAGE_DATA_SHARING: [
|
||||
'/open-source/admin-guide/usage-data',
|
||||
'Help: Anonymous Usage Data Sharing',
|
||||
],
|
||||
DS_ATHENA: [
|
||||
'/data-sources/amazon-athena-setup',
|
||||
'Guide: Help Setting up Amazon Athena',
|
||||
],
|
||||
DS_BIGQUERY: [
|
||||
'/data-sources/bigquery-setup',
|
||||
'Guide: Help Setting up BigQuery',
|
||||
],
|
||||
DS_URL: [
|
||||
'/data-sources/querying-urls',
|
||||
'Guide: Help Setting up URL',
|
||||
],
|
||||
DS_MONGODB: [
|
||||
'/data-sources/mongodb-setup',
|
||||
'Guide: Help Setting up MongoDB',
|
||||
],
|
||||
DS_GOOGLE_SPREADSHEETS: [
|
||||
'/data-sources/querying-a-google-spreadsheet',
|
||||
'Guide: Help Setting up Google Spreadsheets',
|
||||
],
|
||||
DS_GOOGLE_ANALYTICS: [
|
||||
'/data-sources/google-analytics-setup',
|
||||
'Guide: Help Setting up Google Analytics',
|
||||
],
|
||||
DS_AXIBASETSD: [
|
||||
'/data-sources/axibase-time-series-database',
|
||||
'Guide: Help Setting up Axibase Time Series',
|
||||
],
|
||||
DS_RESULTS: [
|
||||
'/user-guide/querying/query-results-data-source',
|
||||
'Guide: Help Setting up Query Results',
|
||||
],
|
||||
ALERT_SETUP: [
|
||||
'/user-guide/alerts/setting-up-an-alert',
|
||||
'Guide: Setting Up a New Alert',
|
||||
],
|
||||
MAIL_CONFIG: [
|
||||
'/open-source/setup/#Mail-Configuration',
|
||||
'Guide: Mail Configuration',
|
||||
],
|
||||
ALERT_NOTIF_TEMPLATE_GUIDE: [
|
||||
'/user-guide/alerts/custom-alert-notifications',
|
||||
'Guide: Custom Alerts Notifications',
|
||||
],
|
||||
FAVORITES: [
|
||||
'/user-guide/querying/favorites-tagging/#Favorites',
|
||||
'Guide: Favorites',
|
||||
],
|
||||
HOME: ["", "Help"],
|
||||
VALUE_SOURCE_OPTIONS: ["/user-guide/querying/query-parameters#Value-Source-Options", "Guide: Value Source Options"],
|
||||
SHARE_DASHBOARD: ["/user-guide/dashboards/sharing-dashboards", "Guide: Sharing and Embedding Dashboards"],
|
||||
AUTHENTICATION_OPTIONS: ["/user-guide/users/authentication-options", "Guide: Authentication Options"],
|
||||
USAGE_DATA_SHARING: ["/open-source/admin-guide/usage-data", "Help: Anonymous Usage Data Sharing"],
|
||||
DS_ATHENA: ["/data-sources/amazon-athena-setup", "Guide: Help Setting up Amazon Athena"],
|
||||
DS_BIGQUERY: ["/data-sources/bigquery-setup", "Guide: Help Setting up BigQuery"],
|
||||
DS_URL: ["/data-sources/querying-urls", "Guide: Help Setting up URL"],
|
||||
DS_MONGODB: ["/data-sources/mongodb-setup", "Guide: Help Setting up MongoDB"],
|
||||
DS_GOOGLE_SPREADSHEETS: ["/data-sources/querying-a-google-spreadsheet", "Guide: Help Setting up Google Spreadsheets"],
|
||||
DS_GOOGLE_ANALYTICS: ["/data-sources/google-analytics-setup", "Guide: Help Setting up Google Analytics"],
|
||||
DS_AXIBASETSD: ["/data-sources/axibase-time-series-database", "Guide: Help Setting up Axibase Time Series"],
|
||||
DS_RESULTS: ["/user-guide/querying/query-results-data-source", "Guide: Help Setting up Query Results"],
|
||||
ALERT_SETUP: ["/user-guide/alerts/setting-up-an-alert", "Guide: Setting Up a New Alert"],
|
||||
MAIL_CONFIG: ["/open-source/setup/#Mail-Configuration", "Guide: Mail Configuration"],
|
||||
ALERT_NOTIF_TEMPLATE_GUIDE: ["/user-guide/alerts/custom-alert-notifications", "Guide: Custom Alerts Notifications"],
|
||||
FAVORITES: ["/user-guide/querying/favorites-tagging/#Favorites", "Guide: Favorites"],
|
||||
MANAGE_PERMISSIONS: [
|
||||
'/user-guide/querying/writing-queries#Managing-Query-Permissions',
|
||||
'Guide: Managing Query Permissions',
|
||||
],
|
||||
NUMBER_FORMAT_SPECS: [
|
||||
'/user-guide/visualizations/formatting-numbers',
|
||||
'Formatting Numbers',
|
||||
"/user-guide/querying/writing-queries#Managing-Query-Permissions",
|
||||
"Guide: Managing Query Permissions",
|
||||
],
|
||||
NUMBER_FORMAT_SPECS: ["/user-guide/visualizations/formatting-numbers", "Formatting Numbers"],
|
||||
};
|
||||
|
||||
export default class HelpTrigger extends React.Component {
|
||||
@@ -120,15 +66,15 @@ export default class HelpTrigger extends React.Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.onPostMessageReceived, false);
|
||||
window.addEventListener("message", this.onPostMessageReceived, false);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.onPostMessageReceived);
|
||||
window.removeEventListener("message", this.onPostMessageReceived);
|
||||
clearTimeout(this.iframeLoadingTimeout);
|
||||
}
|
||||
|
||||
loadIframe = (url) => {
|
||||
loadIframe = url => {
|
||||
clearTimeout(this.iframeLoadingTimeout);
|
||||
this.setState({ loading: true, error: false });
|
||||
|
||||
@@ -143,7 +89,7 @@ export default class HelpTrigger extends React.Component {
|
||||
clearTimeout(this.iframeLoadingTimeout);
|
||||
};
|
||||
|
||||
onPostMessageReceived = (event) => {
|
||||
onPostMessageReceived = event => {
|
||||
if (!startsWith(event.origin, DOMAIN)) {
|
||||
return;
|
||||
}
|
||||
@@ -165,7 +111,7 @@ export default class HelpTrigger extends React.Component {
|
||||
setTimeout(() => this.loadIframe(url), 300);
|
||||
};
|
||||
|
||||
closeDrawer = (event) => {
|
||||
closeDrawer = event => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -175,7 +121,7 @@ export default class HelpTrigger extends React.Component {
|
||||
|
||||
render() {
|
||||
const [, tooltip] = TYPES[this.props.type];
|
||||
const className = cx('help-trigger', this.props.className);
|
||||
const className = cx("help-trigger", this.props.className);
|
||||
const url = this.state.currentUrl;
|
||||
|
||||
return (
|
||||
@@ -192,8 +138,7 @@ export default class HelpTrigger extends React.Component {
|
||||
visible={this.state.visible}
|
||||
className="help-drawer"
|
||||
destroyOnClose
|
||||
width={400}
|
||||
>
|
||||
width={400}>
|
||||
<div className="drawer-wrapper">
|
||||
<div className="drawer-menu">
|
||||
{url && (
|
||||
@@ -230,20 +175,19 @@ export default class HelpTrigger extends React.Component {
|
||||
{/* error message */}
|
||||
{this.state.error && (
|
||||
<BigMessage icon="fa-exclamation-circle" className="help-message">
|
||||
Something went wrong.<br />
|
||||
Something went wrong.
|
||||
<br />
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a href={this.state.error} target="_blank" rel="noopener">Click here</a>{' '}
|
||||
<a href={this.state.error} target="_blank" rel="noopener">
|
||||
Click here
|
||||
</a>{" "}
|
||||
to open the page in a new window.
|
||||
</BigMessage>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* extra content */}
|
||||
<DynamicComponent
|
||||
name="HelpDrawerExtraContent"
|
||||
onLeave={this.closeDrawer}
|
||||
openPageUrl={this.loadIframe}
|
||||
/>
|
||||
<DynamicComponent name="HelpDrawerExtraContent" onLeave={this.closeDrawer} openPageUrl={this.loadIframe} />
|
||||
</Drawer>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { $sanitize } from '@/services/ng';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { $sanitize } from "@/services/ng";
|
||||
|
||||
export default function HtmlContent({ children, ...props }) {
|
||||
return (
|
||||
@@ -16,5 +16,5 @@ HtmlContent.propTypes = {
|
||||
};
|
||||
|
||||
HtmlContent.defaultProps = {
|
||||
children: '',
|
||||
children: "",
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import Input from 'antd/lib/input';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import React from "react";
|
||||
import Input from "antd/lib/input";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
export default class InputWithCopy extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { copied: null };
|
||||
this.ref = React.createRef();
|
||||
this.copyFeatureSupported = document.queryCommandSupported('copy');
|
||||
this.copyFeatureSupported = document.queryCommandSupported("copy");
|
||||
this.resetCopyState = null;
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ export default class InputWithCopy extends React.Component {
|
||||
|
||||
// copy
|
||||
try {
|
||||
const success = document.execCommand('copy');
|
||||
const success = document.execCommand("copy");
|
||||
if (!success) {
|
||||
throw new Error();
|
||||
}
|
||||
this.setState({ copied: 'Copied!' });
|
||||
this.setState({ copied: "Copied!" });
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
copied: 'Copy failed',
|
||||
copied: "Copy failed",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,17 +41,11 @@ export default class InputWithCopy extends React.Component {
|
||||
|
||||
render() {
|
||||
const copyButton = (
|
||||
<Tooltip title={this.state.copied || 'Copy'}>
|
||||
<Icon
|
||||
type="copy"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={this.copy}
|
||||
/>
|
||||
<Tooltip title={this.state.copied || "Copy"}>
|
||||
<Icon type="copy" style={{ cursor: "pointer" }} onClick={this.copy} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<Input {...this.props} ref={this.ref} addonAfter={this.copyFeatureSupported && copyButton} />
|
||||
);
|
||||
return <Input {...this.props} ref={this.ref} addonAfter={this.copyFeatureSupported && copyButton} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { BigMessage } from '@/components/BigMessage';
|
||||
import { TagsControl } from '@/components/tags-control/TagsControl';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import { BigMessage } from "@/components/BigMessage";
|
||||
import { TagsControl } from "@/components/tags-control/TagsControl";
|
||||
|
||||
export function NoTaggedObjectsFound({ objectType, tags }) {
|
||||
return (
|
||||
<BigMessage icon="fa-tags">
|
||||
No {objectType} found tagged with <TagsControl className="inline-tags-control" tags={Array.from(tags)} />.
|
||||
No {objectType} found tagged with
|
||||
<TagsControl className="inline-tags-control" tags={Array.from(tags)} />.
|
||||
</BigMessage>
|
||||
);
|
||||
}
|
||||
|
||||
NoTaggedObjectsFound.propTypes = {
|
||||
objectType: PropTypes.string.isRequired,
|
||||
tags: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.objectOf(Set),
|
||||
]).isRequired,
|
||||
tags: PropTypes.oneOfType([PropTypes.array, PropTypes.objectOf(Set)]).isRequired,
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('noTaggedObjectsFound', react2angular(NoTaggedObjectsFound));
|
||||
ngModule.component("noTaggedObjectsFound", react2angular(NoTaggedObjectsFound));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
export function PageHeader({ title }) {
|
||||
return (
|
||||
@@ -17,7 +17,7 @@ PageHeader.propTypes = {
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('pageHeader', react2angular(PageHeader));
|
||||
ngModule.component("pageHeader", react2angular(PageHeader));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Pagination from 'antd/lib/pagination';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import Pagination from "antd/lib/pagination";
|
||||
|
||||
export function Paginator({
|
||||
page,
|
||||
itemsPerPage,
|
||||
totalCount,
|
||||
onChange,
|
||||
}) {
|
||||
export function Paginator({ page, itemsPerPage, totalCount, onChange }) {
|
||||
if (totalCount <= itemsPerPage) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="paginator-container">
|
||||
<Pagination
|
||||
defaultCurrent={page}
|
||||
defaultPageSize={itemsPerPage}
|
||||
total={totalCount}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Pagination defaultCurrent={page} defaultPageSize={itemsPerPage} total={totalCount} onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -36,8 +26,8 @@ Paginator.defaultProps = {
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('paginatorImpl', react2angular(Paginator));
|
||||
ngModule.component('paginator', {
|
||||
ngModule.component("paginatorImpl", react2angular(Paginator));
|
||||
ngModule.component("paginator", {
|
||||
template: `
|
||||
<paginator-impl
|
||||
page="$ctrl.paginator.page"
|
||||
@@ -46,10 +36,10 @@ export default function init(ngModule) {
|
||||
on-change="$ctrl.onPageChanged"
|
||||
></paginator-impl>`,
|
||||
bindings: {
|
||||
paginator: '<',
|
||||
paginator: "<",
|
||||
},
|
||||
controller($scope) {
|
||||
this.onPageChanged = (page) => {
|
||||
this.onPageChanged = page => {
|
||||
this.paginator.setPage(page);
|
||||
$scope.$applyAsync();
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'antd/lib/button';
|
||||
import Badge from 'antd/lib/badge';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { KeyboardShortcuts } from '@/services/keyboard-shortcuts';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import Badge from "antd/lib/badge";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import { KeyboardShortcuts } from "@/services/keyboard-shortcuts";
|
||||
|
||||
function ParameterApplyButton({ paramCount, onClick }) {
|
||||
// show spinner when count is empty so the fade out is consistent
|
||||
const icon = !paramCount ? 'spinner fa-pulse' : 'check';
|
||||
const icon = !paramCount ? "spinner fa-pulse" : "check";
|
||||
|
||||
return (
|
||||
<div className="parameter-apply-button" data-show={!!paramCount} data-test="ParameterApplyButton">
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
|
||||
import { isString, extend, each, has, map, includes, findIndex, find,
|
||||
fromPairs, clone, isEmpty } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Select from 'antd/lib/select';
|
||||
import Table from 'antd/lib/table';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Tag from 'antd/lib/tag';
|
||||
import Input from 'antd/lib/input';
|
||||
import Radio from 'antd/lib/radio';
|
||||
import Form from 'antd/lib/form';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import ParameterValueInput from '@/components/ParameterValueInput';
|
||||
import { ParameterMappingType } from '@/services/widget';
|
||||
import { Parameter } from '@/services/parameters';
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import { isString, extend, each, has, map, includes, findIndex, find, fromPairs, clone, isEmpty } from "lodash";
|
||||
import React, { Fragment } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import Select from "antd/lib/select";
|
||||
import Table from "antd/lib/table";
|
||||
import Popover from "antd/lib/popover";
|
||||
import Button from "antd/lib/button";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Tag from "antd/lib/tag";
|
||||
import Input from "antd/lib/input";
|
||||
import Radio from "antd/lib/radio";
|
||||
import Form from "antd/lib/form";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import ParameterValueInput from "@/components/ParameterValueInput";
|
||||
import { ParameterMappingType } from "@/services/widget";
|
||||
import { Parameter } from "@/services/parameters";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
|
||||
import './ParameterMappingInput.less';
|
||||
import "./ParameterMappingInput.less";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export const MappingType = {
|
||||
DashboardAddNew: 'dashboard-add-new',
|
||||
DashboardMapToExisting: 'dashboard-map-to-existing',
|
||||
WidgetLevel: 'widget-level',
|
||||
StaticValue: 'static-value',
|
||||
DashboardAddNew: "dashboard-add-new",
|
||||
DashboardMapToExisting: "dashboard-map-to-existing",
|
||||
WidgetLevel: "widget-level",
|
||||
StaticValue: "static-value",
|
||||
};
|
||||
|
||||
export function parameterMappingsToEditableMappings(mappings, parameters, existingParameterNames = []) {
|
||||
return map(mappings, (mapping) => {
|
||||
return map(mappings, mapping => {
|
||||
const result = extend({}, mapping);
|
||||
const alreadyExists = includes(existingParameterNames, mapping.mapTo);
|
||||
result.param = find(parameters, p => p.name === mapping.name);
|
||||
@@ -57,9 +56,11 @@ export function parameterMappingsToEditableMappings(mappings, parameters, existi
|
||||
}
|
||||
|
||||
export function editableMappingsToParameterMappings(mappings) {
|
||||
return fromPairs(map( // convert to map
|
||||
return fromPairs(
|
||||
map(
|
||||
// convert to map
|
||||
mappings,
|
||||
(mapping) => {
|
||||
mapping => {
|
||||
const result = extend({}, mapping);
|
||||
switch (mapping.type) {
|
||||
case MappingType.DashboardAddNew:
|
||||
@@ -84,22 +85,23 @@ export function editableMappingsToParameterMappings(mappings) {
|
||||
}
|
||||
delete result.param;
|
||||
return [result.name, result];
|
||||
},
|
||||
));
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function synchronizeWidgetTitles(sourceMappings, widgets) {
|
||||
const affectedWidgets = [];
|
||||
|
||||
each(sourceMappings, (sourceMapping) => {
|
||||
each(sourceMappings, sourceMapping => {
|
||||
if (sourceMapping.type === ParameterMappingType.DashboardLevel) {
|
||||
each(widgets, (widget) => {
|
||||
each(widgets, widget => {
|
||||
const widgetMappings = widget.options.parameterMappings;
|
||||
each(widgetMappings, (widgetMapping) => {
|
||||
each(widgetMappings, widgetMapping => {
|
||||
// check if mapped to the same dashboard-level parameter
|
||||
if (
|
||||
(widgetMapping.type === ParameterMappingType.DashboardLevel) &&
|
||||
(widgetMapping.mapTo === sourceMapping.mapTo)
|
||||
widgetMapping.type === ParameterMappingType.DashboardLevel &&
|
||||
widgetMapping.mapTo === sourceMapping.mapTo
|
||||
) {
|
||||
// dirty check - update only when needed
|
||||
if (widgetMapping.title !== sourceMapping.title) {
|
||||
@@ -133,33 +135,32 @@ export class ParameterMappingInput extends React.Component {
|
||||
formItemProps = {
|
||||
labelCol: { span: 5 },
|
||||
wrapperCol: { span: 16 },
|
||||
className: 'form-item',
|
||||
className: "form-item",
|
||||
};
|
||||
|
||||
updateSourceType = (type) => {
|
||||
let { mapping: { mapTo } } = this.props;
|
||||
updateSourceType = type => {
|
||||
let {
|
||||
mapping: { mapTo },
|
||||
} = this.props;
|
||||
const { existingParamNames } = this.props;
|
||||
|
||||
// if mapped name doesn't already exists
|
||||
// default to first select option
|
||||
if (
|
||||
type === MappingType.DashboardMapToExisting &&
|
||||
!includes(existingParamNames, mapTo)
|
||||
) {
|
||||
if (type === MappingType.DashboardMapToExisting && !includes(existingParamNames, mapTo)) {
|
||||
mapTo = existingParamNames[0];
|
||||
}
|
||||
|
||||
this.updateParamMapping({ type, mapTo });
|
||||
};
|
||||
|
||||
updateParamMapping = (update) => {
|
||||
updateParamMapping = update => {
|
||||
const { onChange, mapping } = this.props;
|
||||
const newMapping = extend({}, mapping, update);
|
||||
if (newMapping.value !== mapping.value) {
|
||||
newMapping.param = newMapping.param.clone();
|
||||
newMapping.param.setValue(newMapping.value);
|
||||
}
|
||||
if (has(update, 'type')) {
|
||||
if (has(update, "type")) {
|
||||
if (update.type === MappingType.StaticValue) {
|
||||
newMapping.value = newMapping.param.value;
|
||||
} else {
|
||||
@@ -172,19 +173,12 @@ export class ParameterMappingInput extends React.Component {
|
||||
renderMappingTypeSelector() {
|
||||
const noExisting = isEmpty(this.props.existingParamNames);
|
||||
return (
|
||||
<Radio.Group
|
||||
value={this.props.mapping.type}
|
||||
onChange={e => this.updateSourceType(e.target.value)}
|
||||
>
|
||||
<Radio.Group value={this.props.mapping.type} onChange={e => this.updateSourceType(e.target.value)}>
|
||||
<Radio className="radio" value={MappingType.DashboardAddNew} data-test="NewDashboardParameterOption">
|
||||
New dashboard parameter
|
||||
</Radio>
|
||||
<Radio
|
||||
className="radio"
|
||||
value={MappingType.DashboardMapToExisting}
|
||||
disabled={noExisting}
|
||||
>
|
||||
Existing dashboard parameter{' '}
|
||||
<Radio className="radio" value={MappingType.DashboardMapToExisting} disabled={noExisting}>
|
||||
Existing dashboard parameter{" "}
|
||||
{noExisting ? (
|
||||
<Tooltip title="There are no dashboard parameters corresponding to this data type">
|
||||
<Icon type="question-circle" theme="filled" />
|
||||
@@ -202,13 +196,10 @@ export class ParameterMappingInput extends React.Component {
|
||||
}
|
||||
|
||||
renderDashboardAddNew() {
|
||||
const { mapping: { mapTo } } = this.props;
|
||||
return (
|
||||
<Input
|
||||
value={mapTo}
|
||||
onChange={e => this.updateParamMapping({ mapTo: e.target.value })}
|
||||
/>
|
||||
);
|
||||
const {
|
||||
mapping: { mapTo },
|
||||
} = this.props;
|
||||
return <Input value={mapTo} onChange={e => this.updateParamMapping({ mapTo: e.target.value })} />;
|
||||
}
|
||||
|
||||
renderDashboardMapToExisting() {
|
||||
@@ -218,10 +209,11 @@ export class ParameterMappingInput extends React.Component {
|
||||
<Select
|
||||
value={mapping.mapTo}
|
||||
onChange={mapTo => this.updateParamMapping({ mapTo })}
|
||||
dropdownMatchSelectWidth={false}
|
||||
>
|
||||
dropdownMatchSelectWidth={false}>
|
||||
{map(existingParamNames, name => (
|
||||
<Option value={name} key={name}>{ name }</Option>
|
||||
<Option value={name} key={name}>
|
||||
{name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
@@ -245,24 +237,13 @@ export class ParameterMappingInput extends React.Component {
|
||||
const { mapping } = this.props;
|
||||
switch (mapping.type) {
|
||||
case MappingType.DashboardAddNew:
|
||||
return [
|
||||
'Key',
|
||||
'Enter a new parameter keyword',
|
||||
this.renderDashboardAddNew(),
|
||||
];
|
||||
return ["Key", "Enter a new parameter keyword", this.renderDashboardAddNew()];
|
||||
case MappingType.DashboardMapToExisting:
|
||||
return [
|
||||
'Key',
|
||||
'Select from a list of existing parameters',
|
||||
this.renderDashboardMapToExisting(),
|
||||
];
|
||||
return ["Key", "Select from a list of existing parameters", this.renderDashboardMapToExisting()];
|
||||
case MappingType.StaticValue:
|
||||
return [
|
||||
'Value',
|
||||
null,
|
||||
this.renderStaticValue(),
|
||||
];
|
||||
default: return [];
|
||||
return ["Value", null, this.renderStaticValue()];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,10 +257,10 @@ export class ParameterMappingInput extends React.Component {
|
||||
{this.renderMappingTypeSelector()}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
style={{ height: 60, visibility: input ? 'visible' : 'hidden' }}
|
||||
style={{ height: 60, visibility: input ? "visible" : "hidden" }}
|
||||
label={label}
|
||||
{...this.formItemProps}
|
||||
validateStatus={inputError ? 'error' : ''}
|
||||
validateStatus={inputError ? "error" : ""}
|
||||
help={inputError || help} // empty space so line doesn't collapse
|
||||
>
|
||||
{input}
|
||||
@@ -305,18 +286,19 @@ class MappingEditor extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
onVisibleChange = (visible) => {
|
||||
if (visible) this.show(); else this.hide();
|
||||
onVisibleChange = visible => {
|
||||
if (visible) this.show();
|
||||
else this.hide();
|
||||
};
|
||||
|
||||
onChange = (mapping) => {
|
||||
onChange = mapping => {
|
||||
let inputError = null;
|
||||
|
||||
if (mapping.type === MappingType.DashboardAddNew) {
|
||||
if (isEmpty(mapping.mapTo)) {
|
||||
inputError = 'Keyword must have a value';
|
||||
inputError = "Keyword must have a value";
|
||||
} else if (includes(this.props.existingParamNames, mapping.mapTo)) {
|
||||
inputError = 'A parameter with this name already exists';
|
||||
inputError = "A parameter with this name already exists";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +337,9 @@ class MappingEditor extends React.Component {
|
||||
/>
|
||||
<footer>
|
||||
<Button onClick={this.hide}>Cancel</Button>
|
||||
<Button onClick={this.save} disabled={!!inputError} type="primary">OK</Button>
|
||||
<Button onClick={this.save} disabled={!!inputError} type="primary">
|
||||
OK
|
||||
</Button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
@@ -369,8 +353,7 @@ class MappingEditor extends React.Component {
|
||||
trigger="click"
|
||||
content={this.renderContent()}
|
||||
visible={visible}
|
||||
onVisibleChange={this.onVisibleChange}
|
||||
>
|
||||
onVisibleChange={this.onVisibleChange}>
|
||||
<Button size="small" type="dashed" data-test={`EditParamMappingButon-${mapping.param.name}`}>
|
||||
<Icon type="edit" />
|
||||
</Button>
|
||||
@@ -392,24 +375,24 @@ class TitleEditor extends React.Component {
|
||||
|
||||
state = {
|
||||
showPopup: false,
|
||||
title: '', // will be set on editing
|
||||
title: "", // will be set on editing
|
||||
};
|
||||
|
||||
onPopupVisibleChange = (showPopup) => {
|
||||
onPopupVisibleChange = showPopup => {
|
||||
this.setState({
|
||||
showPopup,
|
||||
title: showPopup ? this.getMappingTitle() : '',
|
||||
title: showPopup ? this.getMappingTitle() : "",
|
||||
});
|
||||
};
|
||||
|
||||
onEditingTitleChange = (event) => {
|
||||
onEditingTitleChange = event => {
|
||||
this.setState({ title: event.target.value });
|
||||
};
|
||||
|
||||
getMappingTitle() {
|
||||
let { mapping } = this.props;
|
||||
|
||||
if (isString(mapping.title) && (mapping.title !== '')) {
|
||||
if (isString(mapping.title) && mapping.title !== "") {
|
||||
return mapping.title;
|
||||
}
|
||||
|
||||
@@ -435,7 +418,9 @@ class TitleEditor extends React.Component {
|
||||
};
|
||||
|
||||
renderPopover() {
|
||||
const { param: { title: paramTitle } } = this.props.mapping;
|
||||
const {
|
||||
param: { title: paramTitle },
|
||||
} = this.props.mapping;
|
||||
|
||||
return (
|
||||
<div className="parameter-mapping-title-editor">
|
||||
@@ -473,8 +458,7 @@ class TitleEditor extends React.Component {
|
||||
trigger="click"
|
||||
content={this.renderPopover()}
|
||||
visible={this.state.showPopup}
|
||||
onVisibleChange={this.onPopupVisibleChange}
|
||||
>
|
||||
onVisibleChange={this.onPopupVisibleChange}>
|
||||
<Button size="small" type="dashed">
|
||||
<Icon type="edit" />
|
||||
</Button>
|
||||
@@ -488,7 +472,7 @@ class TitleEditor extends React.Component {
|
||||
const disabled = mapping.type === MappingType.StaticValue;
|
||||
|
||||
return (
|
||||
<div className={classNames('parameter-mapping-title', { disabled })}>
|
||||
<div className={classNames("parameter-mapping-title", { disabled })}>
|
||||
<span className="text">{this.getMappingTitle()}</span>
|
||||
{this.renderEditButton()}
|
||||
</div>
|
||||
@@ -512,17 +496,17 @@ export class ParameterMappingListInput extends React.Component {
|
||||
static getStringValue(value) {
|
||||
// null
|
||||
if (!value) {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
|
||||
// range
|
||||
if (value instanceof Object && 'start' in value && 'end' in value) {
|
||||
if (value instanceof Object && "start" in value && "end" in value) {
|
||||
return `${value.start} ~ ${value.end}`;
|
||||
}
|
||||
|
||||
// just to be safe, array or object
|
||||
if (typeof value === 'object') {
|
||||
return map(value, v => this.getStringValue(v)).join(', ');
|
||||
if (typeof value === "object") {
|
||||
return map(value, v => this.getStringValue(v)).join(", ");
|
||||
}
|
||||
|
||||
// rest
|
||||
@@ -536,7 +520,8 @@ export class ParameterMappingListInput extends React.Component {
|
||||
// if mapped to another param, swap 'em
|
||||
if (type === MappingType.DashboardMapToExisting && mapTo !== name) {
|
||||
const mappedTo = find(existingParams, { name: mapTo });
|
||||
if (mappedTo) { // just being safe
|
||||
if (mappedTo) {
|
||||
// just being safe
|
||||
param = mappedTo;
|
||||
}
|
||||
|
||||
@@ -561,16 +546,15 @@ export class ParameterMappingListInput extends React.Component {
|
||||
case MappingType.DashboardMapToExisting:
|
||||
return (
|
||||
<Fragment>
|
||||
Dashboard{' '}
|
||||
<Tag className="tag">{mapTo}</Tag>
|
||||
Dashboard <Tag className="tag">{mapTo}</Tag>
|
||||
</Fragment>
|
||||
);
|
||||
case MappingType.WidgetLevel:
|
||||
return 'Widget parameter';
|
||||
return "Widget parameter";
|
||||
case MappingType.StaticValue:
|
||||
return 'Static value';
|
||||
return "Static value";
|
||||
default:
|
||||
return ''; // won't happen (typescript-ftw)
|
||||
return ""; // won't happen (typescript-ftw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,12 +576,7 @@ export class ParameterMappingListInput extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="parameters-mapping-list">
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
size="middle"
|
||||
pagination={false}
|
||||
rowKey={(record, idx) => `row${idx}`}
|
||||
>
|
||||
<Table dataSource={dataSource} size="middle" pagination={false} rowKey={(record, idx) => `row${idx}`}>
|
||||
<Table.Column
|
||||
title="Title"
|
||||
dataIndex="mapping"
|
||||
@@ -621,22 +600,20 @@ export class ParameterMappingListInput extends React.Component {
|
||||
title="Default Value"
|
||||
dataIndex="mapping"
|
||||
key="value"
|
||||
render={mapping => (
|
||||
this.constructor.getDefaultValue(mapping, this.props.existingParams)
|
||||
)}
|
||||
render={mapping => this.constructor.getDefaultValue(mapping, this.props.existingParams)}
|
||||
/>
|
||||
<Table.Column
|
||||
title="Value Source"
|
||||
dataIndex="mapping"
|
||||
key="source"
|
||||
render={(mapping) => {
|
||||
render={mapping => {
|
||||
const existingParamsNames = existingParams
|
||||
.filter(({ type }) => type === mapping.param.type) // exclude mismatching param types
|
||||
.map(({ name }) => name); // keep names only
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{this.constructor.getSourceTypeLabel(mapping)}{' '}
|
||||
{this.constructor.getSourceTypeLabel(mapping)}{" "}
|
||||
<MappingEditor
|
||||
mapping={mapping}
|
||||
existingParamNames={existingParamsNames}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
import DateParameter from '@/components/dynamic-parameters/DateParameter';
|
||||
import DateRangeParameter from '@/components/dynamic-parameters/DateRangeParameter';
|
||||
import { isEqual } from 'lodash';
|
||||
import { QueryBasedParameterInput } from './QueryBasedParameterInput';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Select from "antd/lib/select";
|
||||
import Input from "antd/lib/input";
|
||||
import InputNumber from "antd/lib/input-number";
|
||||
import DateParameter from "@/components/dynamic-parameters/DateParameter";
|
||||
import DateRangeParameter from "@/components/dynamic-parameters/DateRangeParameter";
|
||||
import { isEqual } from "lodash";
|
||||
import { QueryBasedParameterInput } from "./QueryBasedParameterInput";
|
||||
|
||||
import './ParameterValueInput.less';
|
||||
import "./ParameterValueInput.less";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -30,13 +30,13 @@ class ParameterValueInput extends React.Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
type: 'text',
|
||||
type: "text",
|
||||
value: null,
|
||||
enumOptions: '',
|
||||
enumOptions: "",
|
||||
queryId: null,
|
||||
parameter: null,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -47,7 +47,7 @@ class ParameterValueInput extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
componentDidUpdate = prevProps => {
|
||||
const { value, parameter } = this.props;
|
||||
// if value prop updated, reset dirty state
|
||||
if (prevProps.value !== value || prevProps.parameter !== parameter) {
|
||||
@@ -56,13 +56,13 @@ class ParameterValueInput extends React.Component {
|
||||
isDirty: parameter.hasPendingValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onSelect = (value) => {
|
||||
onSelect = value => {
|
||||
const isDirty = !isEqual(value, this.props.value);
|
||||
this.setState({ value, isDirty });
|
||||
this.props.onSelect(value, isDirty);
|
||||
}
|
||||
};
|
||||
|
||||
renderDateParameter() {
|
||||
const { type, parameter } = this.props;
|
||||
@@ -95,13 +95,13 @@ class ParameterValueInput extends React.Component {
|
||||
renderEnumInput() {
|
||||
const { enumOptions, parameter } = this.props;
|
||||
const { value } = this.state;
|
||||
const enumOptionsArray = enumOptions.split('\n').filter(v => v !== '');
|
||||
const enumOptionsArray = enumOptions.split("\n").filter(v => v !== "");
|
||||
// Antd Select doesn't handle null in multiple mode
|
||||
const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val);
|
||||
return (
|
||||
<Select
|
||||
className={this.props.className}
|
||||
mode={parameter.multiValuesOptions ? 'multiple' : 'default'}
|
||||
mode={parameter.multiValuesOptions ? "multiple" : "default"}
|
||||
optionFilterProp="children"
|
||||
disabled={enumOptionsArray.length === 0}
|
||||
value={normalize(value)}
|
||||
@@ -111,9 +111,12 @@ class ParameterValueInput extends React.Component {
|
||||
showArrow
|
||||
style={{ minWidth: 60 }}
|
||||
notFoundContent={null}
|
||||
{...multipleValuesProps}
|
||||
>
|
||||
{enumOptionsArray.map(option => (<Option key={option} value={option}>{ option }</Option>))}
|
||||
{...multipleValuesProps}>
|
||||
{enumOptionsArray.map(option => (
|
||||
<Option key={option} value={option}>
|
||||
{option}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -124,7 +127,7 @@ class ParameterValueInput extends React.Component {
|
||||
return (
|
||||
<QueryBasedParameterInput
|
||||
className={this.props.className}
|
||||
mode={parameter.multiValuesOptions ? 'multiple' : 'default'}
|
||||
mode={parameter.multiValuesOptions ? "multiple" : "default"}
|
||||
optionFilterProp="children"
|
||||
parameter={parameter}
|
||||
value={value}
|
||||
@@ -143,11 +146,7 @@ class ParameterValueInput extends React.Component {
|
||||
const normalize = val => (isNaN(val) ? undefined : val);
|
||||
|
||||
return (
|
||||
<InputNumber
|
||||
className={className}
|
||||
value={normalize(value)}
|
||||
onChange={val => this.onSelect(normalize(val))}
|
||||
/>
|
||||
<InputNumber className={className} value={normalize(value)} onChange={val => this.onSelect(normalize(val))} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,16 +167,22 @@ class ParameterValueInput extends React.Component {
|
||||
renderInput() {
|
||||
const { type } = this.props;
|
||||
switch (type) {
|
||||
case 'datetime-with-seconds':
|
||||
case 'datetime-local':
|
||||
case 'date': return this.renderDateParameter();
|
||||
case 'datetime-range-with-seconds':
|
||||
case 'datetime-range':
|
||||
case 'date-range': return this.renderDateRangeParameter();
|
||||
case 'enum': return this.renderEnumInput();
|
||||
case 'query': return this.renderQueryBasedInput();
|
||||
case 'number': return this.renderNumberInput();
|
||||
default: return this.renderTextInput();
|
||||
case "datetime-with-seconds":
|
||||
case "datetime-local":
|
||||
case "date":
|
||||
return this.renderDateParameter();
|
||||
case "datetime-range-with-seconds":
|
||||
case "datetime-range":
|
||||
case "date-range":
|
||||
return this.renderDateRangeParameter();
|
||||
case "enum":
|
||||
return this.renderEnumInput();
|
||||
case "query":
|
||||
return this.renderQueryBasedInput();
|
||||
case "number":
|
||||
return this.renderNumberInput();
|
||||
default:
|
||||
return this.renderTextInput();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { size, filter, forEach, extend } from 'lodash';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { SortableContainer, SortableElement, DragHandle } from '@/components/sortable';
|
||||
import { $location } from '@/services/ng';
|
||||
import { Parameter } from '@/services/parameters';
|
||||
import ParameterApplyButton from '@/components/ParameterApplyButton';
|
||||
import ParameterValueInput from '@/components/ParameterValueInput';
|
||||
import EditParameterSettingsDialog from './EditParameterSettingsDialog';
|
||||
import { toHuman } from '@/filters';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { size, filter, forEach, extend } from "lodash";
|
||||
import { react2angular } from "react2angular";
|
||||
import { SortableContainer, SortableElement, DragHandle } from "@/components/sortable";
|
||||
import { $location } from "@/services/ng";
|
||||
import { Parameter } from "@/services/parameters";
|
||||
import ParameterApplyButton from "@/components/ParameterApplyButton";
|
||||
import ParameterValueInput from "@/components/ParameterValueInput";
|
||||
import EditParameterSettingsDialog from "./EditParameterSettingsDialog";
|
||||
import { toHuman } from "@/filters";
|
||||
|
||||
import './Parameters.less';
|
||||
import "./Parameters.less";
|
||||
|
||||
function updateUrl(parameters) {
|
||||
const params = extend({}, $location.search());
|
||||
parameters.forEach((param) => {
|
||||
parameters.forEach(param => {
|
||||
extend(params, param.toUrlParams());
|
||||
});
|
||||
Object.keys(params).forEach(key => params[key] == null && delete params[key]);
|
||||
@@ -49,7 +49,7 @@ export class Parameters extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
componentDidUpdate = prevProps => {
|
||||
const { parameters, disableUrlUpdate } = this.props;
|
||||
if (prevProps.parameters !== parameters) {
|
||||
this.setState({ parameters });
|
||||
@@ -59,7 +59,7 @@ export class Parameters extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
handleKeyDown = e => {
|
||||
// Cmd/Ctrl/Alt + Enter
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) {
|
||||
e.stopPropagation();
|
||||
@@ -106,9 +106,7 @@ export class Parameters extends React.Component {
|
||||
|
||||
showParameterSettings = (parameter, index) => {
|
||||
const { onParametersEdit } = this.props;
|
||||
EditParameterSettingsDialog
|
||||
.showModal({ parameter })
|
||||
.result.then((updated) => {
|
||||
EditParameterSettingsDialog.showModal({ parameter }).result.then(updated => {
|
||||
this.setState(({ parameters }) => {
|
||||
const updatedParameter = extend(parameter, updated);
|
||||
parameters[index] = Parameter.create(updatedParameter, updatedParameter.parentQueryId);
|
||||
@@ -121,11 +119,7 @@ export class Parameters extends React.Component {
|
||||
renderParameter(param, index) {
|
||||
const { editable } = this.props;
|
||||
return (
|
||||
<div
|
||||
key={param.name}
|
||||
className="di-block"
|
||||
data-test={`ParameterName-${param.name}`}
|
||||
>
|
||||
<div key={param.name} className="di-block" data-test={`ParameterName-${param.name}`}>
|
||||
<div className="parameter-heading">
|
||||
<label>{param.title || toHuman(param.name)}</label>
|
||||
{editable && (
|
||||
@@ -133,8 +127,7 @@ export class Parameters extends React.Component {
|
||||
className="btn btn-default btn-xs m-l-5"
|
||||
onClick={() => this.showParameterSettings(param, index)}
|
||||
data-test={`ParameterSettings-${param.name}`}
|
||||
type="button"
|
||||
>
|
||||
type="button">
|
||||
<i className="fa fa-cog" />
|
||||
</button>
|
||||
)}
|
||||
@@ -154,7 +147,7 @@ export class Parameters extends React.Component {
|
||||
render() {
|
||||
const { parameters } = this.state;
|
||||
const { editable } = this.props;
|
||||
const dirtyParamCount = size(filter(parameters, 'hasPendingValue'));
|
||||
const dirtyParamCount = size(filter(parameters, "hasPendingValue"));
|
||||
return (
|
||||
<SortableContainer
|
||||
disabled={!editable}
|
||||
@@ -165,10 +158,9 @@ export class Parameters extends React.Component {
|
||||
updateBeforeSortStart={this.onBeforeSortStart}
|
||||
onSortEnd={this.moveParameter}
|
||||
containerProps={{
|
||||
className: 'parameter-container',
|
||||
className: "parameter-container",
|
||||
onKeyDown: dirtyParamCount ? this.handleKeyDown : null,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{parameters.map((param, index) => (
|
||||
<SortableElement key={param.name} index={index}>
|
||||
<div className="parameter-block" data-editable={editable || null}>
|
||||
@@ -184,7 +176,7 @@ export class Parameters extends React.Component {
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('parameters', react2angular(Parameters));
|
||||
ngModule.component("parameters", react2angular(Parameters));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
|
||||
// PreviewCard
|
||||
|
||||
export function PreviewCard({ imageUrl, roundedImage, title, body, children, className, ...props }) {
|
||||
return (
|
||||
<div {...props} className={className + ' w-100 d-flex align-items-center'}>
|
||||
<div {...props} className={className + " w-100 d-flex align-items-center"}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
width="32"
|
||||
height="32"
|
||||
className={classNames({ 'profile__image--settings': roundedImage }, 'm-r-5')}
|
||||
className={classNames({ "profile__image--settings": roundedImage }, "m-r-5")}
|
||||
alt="Logo/Avatar"
|
||||
/>
|
||||
<div className="flex-fill">
|
||||
@@ -35,14 +35,14 @@ PreviewCard.propTypes = {
|
||||
PreviewCard.defaultProps = {
|
||||
body: null,
|
||||
roundedImage: true,
|
||||
className: '',
|
||||
className: "",
|
||||
children: null,
|
||||
};
|
||||
|
||||
// UserPreviewCard
|
||||
|
||||
export function UserPreviewCard({ user, withLink, children, ...props }) {
|
||||
const title = withLink ? <a href={'users/' + user.id}>{user.name}</a> : user.name;
|
||||
const title = withLink ? <a href={"users/" + user.id}>{user.name}</a> : user.name;
|
||||
return (
|
||||
<PreviewCard {...props} imageUrl={user.profile_image_url} title={title} body={user.email}>
|
||||
{children}
|
||||
@@ -69,8 +69,12 @@ UserPreviewCard.defaultProps = {
|
||||
|
||||
export function DataSourcePreviewCard({ dataSource, withLink, children, ...props }) {
|
||||
const imageUrl = `/static/images/db-logos/${dataSource.type}.png`;
|
||||
const title = withLink ? <a href={'data_sources/' + dataSource.id}>{dataSource.name}</a> : dataSource.name;
|
||||
return <PreviewCard {...props} imageUrl={imageUrl} title={title}>{children}</PreviewCard>;
|
||||
const title = withLink ? <a href={"data_sources/" + dataSource.id}>{dataSource.name}</a> : dataSource.name;
|
||||
return (
|
||||
<PreviewCard {...props} imageUrl={imageUrl} title={title}>
|
||||
{children}
|
||||
</PreviewCard>
|
||||
);
|
||||
}
|
||||
|
||||
DataSourcePreviewCard.propTypes = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { find, isArray, map, intersection, isEqual } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Select from 'antd/lib/select';
|
||||
import { find, isArray, map, intersection, isEqual } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import Select from "antd/lib/select";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -10,7 +10,7 @@ export class QueryBasedParameterInput extends React.Component {
|
||||
static propTypes = {
|
||||
parameter: PropTypes.any, // eslint-disable-line react/forbid-prop-types
|
||||
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
|
||||
mode: PropTypes.oneOf(['default', 'multiple']),
|
||||
mode: PropTypes.oneOf(["default", "multiple"]),
|
||||
queryId: PropTypes.number,
|
||||
onSelect: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
@@ -18,11 +18,11 @@ export class QueryBasedParameterInput extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
value: null,
|
||||
mode: 'default',
|
||||
mode: "default",
|
||||
parameter: null,
|
||||
queryId: null,
|
||||
onSelect: () => {},
|
||||
className: '',
|
||||
className: "",
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -49,7 +49,7 @@ export class QueryBasedParameterInput extends React.Component {
|
||||
|
||||
setValue(value) {
|
||||
const { options } = this.state;
|
||||
if (this.props.mode === 'multiple') {
|
||||
if (this.props.mode === "multiple") {
|
||||
value = isArray(value) ? value : [value];
|
||||
const optionValues = map(options, option => option.value);
|
||||
const validValues = intersection(value, optionValues);
|
||||
@@ -63,7 +63,7 @@ export class QueryBasedParameterInput extends React.Component {
|
||||
}
|
||||
|
||||
async _loadOptions(queryId) {
|
||||
if (queryId && (queryId !== this.state.queryId)) {
|
||||
if (queryId && queryId !== this.state.queryId) {
|
||||
this.setState({ loading: true });
|
||||
const options = await this.props.parameter.loadDropdownValues();
|
||||
|
||||
@@ -86,7 +86,7 @@ export class QueryBasedParameterInput extends React.Component {
|
||||
<span>
|
||||
<Select
|
||||
className={className}
|
||||
disabled={loading || (options.length === 0)}
|
||||
disabled={loading || options.length === 0}
|
||||
loading={loading}
|
||||
mode={mode}
|
||||
value={this.state.value}
|
||||
@@ -96,9 +96,12 @@ export class QueryBasedParameterInput extends React.Component {
|
||||
showSearch
|
||||
showArrow
|
||||
notFoundContent={null}
|
||||
{...otherProps}
|
||||
>
|
||||
{options.map(option => (<Option value={option.value} key={option.value}>{option.name}</Option>))}
|
||||
{...otherProps}>
|
||||
{options.map(option => (
|
||||
<Option value={option.value} key={option.value}>
|
||||
{option.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</span>
|
||||
);
|
||||
@@ -106,7 +109,7 @@ export class QueryBasedParameterInput extends React.Component {
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('queryBasedParameterInput', react2angular(QueryBasedParameterInput));
|
||||
ngModule.component("queryBasedParameterInput", react2angular(QueryBasedParameterInput));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import AceEditor from 'react-ace';
|
||||
import ace from 'brace';
|
||||
import notification from '@/services/notification';
|
||||
import AceEditor from "react-ace";
|
||||
import ace from "brace";
|
||||
import notification from "@/services/notification";
|
||||
|
||||
import 'brace/ext/language_tools';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/mode/python';
|
||||
import 'brace/mode/sql';
|
||||
import 'brace/mode/yaml';
|
||||
import 'brace/theme/textmate';
|
||||
import 'brace/ext/searchbox';
|
||||
import "brace/ext/language_tools";
|
||||
import "brace/mode/json";
|
||||
import "brace/mode/python";
|
||||
import "brace/mode/sql";
|
||||
import "brace/mode/yaml";
|
||||
import "brace/theme/textmate";
|
||||
import "brace/ext/searchbox";
|
||||
|
||||
import { Query } from '@/services/query';
|
||||
import { QuerySnippet } from '@/services/query-snippet';
|
||||
import { KeyboardShortcuts } from '@/services/keyboard-shortcuts';
|
||||
import { Query } from "@/services/query";
|
||||
import { QuerySnippet } from "@/services/query-snippet";
|
||||
import { KeyboardShortcuts } from "@/services/keyboard-shortcuts";
|
||||
|
||||
import localOptions from '@/lib/localOptions';
|
||||
import AutocompleteToggle from '@/components/AutocompleteToggle';
|
||||
import keywordBuilder from './keywordBuilder';
|
||||
import { DataSource, Schema } from './proptypes';
|
||||
import localOptions from "@/lib/localOptions";
|
||||
import AutocompleteToggle from "@/components/AutocompleteToggle";
|
||||
import keywordBuilder from "./keywordBuilder";
|
||||
import { DataSource, Schema } from "./proptypes";
|
||||
|
||||
import './QueryEditor.css';
|
||||
import "./QueryEditor.css";
|
||||
|
||||
const langTools = ace.acequire('ace/ext/language_tools');
|
||||
const snippetsModule = ace.acequire('ace/snippets');
|
||||
const langTools = ace.acequire("ace/ext/language_tools");
|
||||
const snippetsModule = ace.acequire("ace/snippets");
|
||||
|
||||
// By default Ace will try to load snippet files for the different modes and fail.
|
||||
// We don't need them, so we use these placeholders until we define our own.
|
||||
function defineDummySnippets(mode) {
|
||||
ace.define(`ace/snippets/${mode}`, ['require', 'exports', 'module'], (require, exports) => {
|
||||
exports.snippetText = '';
|
||||
ace.define(`ace/snippets/${mode}`, ["require", "exports", "module"], (require, exports) => {
|
||||
exports.snippetText = "";
|
||||
exports.scope = mode;
|
||||
});
|
||||
}
|
||||
|
||||
defineDummySnippets('python');
|
||||
defineDummySnippets('sql');
|
||||
defineDummySnippets('json');
|
||||
defineDummySnippets('yaml');
|
||||
defineDummySnippets("python");
|
||||
defineDummySnippets("sql");
|
||||
defineDummySnippets("json");
|
||||
defineDummySnippets("yaml");
|
||||
|
||||
class QueryEditor extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -82,7 +82,7 @@ class QueryEditor extends React.Component {
|
||||
column: [],
|
||||
tableColumn: [],
|
||||
},
|
||||
autocompleteQuery: localOptions.get('liveAutocomplete', true),
|
||||
autocompleteQuery: localOptions.get("liveAutocomplete", true),
|
||||
liveAutocompleteDisabled: false,
|
||||
// XXX temporary while interfacing with angular
|
||||
queryText: props.queryText,
|
||||
@@ -101,7 +101,7 @@ class QueryEditor extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prefix[prefix.length - 1] === '.') {
|
||||
if (prefix[prefix.length - 1] === ".") {
|
||||
const tableName = prefix.substring(0, prefix.length - 1);
|
||||
callback(null, tableKeywords.concat(tableColumnKeywords[tableName]));
|
||||
return;
|
||||
@@ -139,33 +139,32 @@ class QueryEditor extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
onLoad = (editor) => {
|
||||
onLoad = editor => {
|
||||
// Release Cmd/Ctrl+L to the browser
|
||||
editor.commands.bindKey('Cmd+L', null);
|
||||
editor.commands.bindKey('Ctrl+P', null);
|
||||
editor.commands.bindKey('Ctrl+L', null);
|
||||
editor.commands.bindKey("Cmd+L", null);
|
||||
editor.commands.bindKey("Ctrl+P", null);
|
||||
editor.commands.bindKey("Ctrl+L", null);
|
||||
|
||||
// Ignore Ctrl+P to open new parameter dialog
|
||||
editor.commands.bindKey({ win: 'Ctrl+P', mac: null }, null);
|
||||
editor.commands.bindKey({ win: "Ctrl+P", mac: null }, null);
|
||||
// Lineup only mac
|
||||
editor.commands.bindKey({ win: null, mac: 'Ctrl+P' }, 'golineup');
|
||||
editor.commands.bindKey({ win: 'Ctrl+Shift+F', mac: 'Cmd+Shift+F' }, this.formatQuery);
|
||||
editor.commands.bindKey({ win: null, mac: "Ctrl+P" }, "golineup");
|
||||
editor.commands.bindKey({ win: "Ctrl+Shift+F", mac: "Cmd+Shift+F" }, this.formatQuery);
|
||||
|
||||
// Reset Completer in case dot is pressed
|
||||
editor.commands.on('afterExec', (e) => {
|
||||
if (e.command.name === 'insertstring' && e.args === '.'
|
||||
&& editor.completer) {
|
||||
editor.commands.on("afterExec", e => {
|
||||
if (e.command.name === "insertstring" && e.args === "." && editor.completer) {
|
||||
editor.completer.showPopup(editor);
|
||||
}
|
||||
});
|
||||
|
||||
QuerySnippet.query((snippets) => {
|
||||
QuerySnippet.query(snippets => {
|
||||
const snippetManager = snippetsModule.snippetManager;
|
||||
const m = {
|
||||
snippetText: '',
|
||||
snippetText: "",
|
||||
};
|
||||
m.snippets = snippetManager.parseSnippetFile(m.snippetText);
|
||||
snippets.forEach((snippet) => {
|
||||
snippets.forEach(snippet => {
|
||||
m.snippets.push(snippet.getSnippet());
|
||||
});
|
||||
snippetManager.register(m.snippets || [], m.scope);
|
||||
@@ -175,11 +174,11 @@ class QueryEditor extends React.Component {
|
||||
this.props.listenForResize(() => editor.resize());
|
||||
this.props.listenForEditorCommand((e, command, ...args) => {
|
||||
switch (command) {
|
||||
case 'focus': {
|
||||
case "focus": {
|
||||
editor.focus();
|
||||
break;
|
||||
}
|
||||
case 'paste': {
|
||||
case "paste": {
|
||||
const [text] = args;
|
||||
editor.session.doc.replace(editor.selection.getRange(), text);
|
||||
const range = editor.selection.getRange();
|
||||
@@ -193,29 +192,29 @@ class QueryEditor extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
updateSelectedQuery = (selection) => {
|
||||
updateSelectedQuery = selection => {
|
||||
const { editor } = this.refEditor.current;
|
||||
const doc = editor.getSession().doc;
|
||||
const rawSelectedQueryText = doc.getTextRange(selection.getRange());
|
||||
const selectedQueryText = (rawSelectedQueryText.length > 1) ? rawSelectedQueryText : null;
|
||||
const selectedQueryText = rawSelectedQueryText.length > 1 ? rawSelectedQueryText : null;
|
||||
this.setState({ selectedQueryText });
|
||||
this.props.updateSelectedQuery(selectedQueryText);
|
||||
};
|
||||
|
||||
updateQuery = (queryText) => {
|
||||
updateQuery = queryText => {
|
||||
this.props.updateQuery(queryText);
|
||||
this.setState({ queryText });
|
||||
};
|
||||
|
||||
formatQuery = () => {
|
||||
Query.format(this.props.dataSource.syntax || 'sql', this.props.queryText)
|
||||
Query.format(this.props.dataSource.syntax || "sql", this.props.queryText)
|
||||
.then(this.updateQuery)
|
||||
.catch(error => notification.error(error));
|
||||
};
|
||||
|
||||
toggleAutocomplete = (state) => {
|
||||
toggleAutocomplete = state => {
|
||||
this.setState({ autocompleteQuery: state });
|
||||
localOptions.set('liveAutocomplete', state);
|
||||
localOptions.set("liveAutocomplete", state);
|
||||
};
|
||||
|
||||
componentDidUpdate = () => {
|
||||
@@ -230,13 +229,16 @@ class QueryEditor extends React.Component {
|
||||
const isExecuteDisabled = this.props.queryExecuting || !this.props.canExecuteQuery;
|
||||
|
||||
return (
|
||||
<section style={{ height: '100%' }} data-test="QueryEditor">
|
||||
<div className="container p-15 m-b-10" style={{ height: '100%' }}>
|
||||
<div data-executing={this.props.queryExecuting} style={{ height: 'calc(100% - 40px)', marginBottom: '0px' }} className="editor__container">
|
||||
<section style={{ height: "100%" }} data-test="QueryEditor">
|
||||
<div className="container p-15 m-b-10" style={{ height: "100%" }}>
|
||||
<div
|
||||
data-executing={this.props.queryExecuting}
|
||||
style={{ height: "calc(100% - 40px)", marginBottom: "0px" }}
|
||||
className="editor__container">
|
||||
<AceEditor
|
||||
ref={this.refEditor}
|
||||
theme="textmate"
|
||||
mode={this.props.dataSource.syntax || 'sql'}
|
||||
mode={this.props.dataSource.syntax || "sql"}
|
||||
value={this.state.queryText}
|
||||
editorProps={{ $blockScrolling: Infinity }}
|
||||
width="100%"
|
||||
@@ -261,13 +263,22 @@ class QueryEditor extends React.Component {
|
||||
<div className="form-inline d-flex">
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={<span>Add New Parameter (<i>{modKey} + P</i>)</span>}
|
||||
>
|
||||
title={
|
||||
<span>
|
||||
Add New Parameter (<i>{modKey} + P</i>)
|
||||
</span>
|
||||
}>
|
||||
<button type="button" className="btn btn-default m-r-5" onClick={this.props.addNewParameter}>
|
||||
{{ }}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={<>Format Query (<i>{modKey} + Shift + F</i>)</>}>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={
|
||||
<>
|
||||
Format Query (<i>{modKey} + Shift + F</i>)
|
||||
</>
|
||||
}>
|
||||
<button type="button" className="btn btn-default m-r-5" onClick={this.formatQuery}>
|
||||
<span className="zmdi zmdi-format-indent-increase" />
|
||||
</button>
|
||||
@@ -280,8 +291,7 @@ class QueryEditor extends React.Component {
|
||||
<select
|
||||
className="form-control datasource-small flex-fill w-100"
|
||||
onChange={this.props.updateDataSource}
|
||||
disabled={!this.props.isQueryOwner}
|
||||
>
|
||||
disabled={!this.props.isQueryOwner}>
|
||||
{this.props.dataSources.map(ds => (
|
||||
<option label={ds.name} value={ds.id} key={`ds-option-${ds.id}`}>
|
||||
{ds.name}
|
||||
@@ -289,21 +299,20 @@ class QueryEditor extends React.Component {
|
||||
))}
|
||||
</select>
|
||||
{this.props.canEdit ? (
|
||||
<Tooltip placement="top" title={modKey + ' + S'}>
|
||||
<Tooltip placement="top" title={modKey + " + S"}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-default m-l-5"
|
||||
onClick={this.props.saveQuery}
|
||||
data-test="SaveButton"
|
||||
title="Save"
|
||||
>
|
||||
title="Save">
|
||||
<span className="fa fa-floppy-o" />
|
||||
<span className="hidden-xs m-l-5">Save</span>
|
||||
{this.props.isDirty ? '*' : null}
|
||||
{this.props.isDirty ? "*" : null}
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Tooltip placement="top" title={modKey + ' + Enter'}>
|
||||
<Tooltip placement="top" title={modKey + " + Enter"}>
|
||||
{/*
|
||||
Tooltip wraps disabled buttons with `<span>` and moves all styles
|
||||
and classes to that `<span>`. There is a piece of CSS that fixes
|
||||
@@ -313,13 +322,14 @@ class QueryEditor extends React.Component {
|
||||
*/}
|
||||
<button
|
||||
type="button"
|
||||
className={'btn btn-primary m-l-5' + (isExecuteDisabled ? ' disabled' : '')}
|
||||
className={"btn btn-primary m-l-5" + (isExecuteDisabled ? " disabled" : "")}
|
||||
disabled={isExecuteDisabled}
|
||||
onClick={this.props.executeQuery}
|
||||
data-test="ExecuteButton"
|
||||
>
|
||||
data-test="ExecuteButton">
|
||||
<span className="zmdi zmdi-play" />
|
||||
<span className="hidden-xs m-l-5">{ (this.state.selectedQueryText == null) ? 'Execute' : 'Execute Selected' }</span>
|
||||
<span className="hidden-xs m-l-5">
|
||||
{this.state.selectedQueryText == null ? "Execute" : "Execute Selected"}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -331,7 +341,7 @@ class QueryEditor extends React.Component {
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('queryEditor', react2angular(QueryEditor));
|
||||
ngModule.component("queryEditor", react2angular(QueryEditor));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { VisualizationType } from '@/visualizations';
|
||||
import { VisualizationName } from '@/visualizations/VisualizationName';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { VisualizationType } from "@/visualizations";
|
||||
import { VisualizationName } from "@/visualizations/VisualizationName";
|
||||
|
||||
function QueryLink({ query, visualization, readOnly }) {
|
||||
const getUrl = () => {
|
||||
let hash = null;
|
||||
if (visualization) {
|
||||
if (visualization.type === 'TABLE') {
|
||||
if (visualization.type === "TABLE") {
|
||||
// link to hard-coded table tab instead of the (hidden) visualization tab
|
||||
hash = 'table';
|
||||
hash = "table";
|
||||
} else {
|
||||
hash = visualization.id;
|
||||
}
|
||||
@@ -20,8 +20,7 @@ function QueryLink({ query, visualization, readOnly }) {
|
||||
|
||||
return (
|
||||
<a href={readOnly ? null : getUrl()} className="query-link">
|
||||
<VisualizationName visualization={visualization} />{' '}
|
||||
<span>{query.name}</span>
|
||||
<VisualizationName visualization={visualization} /> <span>{query.name}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { find } from 'lodash';
|
||||
import Input from 'antd/lib/input';
|
||||
import Select from 'antd/lib/select';
|
||||
import { Query } from '@/services/query';
|
||||
import notification from '@/services/notification';
|
||||
import { QueryTagsControl } from '@/components/tags-control/TagsControl';
|
||||
import useSearchResults from '@/lib/hooks/useSearchResults';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import { react2angular } from "react2angular";
|
||||
import { find } from "lodash";
|
||||
import Input from "antd/lib/input";
|
||||
import Select from "antd/lib/select";
|
||||
import { Query } from "@/services/query";
|
||||
import notification from "@/services/notification";
|
||||
import { QueryTagsControl } from "@/components/tags-control/TagsControl";
|
||||
import useSearchResults from "@/lib/hooks/useSearchResults";
|
||||
|
||||
const { Option } = Select;
|
||||
function search(term) {
|
||||
// get recent
|
||||
if (!term) {
|
||||
return Query.recent().$promise
|
||||
.then((results) => {
|
||||
return Query.recent().$promise.then(results => {
|
||||
const filteredResults = results.filter(item => !item.is_draft); // filter out draft
|
||||
return Promise.resolve(filteredResults);
|
||||
});
|
||||
}
|
||||
|
||||
// search by query
|
||||
return Query.query({ q: term }).$promise
|
||||
.then(({ results }) => Promise.resolve(results));
|
||||
return Query.query({ q: term }).$promise.then(({ results }) => Promise.resolve(results));
|
||||
}
|
||||
|
||||
export function QuerySelector(props) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedQuery, setSelectedQuery] = useState();
|
||||
const [doSearch, searchResults, searching] = useSearchResults(search, { initialResults: [] });
|
||||
|
||||
const placeholder = 'Search a query by name';
|
||||
const placeholder = "Search a query by name";
|
||||
const clearIcon = <i className="fa fa-times hide-in-percy" onClick={() => selectQuery(null)} />;
|
||||
const spinIcon = <i className={cx('fa fa-spinner fa-pulse hide-in-percy', { hidden: !searching })} />;
|
||||
const spinIcon = <i className={cx("fa fa-spinner fa-pulse hide-in-percy", { hidden: !searching })} />;
|
||||
|
||||
useEffect(() => { doSearch(searchTerm); }, [doSearch, searchTerm]);
|
||||
useEffect(() => {
|
||||
doSearch(searchTerm);
|
||||
}, [doSearch, searchTerm]);
|
||||
|
||||
// set selected from prop
|
||||
useEffect(() => {
|
||||
@@ -48,12 +48,13 @@ export function QuerySelector(props) {
|
||||
let query = null;
|
||||
if (queryId) {
|
||||
query = find(searchResults, { id: queryId });
|
||||
if (!query) { // shouldn't happen
|
||||
notification.error('Something went wrong...', 'Couldn\'t select query');
|
||||
if (!query) {
|
||||
// shouldn't happen
|
||||
notification.error("Something went wrong...", "Couldn't select query");
|
||||
}
|
||||
}
|
||||
|
||||
setSearchTerm(query ? null : ''); // empty string triggers recent fetch
|
||||
setSearchTerm(query ? null : ""); // empty string triggers recent fetch
|
||||
setSelectedQuery(query);
|
||||
props.onChange(query);
|
||||
}
|
||||
@@ -67,14 +68,11 @@ export function QuerySelector(props) {
|
||||
<div className="list-group">
|
||||
{searchResults.map(q => (
|
||||
<a
|
||||
className={cx('query-selector-result', 'list-group-item', { inactive: q.is_draft })}
|
||||
className={cx("query-selector-result", "list-group-item", { inactive: q.is_draft })}
|
||||
key={q.id}
|
||||
onClick={() => selectQuery(q.id)}
|
||||
data-test={`QueryId${q.id}`}
|
||||
>
|
||||
{q.name}
|
||||
{' '}
|
||||
<QueryTagsControl isDraft={q.is_draft} tags={q.tags} className="inline-tags-control" />
|
||||
data-test={`QueryId${q.id}`}>
|
||||
{q.name} <QueryTagsControl isDraft={q.is_draft} tags={q.tags} className="inline-tags-control" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -85,7 +83,7 @@ export function QuerySelector(props) {
|
||||
return <Input value={selectedQuery && selectedQuery.name} placeholder={placeholder} disabled />;
|
||||
}
|
||||
|
||||
if (props.type === 'select') {
|
||||
if (props.type === "select") {
|
||||
const suffixIcon = selectedQuery ? clearIcon : null;
|
||||
const value = selectedQuery ? selectedQuery.name : searchTerm;
|
||||
|
||||
@@ -102,14 +100,23 @@ export function QuerySelector(props) {
|
||||
filterOption={false}
|
||||
defaultActiveFirstOption={false}
|
||||
className={props.className}
|
||||
data-test="QuerySelector"
|
||||
>
|
||||
{searchResults && searchResults.map((q) => {
|
||||
data-test="QuerySelector">
|
||||
{searchResults &&
|
||||
searchResults.map(q => {
|
||||
const disabled = q.is_draft;
|
||||
return (
|
||||
<Option value={q.id} key={q.id} disabled={disabled} className="query-selector-result" data-test={`QueryId${q.id}`}>
|
||||
{q.name}{' '}
|
||||
<QueryTagsControl isDraft={q.is_draft} tags={q.tags} className={cx('inline-tags-control', { disabled })} />
|
||||
<Option
|
||||
value={q.id}
|
||||
key={q.id}
|
||||
disabled={disabled}
|
||||
className="query-selector-result"
|
||||
data-test={`QueryId${q.id}`}>
|
||||
{q.name}{" "}
|
||||
<QueryTagsControl
|
||||
isDraft={q.is_draft}
|
||||
tags={q.tags}
|
||||
className={cx("inline-tags-control", { disabled })}
|
||||
/>
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
@@ -129,7 +136,7 @@ export function QuerySelector(props) {
|
||||
suffix={spinIcon}
|
||||
/>
|
||||
)}
|
||||
<div className="scrollbox" style={{ maxHeight: '50vh', marginTop: 15 }}>
|
||||
<div className="scrollbox" style={{ maxHeight: "50vh", marginTop: 15 }}>
|
||||
{searchResults && renderResults()}
|
||||
</div>
|
||||
</span>
|
||||
@@ -139,20 +146,20 @@ export function QuerySelector(props) {
|
||||
QuerySelector.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
selectedQuery: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
type: PropTypes.oneOf(['select', 'default']),
|
||||
type: PropTypes.oneOf(["select", "default"]),
|
||||
className: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
QuerySelector.defaultProps = {
|
||||
selectedQuery: null,
|
||||
type: 'default',
|
||||
type: "default",
|
||||
className: null,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('querySelector', react2angular(QuerySelector));
|
||||
ngModule.component("querySelector", react2angular(QuerySelector));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { filter, debounce, find, isEmpty, size } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Input from 'antd/lib/input';
|
||||
import List from 'antd/lib/list';
|
||||
import Button from 'antd/lib/button';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { BigMessage } from '@/components/BigMessage';
|
||||
import { filter, debounce, find, isEmpty, size } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Input from "antd/lib/input";
|
||||
import List from "antd/lib/list";
|
||||
import Button from "antd/lib/button";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import { BigMessage } from "@/components/BigMessage";
|
||||
|
||||
import LoadingState from '@/components/items-list/components/LoadingState';
|
||||
import notification from '@/services/notification';
|
||||
import LoadingState from "@/components/items-list/components/LoadingState";
|
||||
import notification from "@/services/notification";
|
||||
|
||||
class SelectItemsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -36,20 +36,20 @@ class SelectItemsDialog extends React.Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
dialogTitle: 'Add Items',
|
||||
inputPlaceholder: 'Search...',
|
||||
selectedItemsTitle: 'Selected items',
|
||||
dialogTitle: "Add Items",
|
||||
inputPlaceholder: "Search...",
|
||||
selectedItemsTitle: "Selected items",
|
||||
itemKey: item => item.id,
|
||||
renderItem: () => '',
|
||||
renderItem: () => "",
|
||||
renderStagedItem: null, // hidden by default
|
||||
save: items => items,
|
||||
width: '80%',
|
||||
width: "80%",
|
||||
extraFooterContent: null,
|
||||
showCount: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
searchTerm: '',
|
||||
searchTerm: "",
|
||||
loading: false,
|
||||
items: [],
|
||||
selected: [],
|
||||
@@ -57,10 +57,11 @@ class SelectItemsDialog extends React.Component {
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
loadItems = (searchTerm = '') => {
|
||||
loadItems = (searchTerm = "") => {
|
||||
this.setState({ searchTerm, loading: true }, () => {
|
||||
this.props.searchItems(searchTerm)
|
||||
.then((items) => {
|
||||
this.props
|
||||
.searchItems(searchTerm)
|
||||
.then(items => {
|
||||
// If another search appeared while loading data - just reject this set
|
||||
if (this.state.searchTerm === searchTerm) {
|
||||
this.setState({ items, loading: false });
|
||||
@@ -107,7 +108,7 @@ class SelectItemsDialog extends React.Component {
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ saveInProgress: false });
|
||||
notification.error('Failed to save some of selected items.');
|
||||
notification.error("Failed to save some of selected items.");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -121,9 +122,8 @@ class SelectItemsDialog extends React.Component {
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
className={classNames('p-l-10', 'p-r-10', { clickable: !isDisabled, disabled: isDisabled }, className)}
|
||||
onClick={isDisabled ? null : () => this.toggleItem(item)}
|
||||
>
|
||||
className={classNames("p-l-10", "p-r-10", { clickable: !isDisabled, disabled: isDisabled }, className)}
|
||||
onClick={isDisabled ? null : () => this.toggleItem(item)}>
|
||||
{content}
|
||||
</List.Item>
|
||||
);
|
||||
@@ -140,17 +140,22 @@ class SelectItemsDialog extends React.Component {
|
||||
className="select-items-dialog"
|
||||
width={width}
|
||||
title={dialogTitle}
|
||||
footer={(
|
||||
footer={
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="flex-fill m-r-5" style={{ textAlign: 'left', color: 'rgba(0, 0, 0, 0.5)' }}>{this.props.extraFooterContent}</span>
|
||||
<span className="flex-fill m-r-5" style={{ textAlign: "left", color: "rgba(0, 0, 0, 0.5)" }}>
|
||||
{this.props.extraFooterContent}
|
||||
</span>
|
||||
<Button onClick={dialog.dismiss}>Cancel</Button>
|
||||
<Button onClick={() => this.save()} loading={saveInProgress} disabled={selected.length === 0} type="primary">
|
||||
<Button
|
||||
onClick={() => this.save()}
|
||||
loading={saveInProgress}
|
||||
disabled={selected.length === 0}
|
||||
type="primary">
|
||||
Save
|
||||
{showCount && !isEmpty(selected) ? ` (${size(selected)})` : null}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
}>
|
||||
<div className="d-flex align-items-center m-b-10">
|
||||
<div className="flex-fill">
|
||||
<Input.Search
|
||||
@@ -167,28 +172,20 @@ class SelectItemsDialog extends React.Component {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="d-flex align-items-stretch" style={{ minHeight: '30vh', maxHeight: '50vh' }}>
|
||||
<div className="d-flex align-items-stretch" style={{ minHeight: "30vh", maxHeight: "50vh" }}>
|
||||
<div className="flex-fill scrollbox">
|
||||
{loading && <LoadingState className="" />}
|
||||
{!loading && !hasResults && (
|
||||
<BigMessage icon="fa-search" message="No items match your search." className="" />
|
||||
)}
|
||||
{!loading && hasResults && (
|
||||
<List
|
||||
size="small"
|
||||
dataSource={items}
|
||||
renderItem={item => this.renderItem(item, false)}
|
||||
/>
|
||||
<List size="small" dataSource={items} renderItem={item => this.renderItem(item, false)} />
|
||||
)}
|
||||
</div>
|
||||
{renderStagedItem && (
|
||||
<div className="w-50 m-l-20 scrollbox">
|
||||
{(selected.length > 0) && (
|
||||
<List
|
||||
size="small"
|
||||
dataSource={selected}
|
||||
renderItem={item => this.renderItem(item, true)}
|
||||
/>
|
||||
{selected.length > 0 && (
|
||||
<List size="small" dataSource={selected} renderItem={item => this.renderItem(item, true)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import { PageHeader } from '@/components/PageHeader';
|
||||
import { $location } from '@/services/ng';
|
||||
import settingsMenu from '@/services/settingsMenu';
|
||||
|
||||
import React from "react";
|
||||
import Menu from "antd/lib/menu";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { $location } from "@/services/ng";
|
||||
import settingsMenu from "@/services/settingsMenu";
|
||||
|
||||
function wrapSettingsTab(options, WrappedComponent) {
|
||||
if (options) {
|
||||
@@ -19,7 +18,9 @@ function wrapSettingsTab(options, WrappedComponent) {
|
||||
<div className="bg-white tiled">
|
||||
<Menu selectedKeys={[activeItem && activeItem.title]} selectable={false} mode="horizontal">
|
||||
{settingsMenu.items.map(item => (
|
||||
<Menu.Item key={item.title}><a href={item.path}>{item.title}</a></Menu.Item>
|
||||
<Menu.Item key={item.title}>
|
||||
<a href={item.path}>{item.title}</a>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
<div className="p-15">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
export function SortIcon({ column, sortColumn, reverse }) {
|
||||
if (column !== sortColumn) {
|
||||
@@ -8,7 +8,9 @@ export function SortIcon({ column, sortColumn, reverse }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<span><i className={'fa fa-sort-' + (reverse ? 'desc' : 'asc')} /></span>
|
||||
<span>
|
||||
<i className={"fa fa-sort-" + (reverse ? "desc" : "asc")} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +27,7 @@ SortIcon.defaultProps = {
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('sortIcon', react2angular(SortIcon));
|
||||
ngModule.component("sortIcon", react2angular(SortIcon));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { map } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Badge from 'antd/lib/badge';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import getTags from '@/services/getTags';
|
||||
import { map } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import Badge from "antd/lib/badge";
|
||||
import Menu from "antd/lib/menu";
|
||||
import getTags from "@/services/getTags";
|
||||
|
||||
import './TagsList.less';
|
||||
import "./TagsList.less";
|
||||
|
||||
export class TagsList extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -30,7 +30,7 @@ export class TagsList extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
getTags(this.props.tagsUrl).then((allTags) => {
|
||||
getTags(this.props.tagsUrl).then(allTags => {
|
||||
this.setState({ allTags });
|
||||
});
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export class TagsList extends React.Component {
|
||||
}
|
||||
} else {
|
||||
// if the tag is the only selected, deselect it, otherwise select only it
|
||||
if (selectedTags.has(tag) && (selectedTags.size === 1)) {
|
||||
if (selectedTags.has(tag) && selectedTags.size === 1) {
|
||||
selectedTags.clear();
|
||||
} else {
|
||||
selectedTags.clear();
|
||||
@@ -66,7 +66,9 @@ export class TagsList extends React.Component {
|
||||
<Menu className="invert-stripe-position" mode="inline" selectedKeys={[...selectedTags]}>
|
||||
{map(allTags, tag => (
|
||||
<Menu.Item key={tag.name} className="m-0">
|
||||
<a className="d-flex align-items-center justify-content-between" onClick={event => this.toggleTag(event, tag.name)}>
|
||||
<a
|
||||
className="d-flex align-items-center justify-content-between"
|
||||
onClick={event => this.toggleTag(event, tag.name)}>
|
||||
<span className="max-character col-xs-11">{tag.name}</span>
|
||||
<Badge count={tag.count} />
|
||||
</a>
|
||||
@@ -81,7 +83,7 @@ export class TagsList extends React.Component {
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('tagsList', react2angular(TagsList));
|
||||
ngModule.component("tagsList", react2angular(TagsList));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { pickBy, startsWith } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Radio from 'antd/lib/radio';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { pickBy, startsWith } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import Radio from "antd/lib/radio";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
import './index.less';
|
||||
import "./index.less";
|
||||
|
||||
export default function TextAlignmentSelect({ className, ...props }) {
|
||||
return (
|
||||
// Antd RadioGroup does not use any custom attributes
|
||||
<div {...pickBy(props, (v, k) => startsWith(k, 'data-'))}>
|
||||
<Radio.Group
|
||||
className={cx('text-alignment-select', className)}
|
||||
{...props}
|
||||
>
|
||||
<div {...pickBy(props, (v, k) => startsWith(k, "data-"))}>
|
||||
<Radio.Group className={cx("text-alignment-select", className)} {...props}>
|
||||
<Tooltip title="Align left" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="left" data-test="TextAlignmentSelect.Left">
|
||||
<Icon type="align-left" />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import moment from 'moment';
|
||||
import { isNil } from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import useForceUpdate from '@/lib/hooks/useForceUpdate';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import moment from "moment";
|
||||
import { isNil } from "lodash";
|
||||
import React, { useEffect } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import useForceUpdate from "@/lib/hooks/useForceUpdate";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
function toMoment(value) {
|
||||
value = !isNil(value) ? moment(value) : null;
|
||||
@@ -17,7 +17,7 @@ export function TimeAgo({ date, placeholder, autoUpdate }) {
|
||||
const startDate = toMoment(date);
|
||||
|
||||
const value = startDate ? startDate.fromNow() : placeholder;
|
||||
const title = startDate ? startDate.format(clientConfig.dateTimeFormat) : '';
|
||||
const title = startDate ? startDate.format(clientConfig.dateTimeFormat) : "";
|
||||
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
@@ -38,12 +38,7 @@ export function TimeAgo({ date, placeholder, autoUpdate }) {
|
||||
TimeAgo.propTypes = {
|
||||
// `date` and `placeholder` used in `getDerivedStateFromProps`
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
date: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.instanceOf(Date),
|
||||
Moment,
|
||||
]),
|
||||
date: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date), Moment]),
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
placeholder: PropTypes.string,
|
||||
autoUpdate: PropTypes.bool,
|
||||
@@ -51,35 +46,35 @@ TimeAgo.propTypes = {
|
||||
|
||||
TimeAgo.defaultProps = {
|
||||
date: null,
|
||||
placeholder: '',
|
||||
placeholder: "",
|
||||
autoUpdate: true,
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('amTimeAgo', () => ({
|
||||
ngModule.directive("amTimeAgo", () => ({
|
||||
link($scope, $element, attr) {
|
||||
const modelName = attr.amTimeAgo;
|
||||
$scope.$watch(modelName, (value) => {
|
||||
$scope.$watch(modelName, value => {
|
||||
ReactDOM.render(<TimeAgo date={value} />, $element[0]);
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
$scope.$on("$destroy", () => {
|
||||
ReactDOM.unmountComponentAtNode($element[0]);
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
ngModule.component('rdTimeAgo', {
|
||||
ngModule.component("rdTimeAgo", {
|
||||
bindings: {
|
||||
value: '=',
|
||||
value: "=",
|
||||
},
|
||||
controller($scope, $element) {
|
||||
$scope.$watch('$ctrl.value', () => {
|
||||
$scope.$watch("$ctrl.value", () => {
|
||||
// Initial render will occur here as well
|
||||
ReactDOM.render(<TimeAgo date={this.value} placeholder="-" />, $element[0]);
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
$scope.$on("$destroy", () => {
|
||||
ReactDOM.unmountComponentAtNode($element[0]);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import useForceUpdate from '@/lib/hooks/useForceUpdate';
|
||||
import React, { useMemo, useEffect } from "react";
|
||||
import moment from "moment";
|
||||
import PropTypes from "prop-types";
|
||||
import { react2angular } from "react2angular";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
import useForceUpdate from "@/lib/hooks/useForceUpdate";
|
||||
|
||||
export function Timer({ from }) {
|
||||
const startTime = useMemo(() => moment(from).valueOf(), [from]);
|
||||
@@ -15,18 +15,13 @@ export function Timer({ from }) {
|
||||
}, [forceUpdate]);
|
||||
|
||||
const diff = moment.now() - startTime;
|
||||
const format = diff > 1000 * 60 * 60 ? 'HH:mm:ss' : 'mm:ss'; // no HH under an hour
|
||||
const format = diff > 1000 * 60 * 60 ? "HH:mm:ss" : "mm:ss"; // no HH under an hour
|
||||
|
||||
return (<span className="rd-timer">{moment.utc(diff).format(format)}</span>);
|
||||
return <span className="rd-timer">{moment.utc(diff).format(format)}</span>;
|
||||
}
|
||||
|
||||
Timer.propTypes = {
|
||||
from: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.instanceOf(Date),
|
||||
Moment,
|
||||
]),
|
||||
from: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date), Moment]),
|
||||
};
|
||||
|
||||
Timer.defaultProps = {
|
||||
@@ -34,7 +29,7 @@ Timer.defaultProps = {
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('rdTimer', react2angular(Timer));
|
||||
ngModule.component("rdTimer", react2angular(Timer));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { map } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { map } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Table from 'antd/lib/table';
|
||||
import Card from 'antd/lib/card';
|
||||
import Spin from 'antd/lib/spin';
|
||||
import Badge from 'antd/lib/badge';
|
||||
import { Columns } from '@/components/items-list/components/ItemsTable';
|
||||
import Table from "antd/lib/table";
|
||||
import Card from "antd/lib/card";
|
||||
import Spin from "antd/lib/spin";
|
||||
import Badge from "antd/lib/badge";
|
||||
import { Columns } from "@/components/items-list/components/ItemsTable";
|
||||
|
||||
// CounterCard
|
||||
|
||||
@@ -28,40 +28,48 @@ CounterCard.propTypes = {
|
||||
};
|
||||
|
||||
CounterCard.defaultProps = {
|
||||
value: '',
|
||||
value: "",
|
||||
};
|
||||
|
||||
// Tables
|
||||
|
||||
const commonColumns = [
|
||||
{ title: 'Worker Name', dataIndex: 'worker' },
|
||||
{ title: 'PID', dataIndex: 'worker_pid' },
|
||||
{ title: 'Queue', dataIndex: 'queue' },
|
||||
Columns.custom((value) => {
|
||||
if (value === 'active') {
|
||||
return <span><Badge status="processing" /> Active</span>;
|
||||
{ title: "Worker Name", dataIndex: "worker" },
|
||||
{ title: "PID", dataIndex: "worker_pid" },
|
||||
{ title: "Queue", dataIndex: "queue" },
|
||||
Columns.custom(
|
||||
value => {
|
||||
if (value === "active") {
|
||||
return (
|
||||
<span>
|
||||
<Badge status="processing" /> Active
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span><Badge status="warning" /> {value}</span>;
|
||||
}, {
|
||||
title: 'State',
|
||||
dataIndex: 'state',
|
||||
}),
|
||||
Columns.timeAgo({ title: 'Start Time', dataIndex: 'start_time' }),
|
||||
return (
|
||||
<span>
|
||||
<Badge status="warning" /> {value}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
{
|
||||
title: "State",
|
||||
dataIndex: "state",
|
||||
}
|
||||
),
|
||||
Columns.timeAgo({ title: "Start Time", dataIndex: "start_time" }),
|
||||
];
|
||||
|
||||
const queryColumns = commonColumns.concat([
|
||||
Columns.timeAgo({ title: 'Enqueue Time', dataIndex: 'enqueue_time' }),
|
||||
{ title: 'Query ID', dataIndex: 'query_id' },
|
||||
{ title: 'Org ID', dataIndex: 'org_id' },
|
||||
{ title: 'Data Source ID', dataIndex: 'data_source_id' },
|
||||
{ title: 'User ID', dataIndex: 'user_id' },
|
||||
{ title: 'Scheduled', dataIndex: 'scheduled' },
|
||||
Columns.timeAgo({ title: "Enqueue Time", dataIndex: "enqueue_time" }),
|
||||
{ title: "Query ID", dataIndex: "query_id" },
|
||||
{ title: "Org ID", dataIndex: "org_id" },
|
||||
{ title: "Data Source ID", dataIndex: "data_source_id" },
|
||||
{ title: "User ID", dataIndex: "user_id" },
|
||||
{ title: "Scheduled", dataIndex: "scheduled" },
|
||||
]);
|
||||
|
||||
const queuesColumns = map(
|
||||
['Name', 'Active', 'Reserved', 'Waiting'],
|
||||
c => ({ title: c, dataIndex: c.toLowerCase() }),
|
||||
);
|
||||
const queuesColumns = map(["Name", "Active", "Reserved", "Waiting"], c => ({ title: c, dataIndex: c.toLowerCase() }));
|
||||
|
||||
const TablePropTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
@@ -69,27 +77,13 @@ const TablePropTypes = {
|
||||
};
|
||||
|
||||
export function QueuesTable({ loading, items }) {
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={queuesColumns}
|
||||
rowKey="name"
|
||||
dataSource={items}
|
||||
/>
|
||||
);
|
||||
return <Table loading={loading} columns={queuesColumns} rowKey="name" dataSource={items} />;
|
||||
}
|
||||
|
||||
QueuesTable.propTypes = TablePropTypes;
|
||||
|
||||
export function QueriesTable({ loading, items }) {
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={queryColumns}
|
||||
rowKey="task_id"
|
||||
dataSource={items}
|
||||
/>
|
||||
);
|
||||
return <Table loading={loading} columns={queryColumns} rowKey="task_id" dataSource={items} />;
|
||||
}
|
||||
|
||||
QueriesTable.propTypes = TablePropTypes;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Tabs from 'antd/lib/tabs';
|
||||
import { PageHeader } from '@/components/PageHeader';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Tabs from "antd/lib/tabs";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
|
||||
import './layout.less';
|
||||
import "./layout.less";
|
||||
|
||||
export default function Layout({ activeTab, children }) {
|
||||
return (
|
||||
@@ -13,16 +13,16 @@ export default function Layout({ activeTab, children }) {
|
||||
<div className="bg-white tiled">
|
||||
<Tabs className="admin-page-layout-tabs" defaultActiveKey={activeTab} animated={false}>
|
||||
<Tabs.TabPane key="system_status" tab={<a href="admin/status">System Status</a>}>
|
||||
{(activeTab === 'system_status') ? children : null}
|
||||
{activeTab === "system_status" ? children : null}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="tasks" tab={<a href="admin/queries/tasks">Celery Status</a>}>
|
||||
{(activeTab === 'tasks') ? children : null}
|
||||
{activeTab === "tasks" ? children : null}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="jobs" tab={<a href="admin/queries/jobs">RQ Status</a>}>
|
||||
{(activeTab === 'jobs') ? children : null}
|
||||
{activeTab === "jobs" ? children : null}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="outdated_queries" tab={<a href="admin/queries/outdated">Outdated Queries</a>}>
|
||||
{(activeTab === 'outdated_queries') ? children : null}
|
||||
{activeTab === "outdated_queries" ? children : null}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
@@ -36,6 +36,6 @@ Layout.propTypes = {
|
||||
};
|
||||
|
||||
Layout.defaultProps = {
|
||||
activeTab: 'system_status',
|
||||
activeTab: "system_status",
|
||||
children: null,
|
||||
};
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
import { map } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { map } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Badge from 'antd/lib/badge';
|
||||
import Table from 'antd/lib/table';
|
||||
import { Columns } from '@/components/items-list/components/ItemsTable';
|
||||
import Badge from "antd/lib/badge";
|
||||
import Table from "antd/lib/table";
|
||||
import { Columns } from "@/components/items-list/components/ItemsTable";
|
||||
|
||||
// Tables
|
||||
|
||||
const otherJobsColumns = [
|
||||
{ title: 'Queue', dataIndex: 'queue' },
|
||||
{ title: 'Job Name', dataIndex: 'name' },
|
||||
Columns.timeAgo({ title: 'Start Time', dataIndex: 'started_at' }),
|
||||
Columns.timeAgo({ title: 'Enqueue Time', dataIndex: 'enqueued_at' }),
|
||||
{ title: "Queue", dataIndex: "queue" },
|
||||
{ title: "Job Name", dataIndex: "name" },
|
||||
Columns.timeAgo({ title: "Start Time", dataIndex: "started_at" }),
|
||||
Columns.timeAgo({ title: "Enqueue Time", dataIndex: "enqueued_at" }),
|
||||
];
|
||||
|
||||
const workersColumns = [Columns.custom(
|
||||
const workersColumns = [
|
||||
Columns.custom(
|
||||
value => (
|
||||
<span><Badge status={{ busy: 'processing',
|
||||
idle: 'default',
|
||||
started: 'success',
|
||||
suspended: 'warning' }[value]}
|
||||
/> {value}
|
||||
<span>
|
||||
<Badge status={{ busy: "processing", idle: "default", started: "success", suspended: "warning" }[value]} />{" "}
|
||||
{value}
|
||||
</span>
|
||||
), { title: 'State', dataIndex: 'state' },
|
||||
)].concat(map(['Hostname', 'PID', 'Name', 'Queues', 'Current Job', 'Successful Jobs', 'Failed Jobs'],
|
||||
c => ({ title: c, dataIndex: c.toLowerCase().replace(/\s/g, '_') }))).concat([
|
||||
Columns.dateTime({ title: 'Birth Date', dataIndex: 'birth_date' }),
|
||||
Columns.duration({ title: 'Total Working Time', dataIndex: 'total_working_time' }),
|
||||
),
|
||||
{ title: "State", dataIndex: "state" }
|
||||
),
|
||||
]
|
||||
.concat(
|
||||
map(["Hostname", "PID", "Name", "Queues", "Current Job", "Successful Jobs", "Failed Jobs"], c => ({
|
||||
title: c,
|
||||
dataIndex: c.toLowerCase().replace(/\s/g, "_"),
|
||||
}))
|
||||
)
|
||||
.concat([
|
||||
Columns.dateTime({ title: "Birth Date", dataIndex: "birth_date" }),
|
||||
Columns.duration({ title: "Total Working Time", dataIndex: "total_working_time" }),
|
||||
]);
|
||||
|
||||
const queuesColumns = map(
|
||||
['Name', 'Started', 'Queued'],
|
||||
c => ({ title: c, dataIndex: c.toLowerCase() }),
|
||||
);
|
||||
const queuesColumns = map(["Name", "Started", "Queued"], c => ({ title: c, dataIndex: c.toLowerCase() }));
|
||||
|
||||
const TablePropTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
@@ -49,7 +53,7 @@ export function WorkersTable({ loading, items }) {
|
||||
dataSource={items}
|
||||
pagination={{
|
||||
defaultPageSize: 25,
|
||||
pageSizeOptions: ['10', '25', '50'],
|
||||
pageSizeOptions: ["10", "25", "50"],
|
||||
showSizeChanger: true,
|
||||
}}
|
||||
/>
|
||||
@@ -59,27 +63,13 @@ export function WorkersTable({ loading, items }) {
|
||||
WorkersTable.propTypes = TablePropTypes;
|
||||
|
||||
export function QueuesTable({ loading, items }) {
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={queuesColumns}
|
||||
rowKey="name"
|
||||
dataSource={items}
|
||||
/>
|
||||
);
|
||||
return <Table loading={loading} columns={queuesColumns} rowKey="name" dataSource={items} />;
|
||||
}
|
||||
|
||||
QueuesTable.propTypes = TablePropTypes;
|
||||
|
||||
export function OtherJobsTable({ loading, items }) {
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
columns={otherJobsColumns}
|
||||
rowKey="id"
|
||||
dataSource={items}
|
||||
/>
|
||||
);
|
||||
return <Table loading={loading} columns={otherJobsColumns} rowKey="id" dataSource={items} />;
|
||||
}
|
||||
|
||||
OtherJobsTable.propTypes = TablePropTypes;
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
|
||||
import { toPairs } from 'lodash';
|
||||
import React from 'react';
|
||||
import { toPairs } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import List from 'antd/lib/list';
|
||||
import Card from 'antd/lib/card';
|
||||
import { TimeAgo } from '@/components/TimeAgo';
|
||||
import List from "antd/lib/list";
|
||||
import Card from "antd/lib/card";
|
||||
import { TimeAgo } from "@/components/TimeAgo";
|
||||
|
||||
import { toHuman, prettySize } from '@/filters';
|
||||
import { toHuman, prettySize } from "@/filters";
|
||||
|
||||
export function General({ info }) {
|
||||
info = toPairs(info);
|
||||
return (
|
||||
<Card title="General" size="small">
|
||||
{(info.length === 0) && (
|
||||
<div className="text-muted text-center">No data</div>
|
||||
)}
|
||||
{(info.length > 0) && (
|
||||
{info.length === 0 && <div className="text-muted text-center">No data</div>}
|
||||
{info.length > 0 && (
|
||||
<List
|
||||
size="small"
|
||||
itemLayout="vertical"
|
||||
dataSource={info}
|
||||
renderItem={([name, value]) => (
|
||||
<List.Item extra={<span className="badge">{value}</span>}>
|
||||
{toHuman(name)}
|
||||
</List.Item>
|
||||
<List.Item extra={<span className="badge">{value}</span>}>{toHuman(name)}</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -35,18 +31,14 @@ export function General({ info }) {
|
||||
export function DatabaseMetrics({ info }) {
|
||||
return (
|
||||
<Card title="Redash Database" size="small">
|
||||
{(info.length === 0) && (
|
||||
<div className="text-muted text-center">No data</div>
|
||||
)}
|
||||
{(info.length > 0) && (
|
||||
{info.length === 0 && <div className="text-muted text-center">No data</div>}
|
||||
{info.length > 0 && (
|
||||
<List
|
||||
size="small"
|
||||
itemLayout="vertical"
|
||||
dataSource={info}
|
||||
renderItem={([name, size]) => (
|
||||
<List.Item extra={<span className="badge">{prettySize(size)}</span>}>
|
||||
{name}
|
||||
</List.Item>
|
||||
<List.Item extra={<span className="badge">{prettySize(size)}</span>}>{name}</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -58,18 +50,14 @@ export function Queues({ info }) {
|
||||
info = toPairs(info);
|
||||
return (
|
||||
<Card title="Queues" size="small">
|
||||
{(info.length === 0) && (
|
||||
<div className="text-muted text-center">No data</div>
|
||||
)}
|
||||
{(info.length > 0) && (
|
||||
{info.length === 0 && <div className="text-muted text-center">No data</div>}
|
||||
{info.length > 0 && (
|
||||
<List
|
||||
size="small"
|
||||
itemLayout="vertical"
|
||||
dataSource={info}
|
||||
renderItem={([name, queue]) => (
|
||||
<List.Item extra={<span className="badge">{queue.size}</span>}>
|
||||
{name}
|
||||
</List.Item>
|
||||
<List.Item extra={<span className="badge">{queue.size}</span>}>{name}</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -78,33 +66,34 @@ export function Queues({ info }) {
|
||||
}
|
||||
|
||||
export function Manager({ info }) {
|
||||
const items = info ? [(
|
||||
<List.Item extra={<span className="badge"><TimeAgo date={info.lastRefreshAt} placeholder="n/a" /></span>}>
|
||||
const items = info
|
||||
? [
|
||||
<List.Item
|
||||
extra={
|
||||
<span className="badge">
|
||||
<TimeAgo date={info.lastRefreshAt} placeholder="n/a" />
|
||||
</span>
|
||||
}>
|
||||
Last Refresh
|
||||
</List.Item>
|
||||
), (
|
||||
<List.Item extra={<span className="badge"><TimeAgo date={info.startedAt} placeholder="n/a" /></span>}>
|
||||
</List.Item>,
|
||||
<List.Item
|
||||
extra={
|
||||
<span className="badge">
|
||||
<TimeAgo date={info.startedAt} placeholder="n/a" />
|
||||
</span>
|
||||
}>
|
||||
Started
|
||||
</List.Item>
|
||||
), (
|
||||
</List.Item>,
|
||||
<List.Item extra={<span className="badge">{info.outdatedQueriesCount}</span>}>
|
||||
Outdated Queries Count
|
||||
</List.Item>
|
||||
)] : [];
|
||||
</List.Item>,
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<Card title="Manager" size="small">
|
||||
{!info && (
|
||||
<div className="text-muted text-center">No data</div>
|
||||
)}
|
||||
{info && (
|
||||
<List
|
||||
size="small"
|
||||
itemLayout="vertical"
|
||||
dataSource={items}
|
||||
renderItem={item => item}
|
||||
/>
|
||||
)}
|
||||
{!info && <div className="text-muted text-center">No data</div>}
|
||||
{info && <List size="small" itemLayout="vertical" dataSource={items} renderItem={item => item} />}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import React, { useRef } from "react";
|
||||
import { react2angular } from "react2angular";
|
||||
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Button from 'antd/lib/button';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import Input from 'antd/lib/input';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Button from "antd/lib/button";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Menu from "antd/lib/menu";
|
||||
import Input from "antd/lib/input";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
import FavoritesDropdown from './components/FavoritesDropdown';
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import CreateDashboardDialog from '@/components/dashboards/CreateDashboardDialog';
|
||||
import FavoritesDropdown from "./components/FavoritesDropdown";
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
|
||||
|
||||
import { currentUser, Auth, clientConfig } from '@/services/auth';
|
||||
import { $location, $route } from '@/services/ng';
|
||||
import { Dashboard } from '@/services/dashboard';
|
||||
import { Query } from '@/services/query';
|
||||
import frontendVersion from '@/version.json';
|
||||
import logoUrl from '@/assets/images/redash_icon_small.png';
|
||||
import { currentUser, Auth, clientConfig } from "@/services/auth";
|
||||
import { $location, $route } from "@/services/ng";
|
||||
import { Dashboard } from "@/services/dashboard";
|
||||
import { Query } from "@/services/query";
|
||||
import frontendVersion from "@/version.json";
|
||||
import logoUrl from "@/assets/images/redash_icon_small.png";
|
||||
|
||||
import './AppHeader.less';
|
||||
import "./AppHeader.less";
|
||||
|
||||
function onSearch(q) {
|
||||
$location.path('/queries').search({ q });
|
||||
$location.path("/queries").search({ q });
|
||||
$route.reload();
|
||||
}
|
||||
|
||||
@@ -33,34 +33,34 @@ function DesktopNavbar() {
|
||||
<div className="app-header" data-platform="desktop">
|
||||
<div>
|
||||
<Menu mode="horizontal" selectable={false}>
|
||||
{currentUser.hasPermission('list_dashboards') && (
|
||||
{currentUser.hasPermission("list_dashboards") && (
|
||||
<Menu.Item key="dashboards" className="dropdown-menu-item">
|
||||
<Button href="dashboards">Dashboards</Button>
|
||||
<FavoritesDropdown fetch={Dashboard.favorites} urlTemplate="dashboard/${slug}" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('view_query') && (
|
||||
{currentUser.hasPermission("view_query") && (
|
||||
<Menu.Item key="queries" className="dropdown-menu-item">
|
||||
<Button href="queries">Queries</Button>
|
||||
<FavoritesDropdown fetch={Query.favorites} urlTemplate="queries/${id}" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('list_alerts') && (
|
||||
{currentUser.hasPermission("list_alerts") && (
|
||||
<Menu.Item key="alerts">
|
||||
<Button href="alerts">Alerts</Button>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={(
|
||||
trigger={["click"]}
|
||||
overlay={
|
||||
<Menu>
|
||||
{currentUser.hasPermission('create_query') && (
|
||||
{currentUser.hasPermission("create_query") && (
|
||||
<Menu.Item key="new-query">
|
||||
<a href="queries/new">New Query</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('create_dashboard') && (
|
||||
{currentUser.hasPermission("create_dashboard") && (
|
||||
<Menu.Item key="new-dashboard">
|
||||
<a onMouseUp={() => CreateDashboardDialog.showModal()}>New Dashboard</a>
|
||||
</Menu.Item>
|
||||
@@ -69,8 +69,7 @@ function DesktopNavbar() {
|
||||
<a href="alerts/new">New Alert</a>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
}>
|
||||
<Button type="primary" data-test="CreateButton">
|
||||
Create <Icon type="down" />
|
||||
</Button>
|
||||
@@ -105,26 +104,24 @@ function DesktopNavbar() {
|
||||
<Dropdown
|
||||
overlayStyle={{ minWidth: 200 }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
overlay={(
|
||||
trigger={["click"]}
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item key="profile">
|
||||
<a href="users/me">Edit Profile</a>
|
||||
</Menu.Item>
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
<Menu.Divider />
|
||||
)}
|
||||
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
|
||||
{currentUser.isAdmin && (
|
||||
<Menu.Item key="datasources">
|
||||
<a href="data_sources">Data Sources</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('list_users') && (
|
||||
{currentUser.hasPermission("list_users") && (
|
||||
<Menu.Item key="groups">
|
||||
<a href="groups">Groups</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('list_users') && (
|
||||
{currentUser.hasPermission("list_users") && (
|
||||
<Menu.Item key="users">
|
||||
<a href="users">Users</a>
|
||||
</Menu.Item>
|
||||
@@ -132,38 +129,41 @@ function DesktopNavbar() {
|
||||
<Menu.Item key="snippets">
|
||||
<a href="query_snippets">Query Snippets</a>
|
||||
</Menu.Item>
|
||||
{currentUser.hasPermission('list_users') && (
|
||||
{currentUser.hasPermission("list_users") && (
|
||||
<Menu.Item key="destinations">
|
||||
<a href="destinations">Alert Destinations</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
<Menu.Divider />
|
||||
)}
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
|
||||
{currentUser.hasPermission("super_admin") && (
|
||||
<Menu.Item key="status">
|
||||
<a href="admin/status">System Status</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="logout" onClick={() => Auth.logout()}>Log out</Menu.Item>
|
||||
<Menu.Item key="logout" onClick={() => Auth.logout()}>
|
||||
Log out
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="version" disabled>
|
||||
Version: {clientConfig.version}
|
||||
{frontendVersion !== clientConfig.version && ` (${frontendVersion.substring(0, 8)})`}
|
||||
{clientConfig.newVersionAvailable && currentUser.hasPermission('super_admin') && (
|
||||
{clientConfig.newVersionAvailable && currentUser.hasPermission("super_admin") && (
|
||||
<Tooltip title="Update Available" placement="rightTop">
|
||||
{' '}
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a href="https://version.redash.io/" className="update-available" target="_blank" rel="noopener">
|
||||
{" "}
|
||||
{/* eslint-disable react/jsx-no-target-blank */}
|
||||
<a
|
||||
href="https://version.redash.io/"
|
||||
className="update-available"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
<i className="fa fa-arrow-circle-down" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
}>
|
||||
<Button data-test="ProfileDropdown" className="profile-dropdown">
|
||||
<img src={currentUser.profile_image_url} alt={currentUser.name} />
|
||||
<span>{currentUser.name}</span>
|
||||
@@ -190,21 +190,21 @@ function MobileNavbar() {
|
||||
<div>
|
||||
<Dropdown
|
||||
overlayStyle={{ minWidth: 200 }}
|
||||
trigger={['click']}
|
||||
trigger={["click"]}
|
||||
getPopupContainer={() => ref.current} // so the overlay menu stays with the fixed header when page scrolls
|
||||
overlay={(
|
||||
overlay={
|
||||
<Menu mode="vertical" selectable={false}>
|
||||
{currentUser.hasPermission('list_dashboards') && (
|
||||
{currentUser.hasPermission("list_dashboards") && (
|
||||
<Menu.Item key="dashboards">
|
||||
<a href="dashboards">Dashboards</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('view_query') && (
|
||||
{currentUser.hasPermission("view_query") && (
|
||||
<Menu.Item key="queries">
|
||||
<a href="queries">Queries</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('list_alerts') && (
|
||||
{currentUser.hasPermission("list_alerts") && (
|
||||
<Menu.Item key="alerts">
|
||||
<a href="alerts">Alerts</a>
|
||||
</Menu.Item>
|
||||
@@ -218,23 +218,26 @@ function MobileNavbar() {
|
||||
<a href="data_sources">Settings</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
{currentUser.hasPermission("super_admin") && (
|
||||
<Menu.Item key="status">
|
||||
<a href="admin/status">System Status</a>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{currentUser.hasPermission('super_admin') && (
|
||||
<Menu.Divider />
|
||||
)}
|
||||
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
|
||||
<Menu.Item key="help">
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a href="https://redash.io/help" target="_blank" rel="noopener">Help</a>
|
||||
<a href="https://redash.io/help" target="_blank" rel="noopener">
|
||||
Help
|
||||
</a>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="logout" onClick={() => Auth.logout()}>
|
||||
Log out
|
||||
</Menu.Item>
|
||||
<Menu.Item key="logout" onClick={() => Auth.logout()}>Log out</Menu.Item>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
<Button><Icon type="menu" /></Button>
|
||||
}>
|
||||
<Button>
|
||||
<Icon type="menu" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,7 +254,7 @@ export function AppHeader() {
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('appHeader', react2angular(AppHeader));
|
||||
ngModule.component("appHeader", react2angular(AppHeader));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isEmpty, template } from 'lodash';
|
||||
import React, { useState, useMemo, useCallback, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { isEmpty, template } from "lodash";
|
||||
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Menu from "antd/lib/menu";
|
||||
|
||||
import HelpTrigger from '@/components/HelpTrigger';
|
||||
import HelpTrigger from "@/components/HelpTrigger";
|
||||
|
||||
export default function FavoritesDropdown({ fetch, urlTemplate }) {
|
||||
const [items, setItems] = useState();
|
||||
@@ -15,16 +15,19 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
|
||||
const noItems = isEmpty(items);
|
||||
const urlCompiled = useMemo(() => template(urlTemplate), [urlTemplate]);
|
||||
|
||||
const fetchItems = useCallback((showLoadingState = true) => {
|
||||
const fetchItems = useCallback(
|
||||
(showLoadingState = true) => {
|
||||
setLoading(showLoadingState);
|
||||
fetch().$promise
|
||||
.then(({ results }) => {
|
||||
fetch()
|
||||
.$promise.then(({ results }) => {
|
||||
setItems(results);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [fetch]);
|
||||
},
|
||||
[fetch]
|
||||
);
|
||||
|
||||
// fetch items on init
|
||||
useEffect(() => {
|
||||
@@ -59,7 +62,12 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown disabled={loading} trigger={['click']} placement="bottomLeft" onVisibleChange={onVisibleChange} overlay={menu}>
|
||||
<Dropdown
|
||||
disabled={loading}
|
||||
trigger={["click"]}
|
||||
placement="bottomLeft"
|
||||
onVisibleChange={onVisibleChange}
|
||||
overlay={menu}>
|
||||
{loading ? <Icon type="loading" spin /> : <Icon type="down" />}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import PromiseRejectionError from '@/lib/promise-rejection-error';
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export class ErrorHandler {
|
||||
@@ -18,10 +18,7 @@ export class ErrorHandler {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
if (
|
||||
(error === null) ||
|
||||
(error instanceof PromiseRejectionError)
|
||||
) {
|
||||
if (error === null || error instanceof PromiseRejectionError) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import debug from 'debug';
|
||||
import PromiseRejectionError from '@/lib/promise-rejection-error';
|
||||
import { ErrorHandler } from './error-handler';
|
||||
import template from './template.html';
|
||||
import debug from "debug";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
import { ErrorHandler } from "./error-handler";
|
||||
import template from "./template.html";
|
||||
|
||||
const logger = debug('redash:app-view');
|
||||
const logger = debug("redash:app-view");
|
||||
|
||||
const handler = new ErrorHandler();
|
||||
|
||||
@@ -14,7 +14,7 @@ const layouts = {
|
||||
},
|
||||
fixed: {
|
||||
showHeader: true,
|
||||
bodyClass: 'fixed-layout',
|
||||
bodyClass: "fixed-layout",
|
||||
},
|
||||
defaultSignedOut: {
|
||||
showHeader: false,
|
||||
@@ -37,7 +37,7 @@ class AppViewComponent {
|
||||
this.layout = layouts.defaultSignedOut;
|
||||
this.handler = handler;
|
||||
|
||||
$rootScope.$on('$routeChangeStart', (event, route) => {
|
||||
$rootScope.$on("$routeChangeStart", (event, route) => {
|
||||
this.handler.reset();
|
||||
|
||||
// In case we're handling $routeProvider.otherwise call, there will be no
|
||||
@@ -47,7 +47,7 @@ class AppViewComponent {
|
||||
if ($$route.authenticated) {
|
||||
// For routes that need authentication, check if session is already
|
||||
// loaded, and load it if not.
|
||||
logger('Requested authenticated route: ', route);
|
||||
logger("Requested authenticated route: ", route);
|
||||
if (!Auth.isAuthenticated()) {
|
||||
event.preventDefault();
|
||||
// Auth.requireSession resolves only if session loaded
|
||||
@@ -59,12 +59,12 @@ class AppViewComponent {
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.$on('$routeChangeSuccess', (event, route) => {
|
||||
$rootScope.$on("$routeChangeSuccess", (event, route) => {
|
||||
const $$route = route.$$route || { authenticated: true };
|
||||
this.applyLayout($$route);
|
||||
});
|
||||
|
||||
$rootScope.$on('$routeChangeError', (event, current, previous, rejection) => {
|
||||
$rootScope.$on("$routeChangeError", (event, current, previous, rejection) => {
|
||||
const $$route = current.$$route || { authenticated: true };
|
||||
this.applyLayout($$route);
|
||||
throw new PromiseRejectionError(rejection);
|
||||
@@ -79,13 +79,14 @@ class AppViewComponent {
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.factory(
|
||||
'$exceptionHandler',
|
||||
() => function exceptionHandler(exception) {
|
||||
"$exceptionHandler",
|
||||
() =>
|
||||
function exceptionHandler(exception) {
|
||||
handler.process(exception);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
ngModule.component('appView', {
|
||||
ngModule.component("appView", {
|
||||
template,
|
||||
controller: AppViewComponent,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
function cancelQueryButton() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
restrict: "E",
|
||||
scope: {
|
||||
queryId: '=',
|
||||
taskId: '=',
|
||||
queryId: "=",
|
||||
taskId: "=",
|
||||
},
|
||||
transclude: true,
|
||||
template:
|
||||
@@ -16,11 +16,11 @@ function cancelQueryButton() {
|
||||
$http.delete(`api/jobs/${$scope.taskId}`).success(() => {});
|
||||
|
||||
let queryId = $scope.queryId;
|
||||
if ($scope.queryId === 'adhoc') {
|
||||
if ($scope.queryId === "adhoc") {
|
||||
queryId = null;
|
||||
}
|
||||
|
||||
Events.record('cancel_execute', 'query', queryId, { admin: true });
|
||||
Events.record("cancel_execute", "query", queryId, { admin: true });
|
||||
$scope.inProgress = true;
|
||||
};
|
||||
},
|
||||
@@ -28,7 +28,7 @@ function cancelQueryButton() {
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('cancelQueryButton', cancelQueryButton);
|
||||
ngModule.directive("cancelQueryButton", cancelQueryButton);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Input from 'antd/lib/input';
|
||||
import { includes, isEmpty } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import EmptyState from '@/components/items-list/components/EmptyState';
|
||||
import Input from "antd/lib/input";
|
||||
import { includes, isEmpty } from "lodash";
|
||||
import PropTypes from "prop-types";
|
||||
import React from "react";
|
||||
import EmptyState from "@/components/items-list/components/EmptyState";
|
||||
|
||||
import './CardsList.less';
|
||||
import "./CardsList.less";
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
@@ -16,7 +16,7 @@ export default class CardsList extends React.Component {
|
||||
imgSrc: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
href: PropTypes.string,
|
||||
}),
|
||||
})
|
||||
),
|
||||
showSearch: PropTypes.bool,
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export default class CardsList extends React.Component {
|
||||
};
|
||||
|
||||
state = {
|
||||
searchText: '',
|
||||
searchText: "",
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -35,7 +35,7 @@ export default class CardsList extends React.Component {
|
||||
this.items = [];
|
||||
|
||||
let itemId = 1;
|
||||
props.items.forEach((item) => {
|
||||
props.items.forEach(item => {
|
||||
this.items.push({ id: itemId, ...item });
|
||||
itemId += 1;
|
||||
});
|
||||
@@ -55,23 +55,22 @@ export default class CardsList extends React.Component {
|
||||
const { showSearch } = this.props;
|
||||
const { searchText } = this.state;
|
||||
|
||||
const filteredItems = this.items.filter(item => isEmpty(searchText) ||
|
||||
includes(item.title.toLowerCase(), searchText.toLowerCase()));
|
||||
const filteredItems = this.items.filter(
|
||||
item => isEmpty(searchText) || includes(item.title.toLowerCase(), searchText.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-test="CardsList">
|
||||
{showSearch && (
|
||||
<div className="row p-10">
|
||||
<div className="col-md-4 col-md-offset-4">
|
||||
<Search
|
||||
placeholder="Search..."
|
||||
onChange={e => this.setState({ searchText: e.target.value })}
|
||||
autoFocus
|
||||
/>
|
||||
<Search placeholder="Search..." onChange={e => this.setState({ searchText: e.target.value })} autoFocus />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isEmpty(filteredItems) ? (<EmptyState className="" />) : (
|
||||
{isEmpty(filteredItems) ? (
|
||||
<EmptyState className="" />
|
||||
) : (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 d-inline-flex flex-wrap visual-card-list">
|
||||
{filteredItems.map(item => this.renderListItem(item))}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { each, values, map, includes, first } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'antd/lib/select';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import {
|
||||
MappingType,
|
||||
ParameterMappingListInput,
|
||||
} from '@/components/ParameterMappingInput';
|
||||
import { QuerySelector } from '@/components/QuerySelector';
|
||||
import { each, values, map, includes, first } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Select from "antd/lib/select";
|
||||
import Modal from "antd/lib/modal";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import { MappingType, ParameterMappingListInput } from "@/components/ParameterMappingInput";
|
||||
import { QuerySelector } from "@/components/QuerySelector";
|
||||
|
||||
import notification from '@/services/notification';
|
||||
import notification from "@/services/notification";
|
||||
|
||||
import { Query } from '@/services/query';
|
||||
import { Query } from "@/services/query";
|
||||
|
||||
const { Option, OptGroup } = Select;
|
||||
|
||||
@@ -39,21 +36,19 @@ class AddWidgetDialog extends React.Component {
|
||||
});
|
||||
|
||||
if (selectedQuery) {
|
||||
Query.get({ id: selectedQuery.id }, (query) => {
|
||||
Query.get({ id: selectedQuery.id }, query => {
|
||||
if (query) {
|
||||
const existingParamNames = map(
|
||||
this.props.dashboard.getParametersDefs(),
|
||||
param => param.name,
|
||||
);
|
||||
const existingParamNames = map(this.props.dashboard.getParametersDefs(), param => param.name);
|
||||
this.setState({
|
||||
selectedQuery: query,
|
||||
parameterMappings: map(query.getParametersDefs(), param => ({
|
||||
name: param.name,
|
||||
type: includes(existingParamNames, param.name)
|
||||
? MappingType.DashboardMapToExisting : MappingType.DashboardAddNew,
|
||||
? MappingType.DashboardMapToExisting
|
||||
: MappingType.DashboardAddNew,
|
||||
mapTo: param.name,
|
||||
value: param.normalizedValue,
|
||||
title: '',
|
||||
title: "",
|
||||
param,
|
||||
})),
|
||||
});
|
||||
@@ -66,7 +61,7 @@ class AddWidgetDialog extends React.Component {
|
||||
}
|
||||
|
||||
selectVisualization(query, visualizationId) {
|
||||
each(query.visualizations, (visualization) => {
|
||||
each(query.visualizations, visualization => {
|
||||
if (visualization.id === visualizationId) {
|
||||
this.setState({ selectedVis: visualization });
|
||||
return false;
|
||||
@@ -79,12 +74,13 @@ class AddWidgetDialog extends React.Component {
|
||||
|
||||
this.setState({ saveInProgress: true });
|
||||
|
||||
this.props.onConfirm(selectedVis, parameterMappings)
|
||||
this.props
|
||||
.onConfirm(selectedVis, parameterMappings)
|
||||
.then(() => {
|
||||
this.props.dialog.close();
|
||||
})
|
||||
.catch(() => {
|
||||
notification.error('Widget could not be added');
|
||||
notification.error("Widget could not be added");
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ saveInProgress: false });
|
||||
@@ -98,7 +94,7 @@ class AddWidgetDialog extends React.Component {
|
||||
renderVisualizationInput() {
|
||||
let visualizationGroups = {};
|
||||
if (this.state.selectedQuery) {
|
||||
each(this.state.selectedQuery.visualizations, (vis) => {
|
||||
each(this.state.selectedQuery.visualizations, vis => {
|
||||
visualizationGroups[vis.type] = visualizationGroups[vis.type] || [];
|
||||
visualizationGroups[vis.type].push(vis);
|
||||
});
|
||||
@@ -112,12 +108,13 @@ class AddWidgetDialog extends React.Component {
|
||||
id="choose-visualization"
|
||||
className="w-100"
|
||||
defaultValue={first(this.state.selectedQuery.visualizations).id}
|
||||
onChange={visualizationId => this.selectVisualization(this.state.selectedQuery, visualizationId)}
|
||||
>
|
||||
onChange={visualizationId => this.selectVisualization(this.state.selectedQuery, visualizationId)}>
|
||||
{visualizationGroups.map(visualizations => (
|
||||
<OptGroup label={visualizations[0].type} key={visualizations[0].type}>
|
||||
{visualizations.map(visualization => (
|
||||
<Option value={visualization.id} key={visualization.id}>{visualization.name}</Option>
|
||||
<Option value={visualization.id} key={visualization.id}>
|
||||
{visualization.name}
|
||||
</Option>
|
||||
))}
|
||||
</OptGroup>
|
||||
))}
|
||||
@@ -141,15 +138,15 @@ class AddWidgetDialog extends React.Component {
|
||||
disabled: !this.state.selectedQuery,
|
||||
}}
|
||||
okText="Add to Dashboard"
|
||||
width={700}
|
||||
>
|
||||
width={700}>
|
||||
<div data-test="AddWidgetDialog">
|
||||
<QuerySelector onChange={query => this.selectQuery(query)} />
|
||||
{this.state.selectedQuery && this.renderVisualizationInput()}
|
||||
|
||||
{
|
||||
(this.state.parameterMappings.length > 0) && [
|
||||
<label key="parameters-title" htmlFor="parameter-mappings">Parameters</label>,
|
||||
{this.state.parameterMappings.length > 0 && [
|
||||
<label key="parameters-title" htmlFor="parameter-mappings">
|
||||
Parameters
|
||||
</label>,
|
||||
<ParameterMappingListInput
|
||||
key="parameters-list"
|
||||
id="parameter-mappings"
|
||||
@@ -157,8 +154,7 @@ class AddWidgetDialog extends React.Component {
|
||||
existingParams={existingParams}
|
||||
onChange={mappings => this.updateParamMappings(mappings)}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
]}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { includes, reduce, some } from 'lodash';
|
||||
import { includes, reduce, some } from "lodash";
|
||||
|
||||
// TODO: Revisit this implementation when migrating widget component to React
|
||||
|
||||
const WIDGET_SELECTOR = '[data-widgetid="{0}"]';
|
||||
const WIDGET_CONTENT_SELECTOR = [
|
||||
'.widget-header', // header
|
||||
'.visualization-renderer', // visualization
|
||||
'.scrollbox .alert', // error state
|
||||
'.spinner-container', // loading state
|
||||
'.tile__bottom-control', // footer
|
||||
].join(',');
|
||||
".widget-header", // header
|
||||
".visualization-renderer", // visualization
|
||||
".scrollbox .alert", // error state
|
||||
".spinner-container", // loading state
|
||||
".tile__bottom-control", // footer
|
||||
].join(",");
|
||||
const INTERVAL = 200;
|
||||
|
||||
export default class AutoHeightController {
|
||||
@@ -29,9 +29,7 @@ export default class AutoHeightController {
|
||||
.map(widget => widget.id.toString());
|
||||
|
||||
// added
|
||||
newWidgetIds
|
||||
.filter(id => !includes(Object.keys(this.widgets), id))
|
||||
.forEach(this.add);
|
||||
newWidgetIds.filter(id => !includes(Object.keys(this.widgets), id)).forEach(this.add);
|
||||
|
||||
// removed
|
||||
Object.keys(this.widgets)
|
||||
@@ -39,12 +37,12 @@ export default class AutoHeightController {
|
||||
.forEach(this.remove);
|
||||
}
|
||||
|
||||
add = (id) => {
|
||||
add = id => {
|
||||
if (this.isEmpty()) {
|
||||
this.start();
|
||||
}
|
||||
|
||||
const selector = WIDGET_SELECTOR.replace('{0}', id);
|
||||
const selector = WIDGET_SELECTOR.replace("{0}", id);
|
||||
this.widgets[id] = [
|
||||
function getHeight() {
|
||||
const widgetEl = document.querySelector(selector);
|
||||
@@ -56,15 +54,19 @@ export default class AutoHeightController {
|
||||
const els = widgetEl.querySelectorAll(WIDGET_CONTENT_SELECTOR);
|
||||
|
||||
// calculate accumulated height
|
||||
return reduce(els, (acc, el) => {
|
||||
return reduce(
|
||||
els,
|
||||
(acc, el) => {
|
||||
const height = el ? el.getBoundingClientRect().height : 0;
|
||||
return acc + height;
|
||||
}, 0);
|
||||
},
|
||||
0
|
||||
);
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
remove = (id) => {
|
||||
remove = id => {
|
||||
// ignore if not an active autoHeight widget
|
||||
if (!this.exists(id)) {
|
||||
return;
|
||||
@@ -83,10 +85,9 @@ export default class AutoHeightController {
|
||||
isEmpty = () => !some(this.widgets);
|
||||
|
||||
checkHeightChanges = () => {
|
||||
Object
|
||||
.keys(this.widgets)
|
||||
Object.keys(this.widgets)
|
||||
.filter(this.exists) // reject already removed items
|
||||
.forEach((id) => {
|
||||
.forEach(id => {
|
||||
const [getHeight, prevHeight] = this.widgets[id];
|
||||
const height = getHeight();
|
||||
if (height && height !== prevHeight) {
|
||||
@@ -114,5 +115,5 @@ export default class AutoHeightController {
|
||||
destroy = () => {
|
||||
this.stop();
|
||||
this.widgets = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { trim } from 'lodash';
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Input from 'antd/lib/input';
|
||||
import DynamicComponent from '@/components/DynamicComponent';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { $location, $http } from '@/services/ng';
|
||||
import recordEvent from '@/services/recordEvent';
|
||||
import { policy } from '@/services/policy';
|
||||
import { trim } from "lodash";
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Input from "antd/lib/input";
|
||||
import DynamicComponent from "@/components/DynamicComponent";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import { $location, $http } from "@/services/ng";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import { policy } from "@/services/policy";
|
||||
|
||||
function CreateDashboardDialog({ dialog }) {
|
||||
const [name, setName] = useState('');
|
||||
const [name, setName] = useState("");
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const [saveInProgress, setSaveInProgress] = useState(false);
|
||||
const inputRef = useRef();
|
||||
@@ -29,19 +29,21 @@ function CreateDashboardDialog({ dialog }) {
|
||||
function handleNameChange(event) {
|
||||
const value = trim(event.target.value);
|
||||
setName(value);
|
||||
setIsValid(value !== '');
|
||||
setIsValid(value !== "");
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (name !== '') {
|
||||
if (name !== "") {
|
||||
setSaveInProgress(true);
|
||||
|
||||
$http.post('api/dashboards', { name })
|
||||
.then(({ data }) => {
|
||||
$http.post("api/dashboards", { name }).then(({ data }) => {
|
||||
dialog.close();
|
||||
$location.path(`/dashboard/${data.slug}`).search('edit').replace();
|
||||
$location
|
||||
.path(`/dashboard/${data.slug}`)
|
||||
.search("edit")
|
||||
.replace();
|
||||
});
|
||||
recordEvent('create', 'dashboard');
|
||||
recordEvent("create", "dashboard");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ function CreateDashboardDialog({ dialog }) {
|
||||
okButtonProps={{
|
||||
disabled: !isValid || saveInProgress,
|
||||
loading: saveInProgress,
|
||||
'data-test': 'DashboardSaveButton',
|
||||
"data-test": "DashboardSaveButton",
|
||||
}}
|
||||
cancelButtonProps={{
|
||||
disabled: saveInProgress,
|
||||
@@ -64,9 +66,8 @@ function CreateDashboardDialog({ dialog }) {
|
||||
closable={!saveInProgress}
|
||||
maskClosable={!saveInProgress}
|
||||
wrapProps={{
|
||||
'data-test': 'CreateDashboardDialog',
|
||||
}}
|
||||
>
|
||||
"data-test": "CreateDashboardDialog",
|
||||
}}>
|
||||
<DynamicComponent name="CreateDashboardDialogExtra" disabled={!isCreateDashboardEnabled}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { chain, cloneDeep, find } from 'lodash';
|
||||
import { react2angular } from 'react2angular';
|
||||
import cx from 'classnames';
|
||||
import { Responsive, WidthProvider } from 'react-grid-layout';
|
||||
import { VisualizationWidget, TextboxWidget, RestrictedWidget } from '@/components/dashboards/dashboard-widget';
|
||||
import { FiltersType } from '@/components/Filters';
|
||||
import cfg from '@/config/dashboard-grid-options';
|
||||
import AutoHeightController from './AutoHeightController';
|
||||
import { WidgetTypeEnum } from '@/services/widget';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { chain, cloneDeep, find } from "lodash";
|
||||
import { react2angular } from "react2angular";
|
||||
import cx from "classnames";
|
||||
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||
import { VisualizationWidget, TextboxWidget, RestrictedWidget } from "@/components/dashboards/dashboard-widget";
|
||||
import { FiltersType } from "@/components/Filters";
|
||||
import cfg from "@/config/dashboard-grid-options";
|
||||
import AutoHeightController from "./AutoHeightController";
|
||||
import { WidgetTypeEnum } from "@/services/widget";
|
||||
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import './dashboard-grid.less';
|
||||
import "react-grid-layout/css/styles.css";
|
||||
import "./dashboard-grid.less";
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
|
||||
@@ -31,8 +31,8 @@ const WidgetType = PropTypes.shape({
|
||||
}).isRequired,
|
||||
});
|
||||
|
||||
const SINGLE = 'single-column';
|
||||
const MULTI = 'multi-column';
|
||||
const SINGLE = "single-column";
|
||||
const MULTI = "multi-column";
|
||||
|
||||
class DashboardGrid extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -61,7 +61,10 @@ class DashboardGrid extends React.Component {
|
||||
};
|
||||
|
||||
static normalizeFrom(widget) {
|
||||
const { id, options: { position: pos } } = widget;
|
||||
const {
|
||||
id,
|
||||
options: { position: pos },
|
||||
} = widget;
|
||||
|
||||
return {
|
||||
i: id.toString(),
|
||||
@@ -130,14 +133,14 @@ class DashboardGrid extends React.Component {
|
||||
}
|
||||
|
||||
const normalized = chain(layouts[MULTI])
|
||||
.keyBy('i')
|
||||
.keyBy("i")
|
||||
.mapValues(this.normalizeTo)
|
||||
.value();
|
||||
|
||||
this.props.onLayoutChange(normalized);
|
||||
};
|
||||
|
||||
onBreakpointChange = (mode) => {
|
||||
onBreakpointChange = mode => {
|
||||
this.mode = mode;
|
||||
this.props.onBreakpointChange(mode === SINGLE);
|
||||
};
|
||||
@@ -174,14 +177,22 @@ class DashboardGrid extends React.Component {
|
||||
});
|
||||
|
||||
render() {
|
||||
const className = cx('dashboard-wrapper', this.props.isEditing ? 'editing-mode' : 'preview-mode');
|
||||
const { onLoadWidget, onRefreshWidget, onRemoveWidget,
|
||||
onParameterMappingsChange, filters, dashboard, isPublic, widgets } = this.props;
|
||||
const className = cx("dashboard-wrapper", this.props.isEditing ? "editing-mode" : "preview-mode");
|
||||
const {
|
||||
onLoadWidget,
|
||||
onRefreshWidget,
|
||||
onRemoveWidget,
|
||||
onParameterMappingsChange,
|
||||
filters,
|
||||
dashboard,
|
||||
isPublic,
|
||||
widgets,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ResponsiveGridLayout
|
||||
className={cx('layout', { 'disable-animations': this.state.disableAnimations })}
|
||||
className={cx("layout", { "disable-animations": this.state.disableAnimations })}
|
||||
cols={{ [MULTI]: cfg.columns, [SINGLE]: 1 }}
|
||||
rowHeight={cfg.rowHeight - cfg.margins}
|
||||
margin={[cfg.margins, cfg.margins]}
|
||||
@@ -192,9 +203,8 @@ class DashboardGrid extends React.Component {
|
||||
layouts={this.state.layouts}
|
||||
onLayoutChange={this.onLayoutChange}
|
||||
onBreakpointChange={this.onBreakpointChange}
|
||||
breakpoints={{ [MULTI]: cfg.mobileBreakPoint, [SINGLE]: 0 }}
|
||||
>
|
||||
{widgets.map((widget) => {
|
||||
breakpoints={{ [MULTI]: cfg.mobileBreakPoint, [SINGLE]: 0 }}>
|
||||
{widgets.map(widget => {
|
||||
const widgetProps = {
|
||||
widget,
|
||||
filters,
|
||||
@@ -209,8 +219,9 @@ class DashboardGrid extends React.Component {
|
||||
data-grid={DashboardGrid.normalizeFrom(widget)}
|
||||
data-widgetid={widget.id}
|
||||
data-test={`WidgetId${widget.id}`}
|
||||
className={cx('dashboard-widget-wrapper', { 'widget-auto-height-enabled': this.autoHeightCtrl.exists(widget.id) })}
|
||||
>
|
||||
className={cx("dashboard-widget-wrapper", {
|
||||
"widget-auto-height-enabled": this.autoHeightCtrl.exists(widget.id),
|
||||
})}>
|
||||
{type === WidgetTypeEnum.VISUALIZATION && (
|
||||
<VisualizationWidget
|
||||
{...widgetProps}
|
||||
@@ -232,7 +243,7 @@ class DashboardGrid extends React.Component {
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('dashboardGrid', react2angular(DashboardGrid));
|
||||
ngModule.component("dashboardGrid", react2angular(DashboardGrid));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { isMatch, map, find, sortBy } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { isMatch, map, find, sortBy } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Modal from "antd/lib/modal";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import {
|
||||
MappingType,
|
||||
ParameterMappingListInput,
|
||||
parameterMappingsToEditableMappings,
|
||||
editableMappingsToParameterMappings,
|
||||
synchronizeWidgetTitles,
|
||||
} from '@/components/ParameterMappingInput';
|
||||
import notification from '@/services/notification';
|
||||
} from "@/components/ParameterMappingInput";
|
||||
import notification from "@/services/notification";
|
||||
|
||||
export function getParamValuesSnapshot(mappings, dashboardParameters) {
|
||||
return map(
|
||||
sortBy(mappings, m => m.name),
|
||||
(m) => {
|
||||
m => {
|
||||
let param;
|
||||
switch (m.type) {
|
||||
case MappingType.StaticValue:
|
||||
@@ -28,7 +28,7 @@ export function getParamValuesSnapshot(mappings, dashboardParameters) {
|
||||
return [m.name, param ? param.value : null];
|
||||
// no default
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class EditParameterMappingsDialog extends React.Component {
|
||||
dialog: DialogPropType.isRequired,
|
||||
};
|
||||
|
||||
originalParamValuesSnapshot = null
|
||||
originalParamValuesSnapshot = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -47,12 +47,12 @@ class EditParameterMappingsDialog extends React.Component {
|
||||
const parameterMappings = parameterMappingsToEditableMappings(
|
||||
props.widget.options.parameterMappings,
|
||||
props.widget.query.getParametersDefs(),
|
||||
map(this.props.dashboard.getParametersDefs(), p => p.name),
|
||||
map(this.props.dashboard.getParametersDefs(), p => p.name)
|
||||
);
|
||||
|
||||
this.originalParamValuesSnapshot = getParamValuesSnapshot(
|
||||
parameterMappings,
|
||||
this.props.dashboard.getParametersDefs(),
|
||||
this.props.dashboard.getParametersDefs()
|
||||
);
|
||||
|
||||
this.state = {
|
||||
@@ -71,7 +71,7 @@ class EditParameterMappingsDialog extends React.Component {
|
||||
|
||||
const valuesChanged = !isMatch(
|
||||
this.originalParamValuesSnapshot,
|
||||
getParamValuesSnapshot(this.state.parameterMappings, this.props.dashboard.getParametersDefs()),
|
||||
getParamValuesSnapshot(this.state.parameterMappings, this.props.dashboard.getParametersDefs())
|
||||
);
|
||||
|
||||
const widgetsToSave = [
|
||||
@@ -84,7 +84,7 @@ class EditParameterMappingsDialog extends React.Component {
|
||||
this.props.dialog.close(valuesChanged);
|
||||
})
|
||||
.catch(() => {
|
||||
notification.error('Widget cannot be updated');
|
||||
notification.error("Widget cannot be updated");
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ saveInProgress: false });
|
||||
@@ -103,9 +103,8 @@ class EditParameterMappingsDialog extends React.Component {
|
||||
title="Parameters"
|
||||
onOk={() => this.saveWidget()}
|
||||
okButtonProps={{ loading: this.state.saveInProgress }}
|
||||
width={700}
|
||||
>
|
||||
{(this.state.parameterMappings.length > 0) && (
|
||||
width={700}>
|
||||
{this.state.parameterMappings.length > 0 && (
|
||||
<ParameterMappingListInput
|
||||
mappings={this.state.parameterMappings}
|
||||
existingParams={this.props.dashboard.getParametersDefs()}
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'antd/lib/button';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { VisualizationName } from '@/visualizations/VisualizationName';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import Modal from "antd/lib/modal";
|
||||
import { VisualizationRenderer } from "@/visualizations/VisualizationRenderer";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import { VisualizationName } from "@/visualizations/VisualizationName";
|
||||
|
||||
function ExpandedWidgetDialog({ dialog, widget }) {
|
||||
return (
|
||||
<Modal
|
||||
{...dialog.props}
|
||||
title={(
|
||||
title={
|
||||
<>
|
||||
<VisualizationName visualization={widget.visualization} />{' '}
|
||||
<span>{widget.getQuery().name}</span>
|
||||
<VisualizationName visualization={widget.visualization} /> <span>{widget.getQuery().name}</span>
|
||||
</>
|
||||
)}
|
||||
}
|
||||
width="95%"
|
||||
footer={(<Button onClick={dialog.dismiss}>Close</Button>)}
|
||||
>
|
||||
footer={<Button onClick={dialog.dismiss}>Close</Button>}>
|
||||
<VisualizationRenderer
|
||||
visualization={widget.visualization}
|
||||
queryResult={widget.getQueryResult()}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { markdown } from 'markdown';
|
||||
import { debounce } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Input from 'antd/lib/input';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Divider from 'antd/lib/divider';
|
||||
import HtmlContent from '@/components/HtmlContent';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import notification from '@/services/notification';
|
||||
import { markdown } from "markdown";
|
||||
import { debounce } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Input from "antd/lib/input";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import Divider from "antd/lib/divider";
|
||||
import HtmlContent from "@/components/HtmlContent";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import notification from "@/services/notification";
|
||||
|
||||
import './TextboxDialog.less';
|
||||
import "./TextboxDialog.less";
|
||||
|
||||
class TextboxDialog extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -20,7 +20,7 @@ class TextboxDialog extends React.Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
text: '',
|
||||
text: "",
|
||||
};
|
||||
|
||||
updatePreview = debounce(() => {
|
||||
@@ -40,7 +40,7 @@ class TextboxDialog extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
onTextChanged = (event) => {
|
||||
onTextChanged = event => {
|
||||
this.setState({ text: event.target.value });
|
||||
this.updatePreview();
|
||||
};
|
||||
@@ -48,12 +48,13 @@ class TextboxDialog extends React.Component {
|
||||
saveWidget() {
|
||||
this.setState({ saveInProgress: true });
|
||||
|
||||
this.props.onConfirm(this.state.text)
|
||||
this.props
|
||||
.onConfirm(this.state.text)
|
||||
.then(() => {
|
||||
this.props.dialog.close();
|
||||
})
|
||||
.catch(() => {
|
||||
notification.error('Widget could not be added');
|
||||
notification.error("Widget could not be added");
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ saveInProgress: false });
|
||||
@@ -67,16 +68,15 @@ class TextboxDialog extends React.Component {
|
||||
return (
|
||||
<Modal
|
||||
{...dialog.props}
|
||||
title={isNew ? 'Add Textbox' : 'Edit Textbox'}
|
||||
title={isNew ? "Add Textbox" : "Edit Textbox"}
|
||||
onOk={() => this.saveWidget()}
|
||||
okButtonProps={{
|
||||
loading: this.state.saveInProgress,
|
||||
disabled: !this.state.text,
|
||||
}}
|
||||
okText={isNew ? 'Add to Dashboard' : 'Save'}
|
||||
okText={isNew ? "Add to Dashboard" : "Save"}
|
||||
width={500}
|
||||
wrapProps={{ 'data-test': 'TextboxDialog' }}
|
||||
>
|
||||
wrapProps={{ "data-test": "TextboxDialog" }}>
|
||||
<div className="textbox-dialog">
|
||||
<Input.TextArea
|
||||
className="resize-vertical"
|
||||
@@ -87,14 +87,11 @@ class TextboxDialog extends React.Component {
|
||||
placeholder="This is where you write some text"
|
||||
/>
|
||||
<small>
|
||||
Supports basic{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://www.markdownguide.org/cheat-sheet/#basic-syntax"
|
||||
>
|
||||
Supports basic{" "}
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://www.markdownguide.org/cheat-sheet/#basic-syntax">
|
||||
<Tooltip title="Markdown guide opens in new window">Markdown</Tooltip>
|
||||
</a>.
|
||||
</a>
|
||||
.
|
||||
</small>
|
||||
{this.state.text && (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
import Widget from './Widget';
|
||||
import React from "react";
|
||||
import Widget from "./Widget";
|
||||
|
||||
function RestrictedWidget(props) {
|
||||
return (
|
||||
<Widget {...props} className="d-flex justify-content-center align-items-center widget-restricted">
|
||||
<div className="t-body scrollbox">
|
||||
<div className="text-center">
|
||||
<h1><span className="zmdi zmdi-lock" /></h1>
|
||||
<p className="text-muted">
|
||||
This widget requires access to a data source you don't have access to.
|
||||
</p>
|
||||
<h1>
|
||||
<span className="zmdi zmdi-lock" />
|
||||
</h1>
|
||||
<p className="text-muted">This widget requires access to a data source you don't have access to.</p>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { markdown } from 'markdown';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import HtmlContent from '@/components/HtmlContent';
|
||||
import TextboxDialog from '@/components/dashboards/TextboxDialog';
|
||||
import Widget from './Widget';
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { markdown } from "markdown";
|
||||
import Menu from "antd/lib/menu";
|
||||
import HtmlContent from "@/components/HtmlContent";
|
||||
import TextboxDialog from "@/components/dashboards/TextboxDialog";
|
||||
import Widget from "./Widget";
|
||||
|
||||
function TextboxWidget(props) {
|
||||
const { widget, canEdit } = props;
|
||||
@@ -13,7 +13,7 @@ function TextboxWidget(props) {
|
||||
const editTextBox = () => {
|
||||
TextboxDialog.showModal({
|
||||
text: widget.text,
|
||||
onConfirm: (newText) => {
|
||||
onConfirm: newText => {
|
||||
widget.text = newText;
|
||||
setText(newText);
|
||||
return widget.save();
|
||||
@@ -22,7 +22,9 @@ function TextboxWidget(props) {
|
||||
};
|
||||
|
||||
const TextboxMenuOptions = [
|
||||
<Menu.Item key="edit" onClick={editTextBox}>Edit</Menu.Item>,
|
||||
<Menu.Item key="edit" onClick={editTextBox}>
|
||||
Edit
|
||||
</Menu.Item>,
|
||||
];
|
||||
|
||||
if (!widget.width) {
|
||||
@@ -31,9 +33,7 @@ function TextboxWidget(props) {
|
||||
|
||||
return (
|
||||
<Widget {...props} menuOptions={canEdit ? TextboxMenuOptions : null} className="widget-text">
|
||||
<HtmlContent className="body-row-auto scrollbox t-body p-15 markdown">
|
||||
{markdown.toHTML(text || '')}
|
||||
</HtmlContent>
|
||||
<HtmlContent className="body-row-auto scrollbox t-body p-15 markdown">{markdown.toHTML(text || "")}</HtmlContent>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compact, isEmpty, invoke } from 'lodash';
|
||||
import { markdown } from 'markdown';
|
||||
import cx from 'classnames';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import { currentUser } from '@/services/auth';
|
||||
import recordEvent from '@/services/recordEvent';
|
||||
import { formatDateTime } from '@/filters/datetime';
|
||||
import HtmlContent from '@/components/HtmlContent';
|
||||
import { Parameters } from '@/components/Parameters';
|
||||
import { TimeAgo } from '@/components/TimeAgo';
|
||||
import { Timer } from '@/components/Timer';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import QueryLink from '@/components/QueryLink';
|
||||
import { FiltersType } from '@/components/Filters';
|
||||
import ExpandedWidgetDialog from '@/components/dashboards/ExpandedWidgetDialog';
|
||||
import EditParameterMappingsDialog from '@/components/dashboards/EditParameterMappingsDialog';
|
||||
import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer';
|
||||
import Widget from './Widget';
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { compact, isEmpty, invoke } from "lodash";
|
||||
import { markdown } from "markdown";
|
||||
import cx from "classnames";
|
||||
import Menu from "antd/lib/menu";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import { formatDateTime } from "@/filters/datetime";
|
||||
import HtmlContent from "@/components/HtmlContent";
|
||||
import { Parameters } from "@/components/Parameters";
|
||||
import { TimeAgo } from "@/components/TimeAgo";
|
||||
import { Timer } from "@/components/Timer";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
import QueryLink from "@/components/QueryLink";
|
||||
import { FiltersType } from "@/components/Filters";
|
||||
import ExpandedWidgetDialog from "@/components/dashboards/ExpandedWidgetDialog";
|
||||
import EditParameterMappingsDialog from "@/components/dashboards/EditParameterMappingsDialog";
|
||||
import { VisualizationRenderer } from "@/visualizations/VisualizationRenderer";
|
||||
import Widget from "./Widget";
|
||||
|
||||
function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParametersEdit }) {
|
||||
const canViewQuery = currentUser.hasPermission('view_query');
|
||||
const canEditParameters = canEditDashboard && !isEmpty(invoke(widget, 'query.getParametersDefs'));
|
||||
const canViewQuery = currentUser.hasPermission("view_query");
|
||||
const canEditParameters = canEditDashboard && !isEmpty(invoke(widget, "query.getParametersDefs"));
|
||||
const widgetQueryResult = widget.getQueryResult();
|
||||
const isQueryResultEmpty = !widgetQueryResult || !widgetQueryResult.isEmpty || widgetQueryResult.isEmpty();
|
||||
|
||||
@@ -30,32 +30,33 @@ function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParameters
|
||||
return compact([
|
||||
<Menu.Item key="download_csv" disabled={isQueryResultEmpty}>
|
||||
{!isQueryResultEmpty ? (
|
||||
<a href={downloadLink('csv')} download={downloadName('csv')} target="_self">
|
||||
<a href={downloadLink("csv")} download={downloadName("csv")} target="_self">
|
||||
Download as CSV File
|
||||
</a>
|
||||
) : 'Download as CSV File'}
|
||||
) : (
|
||||
"Download as CSV File"
|
||||
)}
|
||||
</Menu.Item>,
|
||||
<Menu.Item key="download_excel" disabled={isQueryResultEmpty}>
|
||||
{!isQueryResultEmpty ? (
|
||||
<a href={downloadLink('xlsx')} download={downloadName('xlsx')} target="_self">
|
||||
<a href={downloadLink("xlsx")} download={downloadName("xlsx")} target="_self">
|
||||
Download as Excel File
|
||||
</a>
|
||||
) : 'Download as Excel File'}
|
||||
) : (
|
||||
"Download as Excel File"
|
||||
)}
|
||||
</Menu.Item>,
|
||||
((canViewQuery || canEditParameters) && <Menu.Divider key="divider" />),
|
||||
(canViewQuery || canEditParameters) && <Menu.Divider key="divider" />,
|
||||
canViewQuery && (
|
||||
<Menu.Item key="view_query">
|
||||
<a href={widget.getQuery().getUrl(true, widget.visualization.id)}>View Query</a>
|
||||
</Menu.Item>
|
||||
),
|
||||
(canEditParameters && (
|
||||
<Menu.Item
|
||||
key="edit_parameters"
|
||||
onClick={onParametersEdit}
|
||||
>
|
||||
canEditParameters && (
|
||||
<Menu.Item key="edit_parameters" onClick={onParametersEdit}>
|
||||
Edit Parameters
|
||||
</Menu.Item>
|
||||
)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -74,7 +75,7 @@ RefreshIndicator.propTypes = { refreshStartedAt: Moment };
|
||||
RefreshIndicator.defaultProps = { refreshStartedAt: null };
|
||||
|
||||
function VisualizationWidgetHeader({ widget, refreshStartedAt, parameters, onParametersUpdate }) {
|
||||
const canViewQuery = currentUser.hasPermission('view_query');
|
||||
const canViewQuery = currentUser.hasPermission("view_query");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -85,7 +86,7 @@ function VisualizationWidgetHeader({ widget, refreshStartedAt, parameters, onPar
|
||||
<QueryLink query={widget.getQuery()} visualization={widget.visualization} readOnly={!canViewQuery} />
|
||||
</p>
|
||||
<HtmlContent className="text-muted markdown query--description">
|
||||
{markdown.toHTML(widget.getQuery().description || '')}
|
||||
{markdown.toHTML(widget.getQuery().description || "")}
|
||||
</HtmlContent>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,10 +114,10 @@ VisualizationWidgetHeader.defaultProps = {
|
||||
|
||||
function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) {
|
||||
const widgetQueryResult = widget.getQueryResult();
|
||||
const updatedAt = invoke(widgetQueryResult, 'getUpdatedAt');
|
||||
const updatedAt = invoke(widgetQueryResult, "getUpdatedAt");
|
||||
const [refreshClickButtonId, setRefreshClickButtonId] = useState();
|
||||
|
||||
const refreshWidget = (buttonId) => {
|
||||
const refreshWidget = buttonId => {
|
||||
if (!refreshClickButtonId) {
|
||||
setRefreshClickButtonId(buttonId);
|
||||
onRefresh().finally(() => setRefreshClickButtonId(null));
|
||||
@@ -126,22 +127,21 @@ function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) {
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
{(!isPublic && !!widgetQueryResult) && (
|
||||
{!isPublic && !!widgetQueryResult && (
|
||||
<a
|
||||
className="refresh-button hidden-print btn btn-sm btn-default btn-transparent"
|
||||
onClick={() => refreshWidget(1)}
|
||||
data-test="RefreshButton"
|
||||
>
|
||||
<i className={cx('zmdi zmdi-refresh', { 'zmdi-hc-spin': refreshClickButtonId === 1 })} />{' '}
|
||||
data-test="RefreshButton">
|
||||
<i className={cx("zmdi zmdi-refresh", { "zmdi-hc-spin": refreshClickButtonId === 1 })} />{" "}
|
||||
<TimeAgo date={updatedAt} />
|
||||
</a>
|
||||
)}
|
||||
<span className="visible-print">
|
||||
<i className="zmdi zmdi-time-restore" />{' '}{formatDateTime(updatedAt)}
|
||||
<i className="zmdi zmdi-time-restore" /> {formatDateTime(updatedAt)}
|
||||
</span>
|
||||
{isPublic && (
|
||||
<span className="small hidden-print">
|
||||
<i className="zmdi zmdi-time-restore" />{' '}<TimeAgo date={updatedAt} />
|
||||
<i className="zmdi zmdi-time-restore" /> <TimeAgo date={updatedAt} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@@ -149,15 +149,11 @@ function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) {
|
||||
{!isPublic && (
|
||||
<a
|
||||
className="btn btn-sm btn-default hidden-print btn-transparent btn__refresh"
|
||||
onClick={() => refreshWidget(2)}
|
||||
>
|
||||
<i className={cx('zmdi zmdi-refresh', { 'zmdi-hc-spin': refreshClickButtonId === 2 })} />
|
||||
onClick={() => refreshWidget(2)}>
|
||||
<i className={cx("zmdi zmdi-refresh", { "zmdi-hc-spin": refreshClickButtonId === 2 })} />
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
className="btn btn-sm btn-default hidden-print btn-transparent btn__refresh"
|
||||
onClick={onExpand}
|
||||
>
|
||||
<a className="btn btn-sm btn-default hidden-print btn-transparent btn__refresh" onClick={onExpand}>
|
||||
<i className="zmdi zmdi-fullscreen" />
|
||||
</a>
|
||||
</span>
|
||||
@@ -204,8 +200,8 @@ class VisualizationWidget extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { widget, onLoad } = this.props;
|
||||
recordEvent('view', 'query', widget.visualization.query.id, { dashboard: true });
|
||||
recordEvent('view', 'visualization', widget.visualization.id, { dashboard: true });
|
||||
recordEvent("view", "query", widget.visualization.query.id, { dashboard: true });
|
||||
recordEvent("view", "visualization", widget.visualization.id, { dashboard: true });
|
||||
onLoad();
|
||||
}
|
||||
|
||||
@@ -218,7 +214,7 @@ class VisualizationWidget extends React.Component {
|
||||
EditParameterMappingsDialog.showModal({
|
||||
dashboard,
|
||||
widget,
|
||||
}).result.then((valuesChanged) => {
|
||||
}).result.then(valuesChanged => {
|
||||
// refresh widget if any parameter value has been updated
|
||||
if (valuesChanged) {
|
||||
onRefresh();
|
||||
@@ -233,7 +229,7 @@ class VisualizationWidget extends React.Component {
|
||||
const widgetQueryResult = widget.getQueryResult();
|
||||
const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus();
|
||||
switch (widgetStatus) {
|
||||
case 'failed':
|
||||
case "failed":
|
||||
return (
|
||||
<div className="body-row-auto scrollbox">
|
||||
{widgetQueryResult.getError() && (
|
||||
@@ -243,7 +239,7 @@ class VisualizationWidget extends React.Component {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case 'done':
|
||||
case "done":
|
||||
return (
|
||||
<div className="body-row-auto scrollbox">
|
||||
<VisualizationRenderer
|
||||
@@ -275,27 +271,28 @@ class VisualizationWidget extends React.Component {
|
||||
<Widget
|
||||
{...this.props}
|
||||
className="widget-visualization"
|
||||
menuOptions={visualizationWidgetMenuOptions({ widget,
|
||||
menuOptions={visualizationWidgetMenuOptions({
|
||||
widget,
|
||||
canEditDashboard: canEdit,
|
||||
onParametersEdit: this.editParameterMappings })}
|
||||
header={(
|
||||
onParametersEdit: this.editParameterMappings,
|
||||
})}
|
||||
header={
|
||||
<VisualizationWidgetHeader
|
||||
widget={widget}
|
||||
refreshStartedAt={isRefreshing ? widget.refreshStartedAt : null}
|
||||
parameters={localParameters}
|
||||
onParametersUpdate={onRefresh}
|
||||
/>
|
||||
)}
|
||||
footer={(
|
||||
}
|
||||
footer={
|
||||
<VisualizationWidgetFooter
|
||||
widget={widget}
|
||||
isPublic={isPublic}
|
||||
onRefresh={onRefresh}
|
||||
onExpand={this.expandWidget}
|
||||
/>
|
||||
)}
|
||||
tileProps={{ 'data-refreshing': isRefreshing }}
|
||||
>
|
||||
}
|
||||
tileProps={{ "data-refreshing": isRefreshing }}>
|
||||
{this.renderVisualization()}
|
||||
</Widget>
|
||||
);
|
||||
|
||||
@@ -1,31 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { isEmpty } from 'lodash';
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import recordEvent from '@/services/recordEvent';
|
||||
import { Moment } from '@/components/proptypes';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import { isEmpty } from "lodash";
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Menu from "antd/lib/menu";
|
||||
import recordEvent from "@/services/recordEvent";
|
||||
import { Moment } from "@/components/proptypes";
|
||||
|
||||
import './Widget.less';
|
||||
import "./Widget.less";
|
||||
|
||||
function WidgetDropdownButton({ extraOptions, showDeleteOption, onDelete }) {
|
||||
const WidgetMenu = (
|
||||
<Menu data-test="WidgetDropdownButtonMenu">
|
||||
{extraOptions}
|
||||
{(showDeleteOption && extraOptions) && <Menu.Divider />}
|
||||
{showDeleteOption && extraOptions && <Menu.Divider />}
|
||||
{showDeleteOption && <Menu.Item onClick={onDelete}>Remove from Dashboard</Menu.Item>}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="widget-menu-regular">
|
||||
<Dropdown
|
||||
overlay={WidgetMenu}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
>
|
||||
<Dropdown overlay={WidgetMenu} placement="bottomRight" trigger={["click"]}>
|
||||
<a className="action p-l-15 p-r-15" data-test="WidgetDropdownButton">
|
||||
<i className="zmdi zmdi-more-vert" />
|
||||
</a>
|
||||
@@ -75,7 +71,7 @@ class Widget extends React.Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
className: "",
|
||||
children: null,
|
||||
header: null,
|
||||
footer: null,
|
||||
@@ -89,17 +85,17 @@ class Widget extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { widget } = this.props;
|
||||
recordEvent('view', 'widget', widget.id);
|
||||
recordEvent("view", "widget", widget.id);
|
||||
}
|
||||
|
||||
deleteWidget = () => {
|
||||
const { widget, onDelete } = this.props;
|
||||
|
||||
Modal.confirm({
|
||||
title: 'Delete Widget',
|
||||
content: 'Are you sure you want to remove this widget from the dashboard?',
|
||||
okText: 'Delete',
|
||||
okType: 'danger',
|
||||
title: "Delete Widget",
|
||||
content: "Are you sure you want to remove this widget from the dashboard?",
|
||||
okText: "Delete",
|
||||
okType: "danger",
|
||||
onOk: () => widget.delete().then(onDelete),
|
||||
maskClosable: true,
|
||||
autoFocusButton: null,
|
||||
@@ -107,12 +103,11 @@ class Widget extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, children, header, footer, canEdit, isPublic,
|
||||
menuOptions, tileProps } = this.props;
|
||||
const { className, children, header, footer, canEdit, isPublic, menuOptions, tileProps } = this.props;
|
||||
const showDropdownButton = !isPublic && (canEdit || !isEmpty(menuOptions));
|
||||
return (
|
||||
<div className="widget-wrapper">
|
||||
<div className={cx('tile body-container', className)} {...tileProps}>
|
||||
<div className={cx("tile body-container", className)} {...tileProps}>
|
||||
<div className="widget-actions">
|
||||
{showDropdownButton && (
|
||||
<WidgetDropdownButton
|
||||
@@ -123,15 +118,9 @@ class Widget extends React.Component {
|
||||
)}
|
||||
{canEdit && <WidgetDeleteButton onClick={this.deleteWidget} />}
|
||||
</div>
|
||||
<div className="body-row widget-header">
|
||||
{header}
|
||||
</div>
|
||||
<div className="body-row widget-header">{header}</div>
|
||||
{children}
|
||||
{footer && (
|
||||
<div className="body-row tile__bottom-control">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
{footer && <div className="body-row tile__bottom-control">{footer}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { default as VisualizationWidget } from './VisualizationWidget';
|
||||
export { default as TextboxWidget } from './TextboxWidget';
|
||||
export { default as RestrictedWidget } from './RestrictedWidget';
|
||||
export { default as VisualizationWidget } from "./VisualizationWidget";
|
||||
export { default as TextboxWidget } from "./TextboxWidget";
|
||||
export { default as RestrictedWidget } from "./RestrictedWidget";
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Form from 'antd/lib/form';
|
||||
import Input from 'antd/lib/input';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Button from 'antd/lib/button';
|
||||
import Upload from 'antd/lib/upload';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { includes, isFunction, filter, difference, isEmpty, some, isNumber, isBoolean } from 'lodash';
|
||||
import Select from 'antd/lib/select';
|
||||
import notification from '@/services/notification';
|
||||
import Collapse from '@/components/Collapse';
|
||||
import AceEditorInput from '@/components/AceEditorInput';
|
||||
import { toHuman } from '@/filters';
|
||||
import { Field, Action, AntdForm } from '../proptypes';
|
||||
import helper from './dynamicFormHelper';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import cx from "classnames";
|
||||
import Form from "antd/lib/form";
|
||||
import Input from "antd/lib/input";
|
||||
import InputNumber from "antd/lib/input-number";
|
||||
import Checkbox from "antd/lib/checkbox";
|
||||
import Button from "antd/lib/button";
|
||||
import Upload from "antd/lib/upload";
|
||||
import Icon from "antd/lib/icon";
|
||||
import { includes, isFunction, filter, difference, isEmpty, some, isNumber, isBoolean } from "lodash";
|
||||
import Select from "antd/lib/select";
|
||||
import notification from "@/services/notification";
|
||||
import Collapse from "@/components/Collapse";
|
||||
import AceEditorInput from "@/components/AceEditorInput";
|
||||
import { toHuman } from "@/filters";
|
||||
import { Field, Action, AntdForm } from "../proptypes";
|
||||
import helper from "./dynamicFormHelper";
|
||||
|
||||
import './DynamicForm.less';
|
||||
import "./DynamicForm.less";
|
||||
|
||||
const fieldRules = ({ type, required, minLength }) => {
|
||||
const requiredRule = required;
|
||||
const minLengthRule = minLength && includes(['text', 'email', 'password'], type);
|
||||
const emailTypeRule = type === 'email';
|
||||
const minLengthRule = minLength && includes(["text", "email", "password"], type);
|
||||
const emailTypeRule = type === "email";
|
||||
|
||||
return [
|
||||
requiredRule && { required, message: 'This field is required.' },
|
||||
minLengthRule && { min: minLength, message: 'This field is too short.' },
|
||||
emailTypeRule && { type: 'email', message: 'This field must be a valid email.' },
|
||||
requiredRule && { required, message: "This field is required." },
|
||||
minLengthRule && { min: minLength, message: "This field is too short." },
|
||||
emailTypeRule && { type: "email", message: "This field must be a valid email." },
|
||||
].filter(rule => rule);
|
||||
};
|
||||
|
||||
@@ -49,31 +49,34 @@ class DynamicForm extends React.Component {
|
||||
actions: [],
|
||||
feedbackIcons: false,
|
||||
hideSubmitButton: false,
|
||||
saveText: 'Save',
|
||||
saveText: "Save",
|
||||
onSubmit: () => {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const hasFilledExtraField = some(props.fields, (field) => {
|
||||
const hasFilledExtraField = some(props.fields, field => {
|
||||
const { extra, initialValue } = field;
|
||||
return extra && (!isEmpty(initialValue) || isNumber(initialValue) || (isBoolean(initialValue) && initialValue));
|
||||
});
|
||||
|
||||
const inProgressActions = {};
|
||||
props.actions.forEach(action => inProgressActions[action.name] = false);
|
||||
props.actions.forEach(action => (inProgressActions[action.name] = false));
|
||||
|
||||
this.state = {
|
||||
isSubmitting: false,
|
||||
showExtraFields: hasFilledExtraField,
|
||||
inProgressActions
|
||||
inProgressActions,
|
||||
};
|
||||
|
||||
this.actionCallbacks = this.props.actions.reduce((acc, cur) => ({
|
||||
this.actionCallbacks = this.props.actions.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur.name]: cur.callback,
|
||||
}), null);
|
||||
}),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
setActionInProgress = (actionName, inProgress) => {
|
||||
@@ -85,29 +88,29 @@ class DynamicForm extends React.Component {
|
||||
}));
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
handleSubmit = e => {
|
||||
this.setState({ isSubmitting: true });
|
||||
e.preventDefault();
|
||||
this.props.form.validateFieldsAndScroll((err, values) => {
|
||||
if (!err) {
|
||||
this.props.onSubmit(
|
||||
values,
|
||||
(msg) => {
|
||||
msg => {
|
||||
const { setFieldsValue, getFieldsValue } = this.props.form;
|
||||
this.setState({ isSubmitting: false });
|
||||
setFieldsValue(getFieldsValue()); // reset form touched state
|
||||
notification.success(msg);
|
||||
},
|
||||
(msg) => {
|
||||
msg => {
|
||||
this.setState({ isSubmitting: false });
|
||||
notification.error(msg);
|
||||
},
|
||||
}
|
||||
);
|
||||
} else this.setState({ isSubmitting: false });
|
||||
});
|
||||
};
|
||||
|
||||
handleAction = (e) => {
|
||||
handleAction = e => {
|
||||
const actionName = e.target.dataset.action;
|
||||
|
||||
this.setActionInProgress(actionName, true);
|
||||
@@ -118,7 +121,7 @@ class DynamicForm extends React.Component {
|
||||
|
||||
base64File = (fieldName, e) => {
|
||||
if (e && e.fileList[0]) {
|
||||
helper.getBase64(e.file).then((value) => {
|
||||
helper.getBase64(e.file).then(value => {
|
||||
this.props.form.setFieldsValue({ [fieldName]: value });
|
||||
});
|
||||
}
|
||||
@@ -138,7 +141,9 @@ class DynamicForm extends React.Component {
|
||||
|
||||
const upload = (
|
||||
<Upload {...props} beforeUpload={() => false}>
|
||||
<Button disabled={disabled}><Icon type="upload" /> Click to upload</Button>
|
||||
<Button disabled={disabled}>
|
||||
<Icon type="upload" /> Click to upload
|
||||
</Button>
|
||||
</Upload>
|
||||
);
|
||||
|
||||
@@ -155,24 +160,23 @@ class DynamicForm extends React.Component {
|
||||
initialValue,
|
||||
};
|
||||
|
||||
return getFieldDecorator(name, decoratorOptions)(
|
||||
return getFieldDecorator(
|
||||
name,
|
||||
decoratorOptions
|
||||
)(
|
||||
<Select
|
||||
{...props}
|
||||
optionFilterProp="children"
|
||||
loading={loading || false}
|
||||
mode={mode}
|
||||
getPopupContainer={trigger => trigger.parentNode}
|
||||
>
|
||||
{options && options.map(option => (
|
||||
<Option
|
||||
key={`${option.value}`}
|
||||
value={option.value}
|
||||
disabled={readOnly}
|
||||
>
|
||||
getPopupContainer={trigger => trigger.parentNode}>
|
||||
{options &&
|
||||
options.map(option => (
|
||||
<Option key={`${option.value}`} value={option.value} disabled={readOnly}>
|
||||
{option.name || option.value}
|
||||
</Option>
|
||||
))}
|
||||
</Select>,
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,50 +187,50 @@ class DynamicForm extends React.Component {
|
||||
|
||||
const options = {
|
||||
rules: fieldRules(field),
|
||||
valuePropName: type === 'checkbox' ? 'checked' : 'value',
|
||||
valuePropName: type === "checkbox" ? "checked" : "value",
|
||||
initialValue,
|
||||
};
|
||||
|
||||
if (type === 'checkbox') {
|
||||
if (type === "checkbox") {
|
||||
return getFieldDecorator(name, options)(<Checkbox {...props}>{fieldLabel}</Checkbox>);
|
||||
} else if (type === 'file') {
|
||||
} else if (type === "file") {
|
||||
return this.renderUpload(field, props);
|
||||
} else if (type === 'select') {
|
||||
} else if (type === "select") {
|
||||
return this.renderSelect(field, props);
|
||||
} else if (type === 'content') {
|
||||
} else if (type === "content") {
|
||||
return field.content;
|
||||
} else if (type === 'number') {
|
||||
} else if (type === "number") {
|
||||
return getFieldDecorator(name, options)(<InputNumber {...props} />);
|
||||
} else if (type === 'textarea') {
|
||||
} else if (type === "textarea") {
|
||||
return getFieldDecorator(name, options)(<Input.TextArea {...props} />);
|
||||
} else if (type === 'ace') {
|
||||
} else if (type === "ace") {
|
||||
return getFieldDecorator(name, options)(<AceEditorInput {...props} />);
|
||||
}
|
||||
return getFieldDecorator(name, options)(<Input {...props} />);
|
||||
}
|
||||
|
||||
renderFields(fields) {
|
||||
return fields.map((field) => {
|
||||
return fields.map(field => {
|
||||
const FormItem = Form.Item;
|
||||
const { name, title, type, readOnly, autoFocus, contentAfter } = field;
|
||||
const fieldLabel = title || toHuman(name);
|
||||
const { feedbackIcons, form } = this.props;
|
||||
|
||||
const formItemProps = {
|
||||
className: 'm-b-10',
|
||||
hasFeedback: type !== 'checkbox' && type !== 'file' && feedbackIcons,
|
||||
label: type === 'checkbox' ? '' : fieldLabel,
|
||||
className: "m-b-10",
|
||||
hasFeedback: type !== "checkbox" && type !== "file" && feedbackIcons,
|
||||
label: type === "checkbox" ? "" : fieldLabel,
|
||||
};
|
||||
|
||||
const fieldProps = {
|
||||
...field.props,
|
||||
className: 'w-100',
|
||||
className: "w-100",
|
||||
name,
|
||||
type,
|
||||
readOnly,
|
||||
autoFocus,
|
||||
placeholder: field.placeholder,
|
||||
'data-test': fieldLabel,
|
||||
"data-test": fieldLabel,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -239,29 +243,33 @@ class DynamicForm extends React.Component {
|
||||
}
|
||||
|
||||
renderActions() {
|
||||
return this.props.actions.map((action) => {
|
||||
return this.props.actions.map(action => {
|
||||
const inProgress = this.state.inProgressActions[action.name];
|
||||
const { isFieldsTouched } = this.props.form;
|
||||
|
||||
const actionProps = {
|
||||
key: action.name,
|
||||
htmlType: 'button',
|
||||
className: action.pullRight ? 'pull-right m-t-10' : 'm-t-10',
|
||||
htmlType: "button",
|
||||
className: action.pullRight ? "pull-right m-t-10" : "m-t-10",
|
||||
type: action.type,
|
||||
disabled: (isFieldsTouched() && action.disableWhenDirty),
|
||||
disabled: isFieldsTouched() && action.disableWhenDirty,
|
||||
loading: inProgress,
|
||||
onClick: this.handleAction,
|
||||
};
|
||||
|
||||
return (<Button {...actionProps} data-action={action.name}>{action.name}</Button>);
|
||||
return (
|
||||
<Button {...actionProps} data-action={action.name}>
|
||||
{action.name}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const submitProps = {
|
||||
type: 'primary',
|
||||
htmlType: 'submit',
|
||||
className: 'w-100 m-t-20',
|
||||
type: "primary",
|
||||
htmlType: "submit",
|
||||
className: "w-100 m-t-20",
|
||||
disabled: this.state.isSubmitting,
|
||||
loading: this.state.isSubmitting,
|
||||
};
|
||||
@@ -280,10 +288,9 @@ class DynamicForm extends React.Component {
|
||||
type="dashed"
|
||||
block
|
||||
className="extra-options-button"
|
||||
onClick={() => this.setState({ showExtraFields: !showExtraFields })}
|
||||
>
|
||||
onClick={() => this.setState({ showExtraFields: !showExtraFields })}>
|
||||
Additional Settings
|
||||
<i className={cx('fa m-l-5', { 'fa-caret-up': showExtraFields, 'fa-caret-down': !showExtraFields })} />
|
||||
<i className={cx("fa m-l-5", { "fa-caret-up": showExtraFields, "fa-caret-down": !showExtraFields })} />
|
||||
</Button>
|
||||
<Collapse collapsed={!showExtraFields} className="extra-options-content">
|
||||
{this.renderFields(extraFields)}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { each, includes, isUndefined, isEmpty, map } from 'lodash';
|
||||
import React from "react";
|
||||
import { each, includes, isUndefined, isEmpty, map } from "lodash";
|
||||
|
||||
function orderedInputs(properties, order, targetOptions) {
|
||||
const inputs = new Array(order.length);
|
||||
Object.keys(properties).forEach((key) => {
|
||||
Object.keys(properties).forEach(key => {
|
||||
const position = order.indexOf(key);
|
||||
const input = {
|
||||
name: key,
|
||||
@@ -15,8 +15,8 @@ function orderedInputs(properties, order, targetOptions) {
|
||||
initialValue: targetOptions[key],
|
||||
};
|
||||
|
||||
if (input.type === 'select') {
|
||||
input.placeholder = 'Select an option';
|
||||
if (input.type === "select") {
|
||||
input.placeholder = "Select an option";
|
||||
input.options = properties[key].options;
|
||||
}
|
||||
|
||||
@@ -31,29 +31,29 @@ function orderedInputs(properties, order, targetOptions) {
|
||||
|
||||
function normalizeSchema(configurationSchema) {
|
||||
each(configurationSchema.properties, (prop, name) => {
|
||||
if (name === 'password' || name === 'passwd') {
|
||||
prop.type = 'password';
|
||||
if (name === "password" || name === "passwd") {
|
||||
prop.type = "password";
|
||||
}
|
||||
|
||||
if (name.endsWith('File')) {
|
||||
prop.type = 'file';
|
||||
if (name.endsWith("File")) {
|
||||
prop.type = "file";
|
||||
}
|
||||
|
||||
if (prop.type === 'boolean') {
|
||||
prop.type = 'checkbox';
|
||||
if (prop.type === "boolean") {
|
||||
prop.type = "checkbox";
|
||||
}
|
||||
|
||||
if (prop.type === 'string') {
|
||||
prop.type = 'text';
|
||||
if (prop.type === "string") {
|
||||
prop.type = "text";
|
||||
}
|
||||
|
||||
if (!isEmpty(prop.enum)) {
|
||||
prop.type = 'select';
|
||||
prop.type = "select";
|
||||
prop.options = map(prop.enum, value => ({ value, name: value }));
|
||||
}
|
||||
|
||||
if (!isEmpty(prop.extendedEnum)) {
|
||||
prop.type = 'select';
|
||||
prop.type = "select";
|
||||
prop.options = prop.extendedEnum;
|
||||
}
|
||||
|
||||
@@ -66,14 +66,14 @@ function normalizeSchema(configurationSchema) {
|
||||
|
||||
function setDefaultValueToFields(configurationSchema, options = {}) {
|
||||
const properties = configurationSchema.properties;
|
||||
Object.keys(properties).forEach((key) => {
|
||||
Object.keys(properties).forEach(key => {
|
||||
const property = properties[key];
|
||||
// set default value for checkboxes
|
||||
if (!isUndefined(property.default) && property.type === 'checkbox') {
|
||||
if (!isUndefined(property.default) && property.type === "checkbox") {
|
||||
options[key] = property.default;
|
||||
}
|
||||
// set default or first value when value has predefined options
|
||||
if (property.type === 'select') {
|
||||
if (property.type === "select") {
|
||||
const optionValues = map(property.options, option => option.value);
|
||||
options[key] = includes(optionValues, property.default) ? property.default : optionValues[0];
|
||||
}
|
||||
@@ -91,12 +91,12 @@ function getFields(type = {}, target = { options: {} }) {
|
||||
const isNewTarget = !target.id;
|
||||
const inputs = [
|
||||
{
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
type: 'text',
|
||||
name: "name",
|
||||
title: "Name",
|
||||
type: "text",
|
||||
required: true,
|
||||
initialValue: target.name,
|
||||
contentAfter: React.createElement('hr'),
|
||||
contentAfter: React.createElement("hr"),
|
||||
placeholder: `My ${type.name}`,
|
||||
autoFocus: isNewTarget,
|
||||
},
|
||||
@@ -108,8 +108,8 @@ function getFields(type = {}, target = { options: {} }) {
|
||||
|
||||
function updateTargetWithValues(target, values) {
|
||||
target.name = values.name;
|
||||
Object.keys(values).forEach((key) => {
|
||||
if (key !== 'name') {
|
||||
Object.keys(values).forEach(key => {
|
||||
if (key !== "name") {
|
||||
target.options[key] = values[key];
|
||||
}
|
||||
});
|
||||
@@ -119,7 +119,7 @@ function getBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => resolve(reader.result.substr(reader.result.indexOf(',') + 1));
|
||||
reader.onload = () => resolve(reader.result.substr(reader.result.indexOf(",") + 1));
|
||||
reader.onerror = error => reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import { includes } from 'lodash';
|
||||
import { isDynamicDate, getDynamicDateFromString } from '@/services/parameters/DateParameter';
|
||||
import DateInput from '@/components/DateInput';
|
||||
import DateTimeInput from '@/components/DateTimeInput';
|
||||
import DynamicButton from '@/components/dynamic-parameters/DynamicButton';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import moment from "moment";
|
||||
import { includes } from "lodash";
|
||||
import { isDynamicDate, getDynamicDateFromString } from "@/services/parameters/DateParameter";
|
||||
import DateInput from "@/components/DateInput";
|
||||
import DateTimeInput from "@/components/DateTimeInput";
|
||||
import DynamicButton from "@/components/dynamic-parameters/DynamicButton";
|
||||
|
||||
import './DynamicParameters.less';
|
||||
import "./DynamicParameters.less";
|
||||
|
||||
const DYNAMIC_DATE_OPTIONS = [
|
||||
{ name: 'Today/Now',
|
||||
value: getDynamicDateFromString('d_now'),
|
||||
label: () => getDynamicDateFromString('d_now').value().format('MMM D') },
|
||||
{ name: 'Yesterday',
|
||||
value: getDynamicDateFromString('d_yesterday'),
|
||||
label: () => getDynamicDateFromString('d_yesterday').value().format('MMM D') },
|
||||
{
|
||||
name: "Today/Now",
|
||||
value: getDynamicDateFromString("d_now"),
|
||||
label: () =>
|
||||
getDynamicDateFromString("d_now")
|
||||
.value()
|
||||
.format("MMM D"),
|
||||
},
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: getDynamicDateFromString("d_yesterday"),
|
||||
label: () =>
|
||||
getDynamicDateFromString("d_yesterday")
|
||||
.value()
|
||||
.format("MMM D"),
|
||||
},
|
||||
];
|
||||
|
||||
class DateParameter extends React.Component {
|
||||
@@ -29,8 +39,8 @@ class DateParameter extends React.Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
type: '',
|
||||
className: '',
|
||||
type: "",
|
||||
className: "",
|
||||
value: null,
|
||||
parameter: null,
|
||||
onSelect: () => {},
|
||||
@@ -41,9 +51,9 @@ class DateParameter extends React.Component {
|
||||
this.dateComponentRef = React.createRef();
|
||||
}
|
||||
|
||||
onDynamicValueSelect = (dynamicValue) => {
|
||||
onDynamicValueSelect = dynamicValue => {
|
||||
const { onSelect, parameter } = this.props;
|
||||
if (dynamicValue === 'static') {
|
||||
if (dynamicValue === "static") {
|
||||
const parameterValue = parameter.getExecutionValue();
|
||||
if (parameterValue) {
|
||||
onSelect(moment(parameterValue));
|
||||
@@ -60,14 +70,14 @@ class DateParameter extends React.Component {
|
||||
render() {
|
||||
const { type, value, className, onSelect } = this.props;
|
||||
const hasDynamicValue = isDynamicDate(value);
|
||||
const isDateTime = includes(type, 'datetime');
|
||||
const isDateTime = includes(type, "datetime");
|
||||
|
||||
const additionalAttributes = {};
|
||||
|
||||
let DateComponent = DateInput;
|
||||
if (isDateTime) {
|
||||
DateComponent = DateTimeInput;
|
||||
if (includes(type, 'with-seconds')) {
|
||||
if (includes(type, "with-seconds")) {
|
||||
additionalAttributes.withSeconds = true;
|
||||
}
|
||||
}
|
||||
@@ -85,16 +95,16 @@ class DateParameter extends React.Component {
|
||||
return (
|
||||
<DateComponent
|
||||
ref={this.dateComponentRef}
|
||||
className={classNames('redash-datepicker', { 'dynamic-value': hasDynamicValue }, className)}
|
||||
className={classNames("redash-datepicker", { "dynamic-value": hasDynamicValue }, className)}
|
||||
onSelect={onSelect}
|
||||
suffixIcon={(
|
||||
suffixIcon={
|
||||
<DynamicButton
|
||||
options={DYNAMIC_DATE_OPTIONS}
|
||||
selectedDynamicValue={hasDynamicValue ? value : null}
|
||||
enabled={hasDynamicValue}
|
||||
onSelect={this.onDynamicValueSelect}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
{...additionalAttributes}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,67 +1,138 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import { includes, isArray, isObject } from 'lodash';
|
||||
import { isDynamicDateRange, getDynamicDateRangeFromString } from '@/services/parameters/DateRangeParameter';
|
||||
import DateRangeInput from '@/components/DateRangeInput';
|
||||
import DateTimeRangeInput from '@/components/DateTimeRangeInput';
|
||||
import DynamicButton from '@/components/dynamic-parameters/DynamicButton';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import moment from "moment";
|
||||
import { includes, isArray, isObject } from "lodash";
|
||||
import { isDynamicDateRange, getDynamicDateRangeFromString } from "@/services/parameters/DateRangeParameter";
|
||||
import DateRangeInput from "@/components/DateRangeInput";
|
||||
import DateTimeRangeInput from "@/components/DateTimeRangeInput";
|
||||
import DynamicButton from "@/components/dynamic-parameters/DynamicButton";
|
||||
|
||||
import './DynamicParameters.less';
|
||||
import "./DynamicParameters.less";
|
||||
|
||||
const DYNAMIC_DATE_OPTIONS = [
|
||||
{ name: 'This week',
|
||||
value: getDynamicDateRangeFromString('d_this_week'),
|
||||
label: () => getDynamicDateRangeFromString('d_this_week').value()[0].format('MMM D') + ' - ' +
|
||||
getDynamicDateRangeFromString('d_this_week').value()[1].format('MMM D') },
|
||||
{ name: 'This month',
|
||||
value: getDynamicDateRangeFromString('d_this_month'),
|
||||
label: () => getDynamicDateRangeFromString('d_this_month').value()[0].format('MMMM') },
|
||||
{ name: 'This year',
|
||||
value: getDynamicDateRangeFromString('d_this_year'),
|
||||
label: () => getDynamicDateRangeFromString('d_this_year').value()[0].format('YYYY') },
|
||||
{ name: 'Last week',
|
||||
value: getDynamicDateRangeFromString('d_last_week'),
|
||||
label: () => getDynamicDateRangeFromString('d_last_week').value()[0].format('MMM D') + ' - ' +
|
||||
getDynamicDateRangeFromString('d_last_week').value()[1].format('MMM D') },
|
||||
{ name: 'Last month',
|
||||
value: getDynamicDateRangeFromString('d_last_month'),
|
||||
label: () => getDynamicDateRangeFromString('d_last_month').value()[0].format('MMMM') },
|
||||
{ name: 'Last year',
|
||||
value: getDynamicDateRangeFromString('d_last_year'),
|
||||
label: () => getDynamicDateRangeFromString('d_last_year').value()[0].format('YYYY') },
|
||||
{ name: 'Last 7 days',
|
||||
value: getDynamicDateRangeFromString('d_last_7_days'),
|
||||
label: () => getDynamicDateRangeFromString('d_last_7_days').value()[0].format('MMM D') + ' - Today' },
|
||||
{ name: 'Last 14 days',
|
||||
value: getDynamicDateRangeFromString('d_last_14_days'),
|
||||
label: () => getDynamicDateRangeFromString('d_last_14_days').value()[0].format('MMM D') + ' - Today' },
|
||||
{ name: 'Last 30 days',
|
||||
value: getDynamicDateRangeFromString('d_last_30_days'),
|
||||
label: () => getDynamicDateRangeFromString('d_last_30_days').value()[0].format('MMM D') + ' - Today' },
|
||||
{ name: 'Last 60 days',
|
||||
value: getDynamicDateRangeFromString('d_last_60_days'),
|
||||
label: () => getDynamicDateRangeFromString('d_last_60_days').value()[0].format('MMM D') + ' - Today' },
|
||||
{ name: 'Last 90 days',
|
||||
value: getDynamicDateRangeFromString('d_last_90_days'),
|
||||
label: () => getDynamicDateRangeFromString('d_last_90_days').value()[0].format('MMM D') + ' - Today' },
|
||||
{
|
||||
name: "This week",
|
||||
value: getDynamicDateRangeFromString("d_this_week"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_this_week")
|
||||
.value()[0]
|
||||
.format("MMM D") +
|
||||
" - " +
|
||||
getDynamicDateRangeFromString("d_this_week")
|
||||
.value()[1]
|
||||
.format("MMM D"),
|
||||
},
|
||||
{
|
||||
name: "This month",
|
||||
value: getDynamicDateRangeFromString("d_this_month"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_this_month")
|
||||
.value()[0]
|
||||
.format("MMMM"),
|
||||
},
|
||||
{
|
||||
name: "This year",
|
||||
value: getDynamicDateRangeFromString("d_this_year"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_this_year")
|
||||
.value()[0]
|
||||
.format("YYYY"),
|
||||
},
|
||||
{
|
||||
name: "Last week",
|
||||
value: getDynamicDateRangeFromString("d_last_week"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_week")
|
||||
.value()[0]
|
||||
.format("MMM D") +
|
||||
" - " +
|
||||
getDynamicDateRangeFromString("d_last_week")
|
||||
.value()[1]
|
||||
.format("MMM D"),
|
||||
},
|
||||
{
|
||||
name: "Last month",
|
||||
value: getDynamicDateRangeFromString("d_last_month"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_month")
|
||||
.value()[0]
|
||||
.format("MMMM"),
|
||||
},
|
||||
{
|
||||
name: "Last year",
|
||||
value: getDynamicDateRangeFromString("d_last_year"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_year")
|
||||
.value()[0]
|
||||
.format("YYYY"),
|
||||
},
|
||||
{
|
||||
name: "Last 7 days",
|
||||
value: getDynamicDateRangeFromString("d_last_7_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_7_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
},
|
||||
{
|
||||
name: "Last 14 days",
|
||||
value: getDynamicDateRangeFromString("d_last_14_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_14_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
},
|
||||
{
|
||||
name: "Last 30 days",
|
||||
value: getDynamicDateRangeFromString("d_last_30_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_30_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
},
|
||||
{
|
||||
name: "Last 60 days",
|
||||
value: getDynamicDateRangeFromString("d_last_60_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_60_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
},
|
||||
{
|
||||
name: "Last 90 days",
|
||||
value: getDynamicDateRangeFromString("d_last_90_days"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_last_90_days")
|
||||
.value()[0]
|
||||
.format("MMM D") + " - Today",
|
||||
},
|
||||
];
|
||||
|
||||
const DYNAMIC_DATETIME_OPTIONS = [
|
||||
{ name: 'Today',
|
||||
value: getDynamicDateRangeFromString('d_today'),
|
||||
label: () => getDynamicDateRangeFromString('d_today').value()[0].format('MMM D') },
|
||||
{ name: 'Yesterday',
|
||||
value: getDynamicDateRangeFromString('d_yesterday'),
|
||||
label: () => getDynamicDateRangeFromString('d_yesterday').value()[0].format('MMM D') },
|
||||
{
|
||||
name: "Today",
|
||||
value: getDynamicDateRangeFromString("d_today"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_today")
|
||||
.value()[0]
|
||||
.format("MMM D"),
|
||||
},
|
||||
{
|
||||
name: "Yesterday",
|
||||
value: getDynamicDateRangeFromString("d_yesterday"),
|
||||
label: () =>
|
||||
getDynamicDateRangeFromString("d_yesterday")
|
||||
.value()[0]
|
||||
.format("MMM D"),
|
||||
},
|
||||
...DYNAMIC_DATE_OPTIONS,
|
||||
];
|
||||
|
||||
const widthByType = {
|
||||
'date-range': 294,
|
||||
'datetime-range': 352,
|
||||
'datetime-range-with-seconds': 382,
|
||||
"date-range": 294,
|
||||
"datetime-range": 352,
|
||||
"datetime-range-with-seconds": 382,
|
||||
};
|
||||
|
||||
function isValidDateRangeValue(value) {
|
||||
@@ -78,8 +149,8 @@ class DateRangeParameter extends React.Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
type: '',
|
||||
className: '',
|
||||
type: "",
|
||||
className: "",
|
||||
value: null,
|
||||
parameter: null,
|
||||
onSelect: () => {},
|
||||
@@ -90,9 +161,9 @@ class DateRangeParameter extends React.Component {
|
||||
this.dateRangeComponentRef = React.createRef();
|
||||
}
|
||||
|
||||
onDynamicValueSelect = (dynamicValue) => {
|
||||
onDynamicValueSelect = dynamicValue => {
|
||||
const { onSelect, parameter } = this.props;
|
||||
if (dynamicValue === 'static') {
|
||||
if (dynamicValue === "static") {
|
||||
const parameterValue = parameter.getExecutionValue();
|
||||
if (isObject(parameterValue) && parameterValue.start && parameterValue.end) {
|
||||
onSelect([moment(parameterValue.start), moment(parameterValue.end)]);
|
||||
@@ -108,7 +179,7 @@ class DateRangeParameter extends React.Component {
|
||||
|
||||
render() {
|
||||
const { type, value, onSelect, className } = this.props;
|
||||
const isDateTimeRange = includes(type, 'datetime-range');
|
||||
const isDateTimeRange = includes(type, "datetime-range");
|
||||
const hasDynamicValue = isDynamicDateRange(value);
|
||||
const options = isDateTimeRange ? DYNAMIC_DATETIME_OPTIONS : DYNAMIC_DATE_OPTIONS;
|
||||
|
||||
@@ -117,7 +188,7 @@ class DateRangeParameter extends React.Component {
|
||||
let DateRangeComponent = DateRangeInput;
|
||||
if (isDateTimeRange) {
|
||||
DateRangeComponent = DateTimeRangeInput;
|
||||
if (includes(type, 'with-seconds')) {
|
||||
if (includes(type, "with-seconds")) {
|
||||
additionalAttributes.withSeconds = true;
|
||||
}
|
||||
}
|
||||
@@ -134,17 +205,17 @@ class DateRangeParameter extends React.Component {
|
||||
return (
|
||||
<DateRangeComponent
|
||||
ref={this.dateRangeComponentRef}
|
||||
className={classNames('redash-datepicker date-range-input', { 'dynamic-value': hasDynamicValue }, className)}
|
||||
className={classNames("redash-datepicker date-range-input", { "dynamic-value": hasDynamicValue }, className)}
|
||||
onSelect={onSelect}
|
||||
style={{ width: hasDynamicValue ? 195 : widthByType[type] }}
|
||||
suffixIcon={(
|
||||
suffixIcon={
|
||||
<DynamicButton
|
||||
options={options}
|
||||
selectedDynamicValue={hasDynamicValue ? value : null}
|
||||
enabled={hasDynamicValue}
|
||||
onSelect={this.onDynamicValueSelect}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
{...additionalAttributes}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isFunction, get, findIndex } from 'lodash';
|
||||
import Dropdown from 'antd/lib/dropdown';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Menu from 'antd/lib/menu';
|
||||
import Typography from 'antd/lib/typography';
|
||||
import { DynamicDateType } from '@/services/parameters/DateParameter';
|
||||
import { DynamicDateRangeType } from '@/services/parameters/DateRangeParameter';
|
||||
import React, { useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { isFunction, get, findIndex } from "lodash";
|
||||
import Dropdown from "antd/lib/dropdown";
|
||||
import Icon from "antd/lib/icon";
|
||||
import Menu from "antd/lib/menu";
|
||||
import Typography from "antd/lib/typography";
|
||||
import { DynamicDateType } from "@/services/parameters/DateParameter";
|
||||
import { DynamicDateRangeType } from "@/services/parameters/DateRangeParameter";
|
||||
|
||||
import './DynamicButton.less';
|
||||
import "./DynamicButton.less";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -16,22 +16,20 @@ function DynamicButton({ options, selectedDynamicValue, onSelect, enabled }) {
|
||||
const menu = (
|
||||
<Menu
|
||||
className="dynamic-menu"
|
||||
onClick={({ key }) => onSelect(get(options, key, 'static'))}
|
||||
onClick={({ key }) => onSelect(get(options, key, "static"))}
|
||||
selectedKeys={[`${findIndex(options, { value: selectedDynamicValue })}`]}
|
||||
data-test="DynamicButtonMenu"
|
||||
>
|
||||
data-test="DynamicButtonMenu">
|
||||
{options.map((option, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Menu.Item key={index}>
|
||||
{option.name} {option.label && (
|
||||
<em>{isFunction(option.label) ? option.label() : option.label}</em>
|
||||
)}
|
||||
{option.name} {option.label && <em>{isFunction(option.label) ? option.label() : option.label}</em>}
|
||||
</Menu.Item>
|
||||
))}
|
||||
{enabled && <Menu.Divider />}
|
||||
{enabled && (
|
||||
<Menu.Item>
|
||||
<Icon type="arrow-left" /><Text type="secondary">Back to Static Value</Text>
|
||||
<Icon type="arrow-left" />
|
||||
<Text type="secondary">Back to Static Value</Text>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu>
|
||||
@@ -46,14 +44,8 @@ function DynamicButton({ options, selectedDynamicValue, onSelect, enabled }) {
|
||||
overlay={menu}
|
||||
className="dynamic-button"
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
icon={(
|
||||
<Icon
|
||||
type="thunderbolt"
|
||||
theme={enabled ? 'twoTone' : 'outlined'}
|
||||
className="dynamic-icon"
|
||||
/>
|
||||
)}
|
||||
trigger={["click"]}
|
||||
icon={<Icon type="thunderbolt" theme={enabled ? "twoTone" : "outlined"} className="dynamic-icon" />}
|
||||
getPopupContainer={() => containerRef.current}
|
||||
data-test="DynamicButton"
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { keys, some } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import CreateDashboardDialog from '@/components/dashboards/CreateDashboardDialog';
|
||||
import { currentUser } from '@/services/auth';
|
||||
import organizationStatus from '@/services/organizationStatus';
|
||||
import './empty-state.less';
|
||||
import { keys, some } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
|
||||
import { currentUser } from "@/services/auth";
|
||||
import organizationStatus from "@/services/organizationStatus";
|
||||
import "./empty-state.less";
|
||||
|
||||
function Step({ show, completed, text, url, urlText, onClick }) {
|
||||
if (!show) {
|
||||
@@ -16,7 +16,7 @@ function Step({ show, completed, text, url, urlText, onClick }) {
|
||||
<li className={classNames({ done: completed })}>
|
||||
<a href={url} onClick={onClick}>
|
||||
{urlText}
|
||||
</a>{' '}
|
||||
</a>{" "}
|
||||
{text}
|
||||
</li>
|
||||
);
|
||||
@@ -80,8 +80,8 @@ function EmptyState({
|
||||
</h2>
|
||||
<p>{description}</p>
|
||||
<img
|
||||
src={'/static/images/illustrations/' + illustration + '.svg'}
|
||||
alt={illustration + ' Illustration'}
|
||||
src={"/static/images/illustrations/" + illustration + ".svg"}
|
||||
alt={illustration + " Illustration"}
|
||||
width="75%"
|
||||
/>
|
||||
</div>
|
||||
@@ -134,7 +134,7 @@ function EmptyState({
|
||||
/>
|
||||
</ol>
|
||||
<p>
|
||||
Need more support?{' '}
|
||||
Need more support?{" "}
|
||||
<a href={helpLink} target="_blank" rel="noopener noreferrer">
|
||||
See our Help
|
||||
<i className="fa fa-external-link m-l-5" aria-hidden="true" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Input from 'antd/lib/input';
|
||||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
|
||||
import { Group } from '@/services/group';
|
||||
import React from "react";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Input from "antd/lib/input";
|
||||
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
|
||||
import { Group } from "@/services/group";
|
||||
|
||||
class CreateGroupDialog extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -10,13 +10,15 @@ class CreateGroupDialog extends React.Component {
|
||||
};
|
||||
|
||||
state = {
|
||||
name: '',
|
||||
name: "",
|
||||
};
|
||||
|
||||
save = () => {
|
||||
this.props.dialog.close(new Group({
|
||||
this.props.dialog.close(
|
||||
new Group({
|
||||
name: this.state.name,
|
||||
}));
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { isString } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'antd/lib/button';
|
||||
import Modal from 'antd/lib/modal';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import notification from '@/services/notification';
|
||||
import { isString } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import Modal from "antd/lib/modal";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
import notification from "@/services/notification";
|
||||
|
||||
function deleteGroup(event, group, onGroupDeleted) {
|
||||
Modal.confirm({
|
||||
title: 'Delete Group',
|
||||
content: 'Are you sure you want to delete this group?',
|
||||
okText: 'Yes',
|
||||
okType: 'danger',
|
||||
cancelText: 'No',
|
||||
title: "Delete Group",
|
||||
content: "Are you sure you want to delete this group?",
|
||||
okText: "Yes",
|
||||
okType: "danger",
|
||||
cancelText: "No",
|
||||
onOk: () => {
|
||||
group.$delete(() => {
|
||||
notification.success('Group deleted successfully.');
|
||||
notification.success("Group deleted successfully.");
|
||||
onGroupDeleted();
|
||||
});
|
||||
},
|
||||
@@ -27,11 +27,17 @@ export default function DeleteGroupButton({ group, title, onClick, children, ...
|
||||
return null;
|
||||
}
|
||||
const button = (
|
||||
<Button {...props} type="danger" onClick={event => deleteGroup(event, group, onClick)}>{children}</Button>
|
||||
<Button {...props} type="danger" onClick={event => deleteGroup(event, group, onClick)}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (isString(title) && (title !== '')) {
|
||||
return <Tooltip placement="top" title={title} mouseLeaveDelay={0}>{button}</Tooltip>;
|
||||
if (isString(title) && title !== "") {
|
||||
return (
|
||||
<Tooltip placement="top" title={title} mouseLeaveDelay={0}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'antd/lib/button';
|
||||
import Divider from 'antd/lib/divider';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Button from "antd/lib/button";
|
||||
import Divider from "antd/lib/divider";
|
||||
|
||||
import * as Sidebar from '@/components/items-list/components/Sidebar';
|
||||
import { ControllerType } from '@/components/items-list/ItemsList';
|
||||
import DeleteGroupButton from './DeleteGroupButton';
|
||||
import * as Sidebar from "@/components/items-list/components/Sidebar";
|
||||
import { ControllerType } from "@/components/items-list/ItemsList";
|
||||
import DeleteGroupButton from "./DeleteGroupButton";
|
||||
|
||||
import { currentUser } from '@/services/auth';
|
||||
import { currentUser } from "@/services/auth";
|
||||
|
||||
export default function DetailsPageSidebar({
|
||||
controller, group, items,
|
||||
canAddMembers, onAddMembersClick,
|
||||
canAddDataSources, onAddDataSourcesClick,
|
||||
controller,
|
||||
group,
|
||||
items,
|
||||
canAddMembers,
|
||||
onAddMembersClick,
|
||||
canAddDataSources,
|
||||
onAddDataSourcesClick,
|
||||
onGroupDeleted,
|
||||
}) {
|
||||
const canRemove = group && currentUser.isAdmin && (group.type !== 'builtin');
|
||||
const canRemove = group && currentUser.isAdmin && group.type !== "builtin";
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -28,18 +32,22 @@ export default function DetailsPageSidebar({
|
||||
/>
|
||||
{canAddMembers && (
|
||||
<Button className="w-100 m-t-5" type="primary" onClick={onAddMembersClick}>
|
||||
<i className="fa fa-plus m-r-5" />Add Members
|
||||
<i className="fa fa-plus m-r-5" />
|
||||
Add Members
|
||||
</Button>
|
||||
)}
|
||||
{canAddDataSources && (
|
||||
<Button className="w-100 m-t-5" type="primary" onClick={onAddDataSourcesClick}>
|
||||
<i className="fa fa-plus m-r-5" />Add Data Sources
|
||||
<i className="fa fa-plus m-r-5" />
|
||||
Add Data Sources
|
||||
</Button>
|
||||
)}
|
||||
{canRemove && (
|
||||
<React.Fragment>
|
||||
<Divider dashed className="m-t-10 m-b-10" />
|
||||
<DeleteGroupButton className="w-100" group={group} onClick={onGroupDeleted}>Delete Group</DeleteGroupButton>
|
||||
<DeleteGroupButton className="w-100" group={group} onClick={onGroupDeleted}>
|
||||
Delete Group
|
||||
</DeleteGroupButton>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { EditInPlace } from '@/components/EditInPlace';
|
||||
import { currentUser } from '@/services/auth';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { EditInPlace } from "@/components/EditInPlace";
|
||||
import { currentUser } from "@/services/auth";
|
||||
|
||||
function updateGroupName(group, name, onChange) {
|
||||
group.name = name;
|
||||
@@ -14,7 +14,7 @@ export default function GroupName({ group, onChange, ...props }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canEdit = currentUser.isAdmin && (group.type !== 'builtin');
|
||||
const canEdit = currentUser.isAdmin && group.type !== "builtin";
|
||||
|
||||
return (
|
||||
<h3 {...props}>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Tooltip from "antd/lib/tooltip";
|
||||
|
||||
export default function ListItemAddon({ isSelected, isStaged, alreadyInGroup, deselectedIcon }) {
|
||||
if (isStaged) {
|
||||
return <i className="fa fa-remove" />;
|
||||
}
|
||||
if (alreadyInGroup) {
|
||||
return <Tooltip title="Already selected"><i className="fa fa-check" /></Tooltip>;
|
||||
return (
|
||||
<Tooltip title="Already selected">
|
||||
<i className="fa fa-check" />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return isSelected ? <i className="fa fa-check" /> : <i className={`fa ${deselectedIcon}`} />;
|
||||
}
|
||||
@@ -23,5 +27,5 @@ ListItemAddon.defaultProps = {
|
||||
isSelected: false,
|
||||
isStaged: false,
|
||||
alreadyInGroup: false,
|
||||
deselectedIcon: 'fa-angle-double-right',
|
||||
deselectedIcon: "fa-angle-double-right",
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { omit, debounce } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||
import { $route, $routeParams } from '@/services/ng';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { StateStorage } from './classes/StateStorage';
|
||||
import { omit, debounce } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import hoistNonReactStatics from "hoist-non-react-statics";
|
||||
import { $route, $routeParams } from "@/services/ng";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { StateStorage } from "./classes/StateStorage";
|
||||
|
||||
export const ControllerType = PropTypes.shape({
|
||||
// values of props declared by wrapped component, current route's locals (`resolve: { ... }`) and title
|
||||
@@ -40,16 +40,18 @@ export const ControllerType = PropTypes.shape({
|
||||
export function wrap(WrappedComponent, itemsSource, stateStorage) {
|
||||
class ItemsListWrapper extends React.Component {
|
||||
static propTypes = {
|
||||
...omit(WrappedComponent.propTypes, ['controller']),
|
||||
...omit(WrappedComponent.propTypes, ["controller"]),
|
||||
onError: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
...omit(WrappedComponent.defaultProps, ['controller']),
|
||||
onError: (error) => {
|
||||
...omit(WrappedComponent.defaultProps, ["controller"]),
|
||||
onError: error => {
|
||||
// Allow calling chain to roll up, and then throw the error in global context
|
||||
setTimeout(() => { throw error; });
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
children: null,
|
||||
};
|
||||
@@ -107,10 +109,10 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
|
||||
// ANGULAR_REMOVE_ME Revisit when some React router will be used
|
||||
title: $route.current.title,
|
||||
...$routeParams,
|
||||
...omit($route.current.locals, ['$scope', '$template']),
|
||||
...omit($route.current.locals, ["$scope", "$template"]),
|
||||
|
||||
// Add to params all props except of own ones
|
||||
...omit(this.props, ['onError', 'children']),
|
||||
...omit(this.props, ["onError", "children"]),
|
||||
};
|
||||
return {
|
||||
...rest,
|
||||
@@ -118,7 +120,7 @@ export function wrap(WrappedComponent, itemsSource, stateStorage) {
|
||||
params,
|
||||
|
||||
isLoaded,
|
||||
isEmpty: !isLoaded || (totalCount === 0),
|
||||
isEmpty: !isLoaded || totalCount === 0,
|
||||
totalItemsCount: isLoaded ? totalCount : 0,
|
||||
pageSizeOptions: clientConfig.pageSizeOptions,
|
||||
pageItems: isLoaded ? pageItems : [],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { identity, isFunction, isNil, isString } from 'lodash';
|
||||
import { identity, isFunction, isNil, isString } from "lodash";
|
||||
|
||||
class ItemsFetcher {
|
||||
_getRequest(state, context) {
|
||||
@@ -20,8 +20,7 @@ class ItemsFetcher {
|
||||
|
||||
fetch(changes, state, context) {
|
||||
const request = this._getRequest(state, context);
|
||||
return this._originalDoRequest(request, context)
|
||||
.then(data => this._processResults(data, state, context));
|
||||
return this._originalDoRequest(request, context).then(data => this._processResults(data, state, context));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +30,13 @@ export class PlainListFetcher extends ItemsFetcher {
|
||||
_allItems = [];
|
||||
|
||||
_getRequest({ searchTerm, selectedTags }, context) {
|
||||
return this._originalGetRequest({
|
||||
q: isString(searchTerm) && (searchTerm !== '') ? searchTerm : undefined,
|
||||
return this._originalGetRequest(
|
||||
{
|
||||
q: isString(searchTerm) && searchTerm !== "" ? searchTerm : undefined,
|
||||
tags: selectedTags,
|
||||
}, context);
|
||||
},
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
_processResults(data, { paginator, sorter }, context) {
|
||||
@@ -69,12 +71,15 @@ export class PlainListFetcher extends ItemsFetcher {
|
||||
// items for current page and total items count)
|
||||
export class PaginatedListFetcher extends ItemsFetcher {
|
||||
_getRequest({ paginator, sorter, searchTerm, selectedTags }, context) {
|
||||
return this._originalGetRequest({
|
||||
return this._originalGetRequest(
|
||||
{
|
||||
page: paginator.page,
|
||||
page_size: paginator.itemsPerPage,
|
||||
order: sorter.compiled,
|
||||
q: isString(searchTerm) && (searchTerm !== '') ? searchTerm : undefined,
|
||||
q: isString(searchTerm) && searchTerm !== "" ? searchTerm : undefined,
|
||||
tags: selectedTags,
|
||||
}, context);
|
||||
},
|
||||
context
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isFunction, identity, map, extend } from 'lodash';
|
||||
import Paginator from './Paginator';
|
||||
import Sorter from './Sorter';
|
||||
import PromiseRejectionError from '@/lib/promise-rejection-error';
|
||||
import { PlainListFetcher, PaginatedListFetcher } from './ItemsFetcher';
|
||||
import { isFunction, identity, map, extend } from "lodash";
|
||||
import Paginator from "./Paginator";
|
||||
import Sorter from "./Sorter";
|
||||
import PromiseRejectionError from "@/lib/promise-rejection-error";
|
||||
import { PlainListFetcher, PaginatedListFetcher } from "./ItemsFetcher";
|
||||
|
||||
export class ItemsSource {
|
||||
onBeforeUpdate = null;
|
||||
@@ -38,12 +38,13 @@ export class ItemsSource {
|
||||
const customParams = {};
|
||||
const context = {
|
||||
...this.getCallbackContext(),
|
||||
setCustomParams: (params) => {
|
||||
setCustomParams: params => {
|
||||
extend(customParams, params);
|
||||
},
|
||||
};
|
||||
return this._beforeUpdate().then(() => (
|
||||
this._fetcher.fetch(changes, state, context)
|
||||
return this._beforeUpdate().then(() =>
|
||||
this._fetcher
|
||||
.fetch(changes, state, context)
|
||||
.then(({ results, count, allResults }) => {
|
||||
this._pageItems = results;
|
||||
this._allItems = allResults || null;
|
||||
@@ -51,10 +52,10 @@ export class ItemsSource {
|
||||
this._params = { ...this._params, ...customParams };
|
||||
return this._afterUpdate();
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
this.handleError(error);
|
||||
})
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
constructor({ getRequest, doRequest, processResults, isPlainList = false, ...defaultState }) {
|
||||
@@ -62,9 +63,9 @@ export class ItemsSource {
|
||||
getRequest = identity;
|
||||
}
|
||||
|
||||
this._fetcher = isPlainList ?
|
||||
new PlainListFetcher({ getRequest, doRequest, processResults }) :
|
||||
new PaginatedListFetcher({ getRequest, doRequest, processResults });
|
||||
this._fetcher = isPlainList
|
||||
? new PlainListFetcher({ getRequest, doRequest, processResults })
|
||||
: new PaginatedListFetcher({ getRequest, doRequest, processResults });
|
||||
|
||||
this.setState(defaultState);
|
||||
this._pageItems = [];
|
||||
@@ -91,7 +92,7 @@ export class ItemsSource {
|
||||
this._paginator = new Paginator(state);
|
||||
this._sorter = new Sorter(state);
|
||||
|
||||
this._searchTerm = state.searchTerm || '';
|
||||
this._searchTerm = state.searchTerm || "";
|
||||
this._selectedTags = state.selectedTags || [];
|
||||
|
||||
this._savedOrderByField = this._sorter.field;
|
||||
@@ -109,19 +110,19 @@ export class ItemsSource {
|
||||
});
|
||||
};
|
||||
|
||||
toggleSorting = (orderByField) => {
|
||||
toggleSorting = orderByField => {
|
||||
this._sorter.toggleField(orderByField);
|
||||
this._savedOrderByField = this._sorter.field;
|
||||
this._changed({ sorting: true });
|
||||
};
|
||||
|
||||
updateSearch = (searchTerm) => {
|
||||
updateSearch = searchTerm => {
|
||||
// here we update state directly, but later `fetchData` will update it properly
|
||||
this._searchTerm = searchTerm;
|
||||
// in search mode ignore the ordering and use the ranking order
|
||||
// provided by the server-side FTS backend instead, unless it was
|
||||
// requested by the user by actively ordering in search mode
|
||||
if (searchTerm === '') {
|
||||
if (searchTerm === "") {
|
||||
this._sorter.setField(this._savedOrderByField); // restore ordering
|
||||
} else {
|
||||
this._sorter.setField(null);
|
||||
@@ -130,7 +131,7 @@ export class ItemsSource {
|
||||
this._changed({ search: true, pagination: { page: true } });
|
||||
};
|
||||
|
||||
updateSelectedTags = (selectedTags) => {
|
||||
updateSelectedTags = selectedTags => {
|
||||
this._selectedTags = selectedTags;
|
||||
this._paginator.setPage(1);
|
||||
this._changed({ tags: true, pagination: { page: true } });
|
||||
@@ -138,7 +139,7 @@ export class ItemsSource {
|
||||
|
||||
update = () => this._changed();
|
||||
|
||||
handleError = (error) => {
|
||||
handleError = error => {
|
||||
if (isFunction(this.onError)) {
|
||||
// ANGULAR_REMOVE_ME This code is related to Angular's HTTP services
|
||||
if (error.status && error.data) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isUndefined } from 'lodash';
|
||||
import { isUndefined } from "lodash";
|
||||
|
||||
export default class Paginator {
|
||||
page = 1;
|
||||
@@ -17,9 +17,9 @@ export default class Paginator {
|
||||
}
|
||||
value = parseInt(value, 10) || 1;
|
||||
if (validate) {
|
||||
this.page = ((value >= 1) && (value <= this.totalPages)) ? value : 1;
|
||||
this.page = value >= 1 && value <= this.totalPages ? value : 1;
|
||||
} else {
|
||||
this.page = (value >= 1) ? value : 1;
|
||||
this.page = value >= 1 ? value : 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default class Paginator {
|
||||
return;
|
||||
}
|
||||
value = parseInt(value, 10) || 20;
|
||||
this.itemsPerPage = (value >= 1) ? value : 1;
|
||||
this.itemsPerPage = value >= 1 ? value : 1;
|
||||
if (validate) {
|
||||
this.setPage(this.page, validate);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isString, sortBy } from 'lodash';
|
||||
import { isString, sortBy } from "lodash";
|
||||
|
||||
const ORDER_BY_REVERSE = '-';
|
||||
const ORDER_BY_REVERSE = "-";
|
||||
|
||||
export function compile(field, reverse) {
|
||||
if (!field) {
|
||||
@@ -10,12 +10,12 @@ export function compile(field, reverse) {
|
||||
}
|
||||
|
||||
export function parse(compiled) {
|
||||
compiled = isString(compiled) ? compiled : '';
|
||||
compiled = isString(compiled) ? compiled : "";
|
||||
const reverse = compiled.startsWith(ORDER_BY_REVERSE);
|
||||
if (reverse) {
|
||||
compiled = compiled.substring(1);
|
||||
}
|
||||
const field = compiled !== '' ? compiled : null;
|
||||
const field = compiled !== "" ? compiled : null;
|
||||
return { field, reverse };
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class Sorter {
|
||||
}
|
||||
|
||||
setField(value) {
|
||||
this.field = isString(value) && (value !== '') ? value : null;
|
||||
this.field = isString(value) && value !== "" ? value : null;
|
||||
}
|
||||
|
||||
setReverse(value) {
|
||||
@@ -48,7 +48,7 @@ export default class Sorter {
|
||||
}
|
||||
|
||||
toggleField(field) {
|
||||
if (!isString(field) || (field === '')) {
|
||||
if (!isString(field) || field === "") {
|
||||
return;
|
||||
}
|
||||
if (field === this.field) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defaults } from 'lodash';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import { $location } from '@/services/ng';
|
||||
import { parse as parseOrderBy, compile as compileOrderBy } from './Sorter';
|
||||
import { defaults } from "lodash";
|
||||
import { clientConfig } from "@/services/auth";
|
||||
import { $location } from "@/services/ng";
|
||||
import { parse as parseOrderBy, compile as compileOrderBy } from "./Sorter";
|
||||
|
||||
export class StateStorage {
|
||||
constructor(state = {}) {
|
||||
@@ -12,16 +12,15 @@ export class StateStorage {
|
||||
return defaults(this._state, {
|
||||
page: 1,
|
||||
itemsPerPage: clientConfig.pageSize,
|
||||
orderByField: 'created_at',
|
||||
orderByField: "created_at",
|
||||
orderByReverse: false,
|
||||
searchTerm: '',
|
||||
searchTerm: "",
|
||||
tags: [],
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
setState() {
|
||||
}
|
||||
setState() {}
|
||||
}
|
||||
|
||||
export class UrlStateStorage extends StateStorage {
|
||||
@@ -29,15 +28,13 @@ export class UrlStateStorage extends StateStorage {
|
||||
const defaultState = super.getState();
|
||||
const params = $location.search();
|
||||
|
||||
const searchTerm = params.q || '';
|
||||
const searchTerm = params.q || "";
|
||||
|
||||
// in search mode order by should be explicitly specified in url, otherwise use default
|
||||
const defaultOrderBy = searchTerm !== '' ? '' : compileOrderBy(defaultState.orderByField, defaultState.orderByReverse);
|
||||
const defaultOrderBy =
|
||||
searchTerm !== "" ? "" : compileOrderBy(defaultState.orderByField, defaultState.orderByReverse);
|
||||
|
||||
const {
|
||||
field: orderByField,
|
||||
reverse: orderByReverse,
|
||||
} = parseOrderBy(params.order || defaultOrderBy);
|
||||
const { field: orderByField, reverse: orderByReverse } = parseOrderBy(params.order || defaultOrderBy);
|
||||
|
||||
return {
|
||||
page: parseInt(params.page, 10) || defaultState.page,
|
||||
@@ -54,7 +51,7 @@ export class UrlStateStorage extends StateStorage {
|
||||
page,
|
||||
page_size: itemsPerPage,
|
||||
order: compileOrderBy(orderByField, orderByReverse),
|
||||
q: searchTerm !== '' ? searchTerm : null,
|
||||
q: searchTerm !== "" ? searchTerm : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { BigMessage } from '@/components/BigMessage';
|
||||
import React from "react";
|
||||
import { BigMessage } from "@/components/BigMessage";
|
||||
|
||||
// Default "list empty" message for list pages
|
||||
export default function EmptyState(props) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { isFunction, map, filter, extend, omit, identity } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Table from 'antd/lib/table';
|
||||
import { FavoritesControl } from '@/components/FavoritesControl';
|
||||
import { TimeAgo } from '@/components/TimeAgo';
|
||||
import { durationHumanize } from '@/filters';
|
||||
import { formatDate, formatDateTime } from '@/filters/datetime';
|
||||
import { isFunction, map, filter, extend, omit, identity } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
import Table from "antd/lib/table";
|
||||
import { FavoritesControl } from "@/components/FavoritesControl";
|
||||
import { TimeAgo } from "@/components/TimeAgo";
|
||||
import { durationHumanize } from "@/filters";
|
||||
import { formatDate, formatDateTime } from "@/filters/datetime";
|
||||
|
||||
// `this` refers to previous function in the chain (`Columns.***`).
|
||||
// Adds `sorter: true` field to column definition
|
||||
@@ -16,15 +16,19 @@ function sortable(...args) {
|
||||
|
||||
export const Columns = {
|
||||
favorites(overrides) {
|
||||
return extend({
|
||||
width: '1%',
|
||||
return extend(
|
||||
{
|
||||
width: "1%",
|
||||
render: (text, item) => <FavoritesControl item={item} />,
|
||||
}, overrides);
|
||||
},
|
||||
overrides
|
||||
);
|
||||
},
|
||||
avatar(overrides, formatTitle) {
|
||||
formatTitle = isFunction(formatTitle) ? formatTitle : identity;
|
||||
return extend({
|
||||
width: '1%',
|
||||
return extend(
|
||||
{
|
||||
width: "1%",
|
||||
render: (user, item) => (
|
||||
<img
|
||||
src={item.user.profile_image_url}
|
||||
@@ -33,34 +37,51 @@ export const Columns = {
|
||||
title={formatTitle(user.name, item)}
|
||||
/>
|
||||
),
|
||||
}, overrides);
|
||||
},
|
||||
overrides
|
||||
);
|
||||
},
|
||||
date(overrides) {
|
||||
return extend({
|
||||
return extend(
|
||||
{
|
||||
render: text => formatDate(text),
|
||||
}, overrides);
|
||||
},
|
||||
overrides
|
||||
);
|
||||
},
|
||||
dateTime(overrides) {
|
||||
return extend({
|
||||
return extend(
|
||||
{
|
||||
render: text => formatDateTime(text),
|
||||
}, overrides);
|
||||
},
|
||||
overrides
|
||||
);
|
||||
},
|
||||
duration(overrides) {
|
||||
return extend({
|
||||
width: '1%',
|
||||
className: 'text-nowrap',
|
||||
return extend(
|
||||
{
|
||||
width: "1%",
|
||||
className: "text-nowrap",
|
||||
render: text => durationHumanize(text),
|
||||
}, overrides);
|
||||
},
|
||||
overrides
|
||||
);
|
||||
},
|
||||
timeAgo(overrides) {
|
||||
return extend({
|
||||
return extend(
|
||||
{
|
||||
render: value => <TimeAgo date={value} />,
|
||||
}, overrides);
|
||||
},
|
||||
overrides
|
||||
);
|
||||
},
|
||||
custom(render, overrides) {
|
||||
return extend({
|
||||
return extend(
|
||||
{
|
||||
render,
|
||||
}, overrides);
|
||||
},
|
||||
overrides
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -75,12 +96,14 @@ export default class ItemsTable extends React.Component {
|
||||
loading: PropTypes.bool,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
columns: PropTypes.arrayOf(PropTypes.shape({
|
||||
columns: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
field: PropTypes.string, // data field
|
||||
orderByField: PropTypes.string, // field to order by (defaults to `field`)
|
||||
render: PropTypes.func, // (prop, item) => text | node; `prop` is `item[field]`
|
||||
isAvailable: PropTypes.func, // return `true` to show column and `false` to hide; if omitted: show column
|
||||
})),
|
||||
})
|
||||
),
|
||||
showHeader: PropTypes.bool,
|
||||
onRowClick: PropTypes.func, // (event, item) => void
|
||||
|
||||
@@ -103,55 +126,49 @@ export default class ItemsTable extends React.Component {
|
||||
|
||||
prepareColumns() {
|
||||
const { orderByField, orderByReverse, toggleSorting } = this.props;
|
||||
const orderByDirection = orderByReverse ? 'descend' : 'ascend';
|
||||
const orderByDirection = orderByReverse ? "descend" : "ascend";
|
||||
|
||||
return map(
|
||||
map(
|
||||
filter(this.props.columns, column => (isFunction(column.isAvailable) ? column.isAvailable() : true)),
|
||||
column => extend(column, { orderByField: column.orderByField || column.field }),
|
||||
column => extend(column, { orderByField: column.orderByField || column.field })
|
||||
),
|
||||
(column, index) => {
|
||||
// Bind click events only to sortable columns
|
||||
const onHeaderCell = column.sorter ? (
|
||||
() => ({ onClick: () => toggleSorting(column.orderByField) })
|
||||
) : null;
|
||||
const onHeaderCell = column.sorter ? () => ({ onClick: () => toggleSorting(column.orderByField) }) : null;
|
||||
|
||||
// Wrap render function to pass correct arguments
|
||||
const render = isFunction(column.render) ? (text, row) => column.render(text, row.item) : identity;
|
||||
|
||||
return extend(
|
||||
omit(column, ['field', 'orderByField', 'render']),
|
||||
{
|
||||
key: 'column' + index,
|
||||
dataIndex: 'item[' + JSON.stringify(column.field) + ']',
|
||||
return extend(omit(column, ["field", "orderByField", "render"]), {
|
||||
key: "column" + index,
|
||||
dataIndex: "item[" + JSON.stringify(column.field) + "]",
|
||||
defaultSortOrder: column.orderByField === orderByField ? orderByDirection : null,
|
||||
onHeaderCell,
|
||||
render,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const columns = this.prepareColumns();
|
||||
const rows = map(
|
||||
this.props.items,
|
||||
(item, index) => ({ key: 'row' + index, item }),
|
||||
);
|
||||
const rows = map(this.props.items, (item, index) => ({ key: "row" + index, item }));
|
||||
|
||||
// Bind events only if `onRowClick` specified
|
||||
const onTableRow = isFunction(this.props.onRowClick) ? (
|
||||
row => ({
|
||||
onClick: (event) => { this.props.onRowClick(event, row.item); },
|
||||
const onTableRow = isFunction(this.props.onRowClick)
|
||||
? row => ({
|
||||
onClick: event => {
|
||||
this.props.onRowClick(event, row.item);
|
||||
},
|
||||
})
|
||||
) : null;
|
||||
: null;
|
||||
|
||||
const { showHeader } = this.props;
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={classNames('table-data', { 'ant-table-headerless': !showHeader })}
|
||||
className={classNames("table-data", { "ant-table-headerless": !showHeader })}
|
||||
loading={this.props.loading}
|
||||
columns={columns}
|
||||
showHeader={showHeader}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { BigMessage } from '@/components/BigMessage';
|
||||
import React from "react";
|
||||
import { BigMessage } from "@/components/BigMessage";
|
||||
|
||||
// Default "loading" message for list pages
|
||||
export default function LoadingState(props) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { isFunction, isString, filter, map } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Input from 'antd/lib/input';
|
||||
import AntdMenu from 'antd/lib/menu';
|
||||
import Select from 'antd/lib/select';
|
||||
import { TagsList } from '@/components/TagsList';
|
||||
import { isFunction, isString, filter, map } from "lodash";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Input from "antd/lib/input";
|
||||
import AntdMenu from "antd/lib/menu";
|
||||
import Select from "antd/lib/select";
|
||||
import { TagsList } from "@/components/TagsList";
|
||||
|
||||
/*
|
||||
SearchInput
|
||||
@@ -33,7 +33,7 @@ SearchInput.propTypes = {
|
||||
};
|
||||
|
||||
SearchInput.defaultProps = {
|
||||
placeholder: 'Search...',
|
||||
placeholder: "Search...",
|
||||
showIcon: false,
|
||||
};
|
||||
|
||||
@@ -42,10 +42,7 @@ SearchInput.defaultProps = {
|
||||
*/
|
||||
|
||||
export function Menu({ items, selected }) {
|
||||
items = filter(
|
||||
items,
|
||||
item => (isFunction(item.isAvailable) ? item.isAvailable() : true),
|
||||
);
|
||||
items = filter(items, item => (isFunction(item.isAvailable) ? item.isAvailable() : true));
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -55,10 +52,11 @@ export function Menu({ items, selected }) {
|
||||
{map(items, item => (
|
||||
<AntdMenu.Item key={item.key} className="m-0">
|
||||
<a href={item.href}>
|
||||
{
|
||||
isString(item.icon) && (item.icon !== '') &&
|
||||
<span className="btn-favourite m-r-5"><i className={item.icon} aria-hidden="true" /></span>
|
||||
}
|
||||
{isString(item.icon) && item.icon !== "" && (
|
||||
<span className="btn-favourite m-r-5">
|
||||
<i className={item.icon} aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
{isFunction(item.icon) && (item.icon(item) || null)}
|
||||
{item.title}
|
||||
</a>
|
||||
@@ -70,13 +68,15 @@ export function Menu({ items, selected }) {
|
||||
}
|
||||
|
||||
Menu.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
href: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
icon: PropTypes.func, // function to render icon
|
||||
isAvailable: PropTypes.func, // return `true` to show item and `false` to hide; if omitted: show item
|
||||
})),
|
||||
})
|
||||
),
|
||||
selected: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -90,7 +90,11 @@ Menu.defaultProps = {
|
||||
*/
|
||||
|
||||
export function MenuIcon({ icon }) {
|
||||
return <span className="btn-favourite m-r-5"><i className={icon} aria-hidden="true" /></span>;
|
||||
return (
|
||||
<span className="btn-favourite m-r-5">
|
||||
<i className={icon} aria-hidden="true" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
MenuIcon.propTypes = {
|
||||
@@ -102,7 +106,7 @@ MenuIcon.propTypes = {
|
||||
*/
|
||||
|
||||
export function ProfileImage({ user }) {
|
||||
if (!isString(user.profile_image_url) || (user.profile_image_url === '')) {
|
||||
if (!isString(user.profile_image_url) || user.profile_image_url === "") {
|
||||
return null;
|
||||
}
|
||||
return <img src={user.profile_image_url} className="profile__image--sidebar m-r-5" width="13" alt={user.name} />;
|
||||
@@ -120,7 +124,7 @@ ProfileImage.propTypes = {
|
||||
*/
|
||||
|
||||
export function Tags({ url, onChange }) {
|
||||
if (url === '') {
|
||||
if (url === "") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@@ -142,13 +146,11 @@ Tags.propTypes = {
|
||||
export function PageSizeSelect({ options, value, onChange, ...props }) {
|
||||
return (
|
||||
<div {...props}>
|
||||
<Select
|
||||
className="w-100"
|
||||
defaultValue={value}
|
||||
onChange={onChange}
|
||||
>
|
||||
<Select className="w-100" defaultValue={value} onChange={onChange}>
|
||||
{map(options, option => (
|
||||
<Select.Option key={option} value={option}>{ option } results</Select.Option>
|
||||
<Select.Option key={option} value={option}>
|
||||
{option} results
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
|
||||
import { isFinite, isString, isArray, isObject, keys, map } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isFinite, isString, isArray, isObject, keys, map } from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import cx from "classnames";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import './json-view-interactive.less';
|
||||
import "./json-view-interactive.less";
|
||||
|
||||
function JsonBlock({ value, children, openingBrace, closingBrace, withKeys }) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
@@ -15,14 +15,16 @@ function JsonBlock({ value, children, openingBrace, closingBrace, withKeys }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{(count > 0) && (
|
||||
{count > 0 && (
|
||||
<span className="jvi-toggle" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<i className={cx('fa', { 'fa-caret-right': !isExpanded, 'fa-caret-down': isExpanded })} />
|
||||
<i className={cx("fa", { "fa-caret-right": !isExpanded, "fa-caret-down": isExpanded })} />
|
||||
</span>
|
||||
)}
|
||||
<span className="jvi-punctuation jvi-braces">{openingBrace}</span>
|
||||
{!isExpanded && (count > 0) && (
|
||||
<span className="jvi-punctuation jvi-ellipsis" onClick={() => setIsExpanded(true)}>…</span>
|
||||
{!isExpanded && count > 0 && (
|
||||
<span className="jvi-punctuation jvi-ellipsis" onClick={() => setIsExpanded(true)}>
|
||||
…
|
||||
</span>
|
||||
)}
|
||||
{isExpanded && (
|
||||
<span className="jvi-block">
|
||||
@@ -32,9 +34,8 @@ function JsonBlock({ value, children, openingBrace, closingBrace, withKeys }) {
|
||||
const comma = isLast ? null : <span className="jvi-punctuation jvi-comma">,</span>;
|
||||
return (
|
||||
<span
|
||||
key={'item-' + key}
|
||||
className={cx('jvi-item', { 'jvi-nested-first': isFirst, 'jvi-nested-last': isLast })}
|
||||
>
|
||||
key={"item-" + key}
|
||||
className={cx("jvi-item", { "jvi-nested-first": isFirst, "jvi-nested-last": isLast })}>
|
||||
{withKeys && (
|
||||
<span className="jvi-object-key">
|
||||
<JsonValue value={key}>
|
||||
@@ -50,18 +51,16 @@ function JsonBlock({ value, children, openingBrace, closingBrace, withKeys }) {
|
||||
)}
|
||||
<span className="jvi-punctuation jvi-braces">{closingBrace}</span>
|
||||
{children}
|
||||
{!isExpanded && (
|
||||
<span className="jvi-comment">{' // ' + count + ' ' + (count === 1 ? 'item' : 'items')}</span>
|
||||
)}
|
||||
{!isExpanded && <span className="jvi-comment">{" // " + count + " " + (count === 1 ? "item" : "items")}</span>}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function JsonValue({ value, children }) {
|
||||
if ((value === null) || (value === false) || (value === true) || isFinite(value)) {
|
||||
if (value === null || value === false || value === true || isFinite(value)) {
|
||||
return (
|
||||
<span className="jvi-value jvi-primitive">
|
||||
{'' + value}
|
||||
{"" + value}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
@@ -77,10 +76,18 @@ function JsonValue({ value, children }) {
|
||||
);
|
||||
}
|
||||
if (isArray(value)) {
|
||||
return <JsonBlock value={value} openingBrace="[" closingBrace="]">{children}</JsonBlock>;
|
||||
return (
|
||||
<JsonBlock value={value} openingBrace="[" closingBrace="]">
|
||||
{children}
|
||||
</JsonBlock>
|
||||
);
|
||||
}
|
||||
if (isObject(value)) {
|
||||
return <JsonBlock value={value} openingBrace="{" closingBrace="}" withKeys>{children}</JsonBlock>;
|
||||
return (
|
||||
<JsonBlock value={value} openingBrace="{" closingBrace="}" withKeys>
|
||||
{children}
|
||||
</JsonBlock>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { map } from 'lodash';
|
||||
import { map } from "lodash";
|
||||
|
||||
function buildTableColumnKeywords(table) {
|
||||
const keywords = [];
|
||||
table.columns.forEach((column) => {
|
||||
table.columns.forEach(column => {
|
||||
keywords.push({
|
||||
caption: column,
|
||||
name: `${table.name}.${column}`,
|
||||
value: `${table.name}.${column}`,
|
||||
score: 100,
|
||||
meta: 'Column',
|
||||
className: 'completion',
|
||||
meta: "Column",
|
||||
className: "completion",
|
||||
});
|
||||
});
|
||||
return keywords;
|
||||
@@ -20,16 +20,16 @@ function buildKeywordsFromSchema(schema) {
|
||||
const columnKeywords = {};
|
||||
const tableColumnKeywords = {};
|
||||
|
||||
schema.forEach((table) => {
|
||||
schema.forEach(table => {
|
||||
tableKeywords.push({
|
||||
name: table.name,
|
||||
value: table.name,
|
||||
score: 100,
|
||||
meta: 'Table',
|
||||
meta: "Table",
|
||||
});
|
||||
tableColumnKeywords[table.name] = buildTableColumnKeywords(table);
|
||||
table.columns.forEach((c) => {
|
||||
columnKeywords[c] = 'Column';
|
||||
table.columns.forEach(c => {
|
||||
columnKeywords[c] = "Column";
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from "classnames";
|
||||
|
||||
import './content-with-sidebar.less';
|
||||
import "./content-with-sidebar.less";
|
||||
|
||||
const propTypes = {
|
||||
className: PropTypes.string,
|
||||
@@ -18,7 +18,7 @@ const defaultProps = {
|
||||
|
||||
function Sidebar({ className, children, ...props }) {
|
||||
return (
|
||||
<div className={classNames('layout-sidebar', className)} {...props}>
|
||||
<div className={classNames("layout-sidebar", className)} {...props}>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -31,7 +31,7 @@ Sidebar.defaultProps = defaultProps;
|
||||
|
||||
function Content({ className, children, ...props }) {
|
||||
return (
|
||||
<div className={classNames('layout-content', className)} {...props}>
|
||||
<div className={classNames("layout-content", className)} {...props}>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -43,7 +43,11 @@ Content.defaultProps = defaultProps;
|
||||
// Layout
|
||||
|
||||
export default function Layout({ className, children, ...props }) {
|
||||
return <div className={classNames('layout-with-sidebar', className)} {...props}>{children}</div>;
|
||||
return (
|
||||
<div className={classNames("layout-with-sidebar", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Layout.propTypes = propTypes;
|
||||
|
||||
@@ -12,7 +12,7 @@ const Overlay = {
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('overlay', Overlay);
|
||||
ngModule.component("overlay", Overlay);
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user