From a02ce8f0aaadcc930f7d40feb2ef03942109b38b Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 1 Aug 2019 13:03:14 -0300 Subject: [PATCH 01/34] Improve sizing for Number inputs Co-Authored-By: Ran Byron --- client/app/components/ParameterValueInput.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/components/ParameterValueInput.less b/client/app/components/ParameterValueInput.less index 9921c74a94..25acdca106 100644 --- a/client/app/components/ParameterValueInput.less +++ b/client/app/components/ParameterValueInput.less @@ -13,7 +13,7 @@ } .@{ant-prefix}-select { - width: 100%; + min-width: 100% !important; } &[data-dirty] { From 97bfcd3b6552b9006cba1fac24545c5c4b722348 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 30 Jul 2019 21:11:18 -0300 Subject: [PATCH 02/34] Migrate WidgetDialog --- .../dashboards/ExpandWidgetDialog.jsx | 29 +++++++++++++++++++ .../components/dashboards/widget-dialog.html | 12 -------- .../components/dashboards/widget-dialog.less | 8 ----- client/app/components/dashboards/widget.js | 26 ++--------------- client/app/lib/hooks/useQueryResult.js | 7 +++-- 5 files changed, 36 insertions(+), 46 deletions(-) create mode 100644 client/app/components/dashboards/ExpandWidgetDialog.jsx delete mode 100644 client/app/components/dashboards/widget-dialog.html delete mode 100644 client/app/components/dashboards/widget-dialog.less diff --git a/client/app/components/dashboards/ExpandWidgetDialog.jsx b/client/app/components/dashboards/ExpandWidgetDialog.jsx new file mode 100644 index 0000000000..bd8dac44f8 --- /dev/null +++ b/client/app/components/dashboards/ExpandWidgetDialog.jsx @@ -0,0 +1,29 @@ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { get } from 'lodash'; +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'; + +function ExpandWidgetDialog({ dialog, widget }) { + const visualizationName = get(widget, 'visualization.name'); + return ( + Close)} + > + + + ); +} + +ExpandWidgetDialog.propTypes = { + dialog: DialogPropType.isRequired, + widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types +}; + +export default wrapDialog(ExpandWidgetDialog); diff --git a/client/app/components/dashboards/widget-dialog.html b/client/app/components/dashboards/widget-dialog.html deleted file mode 100644 index 0802a81411..0000000000 --- a/client/app/components/dashboards/widget-dialog.html +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/client/app/components/dashboards/widget-dialog.less b/client/app/components/dashboards/widget-dialog.less deleted file mode 100644 index 5168a67581..0000000000 --- a/client/app/components/dashboards/widget-dialog.less +++ /dev/null @@ -1,8 +0,0 @@ -.visualization-title { - font-weight: 500; - font-size: 15px; -} - -body.modal-open .dropdown.open { - z-index: 10000; -} diff --git a/client/app/components/dashboards/widget.js b/client/app/components/dashboards/widget.js index 95b9d6380d..b9e2c74251 100644 --- a/client/app/components/dashboards/widget.js +++ b/client/app/components/dashboards/widget.js @@ -2,26 +2,13 @@ import { filter } from 'lodash'; import { angular2react } from 'angular2react'; import template from './widget.html'; import TextboxDialog from '@/components/dashboards/TextboxDialog'; -import widgetDialogTemplate from './widget-dialog.html'; +import ExpandWidgetDialog from '@/components/dashboards/ExpandWidgetDialog'; import EditParameterMappingsDialog from '@/components/dashboards/EditParameterMappingsDialog'; import './widget.less'; -import './widget-dialog.less'; - -const WidgetDialog = { - template: widgetDialogTemplate, - bindings: { - resolve: '<', - close: '&', - dismiss: '&', - }, - controller() { - this.widget = this.resolve.widget; - }, -}; export let DashboardWidget = null; // eslint-disable-line import/no-mutable-exports -function DashboardWidgetCtrl($scope, $location, $uibModal, $window, $rootScope, $timeout, Events, currentUser) { +function DashboardWidgetCtrl($scope, $location, $window, $rootScope, $timeout, Events, currentUser) { this.canViewQuery = currentUser.hasPermission('view_query'); this.editTextBox = () => { @@ -36,13 +23,7 @@ function DashboardWidgetCtrl($scope, $location, $uibModal, $window, $rootScope, }; this.expandVisualization = () => { - $uibModal.open({ - component: 'widgetDialog', - resolve: { - widget: this.widget, - }, - size: 'lg', - }); + ExpandWidgetDialog.showModal({ widget: this.widget }); }; this.hasParameters = () => this.widget.query.getParametersDefs().length > 0; @@ -125,7 +106,6 @@ const DashboardWidgetOptions = { }; export default function init(ngModule) { - ngModule.component('widgetDialog', WidgetDialog); ngModule.component('dashboardWidget', DashboardWidgetOptions); ngModule.run(['$injector', ($injector) => { DashboardWidget = angular2react('dashboardWidget ', DashboardWidgetOptions, $injector); diff --git a/client/app/lib/hooks/useQueryResult.js b/client/app/lib/hooks/useQueryResult.js index 3918dd2fb9..045abd7e9f 100644 --- a/client/app/lib/hooks/useQueryResult.js +++ b/client/app/lib/hooks/useQueryResult.js @@ -1,10 +1,11 @@ import { useState, useEffect } from 'react'; +import { isFunction } from 'lodash'; function getQueryResultData(queryResult) { return { - columns: (queryResult && queryResult.getColumns()) || [], - rows: (queryResult && queryResult.getData()) || [], - filters: (queryResult && queryResult.getFilters()) || [], + columns: (queryResult && isFunction(queryResult.getColumns) && queryResult.getColumns()) || [], + rows: (queryResult && isFunction(queryResult.getData) && queryResult.getData()) || [], + filters: (queryResult && isFunction(queryResult.getFilters) && queryResult.getFilters()) || [], }; } From d415c5e58ee0ebffda7e099fe9d97e3047f1027c Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 30 Jul 2019 21:12:06 -0300 Subject: [PATCH 03/34] Start migrating Widget --- .../components/dashboards/DashboardGrid.jsx | 3 +- client/app/components/dashboards/Widget.jsx | 74 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 client/app/components/dashboards/Widget.jsx diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index c6fc7f175e..224d2a0ffd 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -4,7 +4,8 @@ import { chain, cloneDeep, find } from 'lodash'; import { react2angular } from 'react2angular'; import cx from 'classnames'; import { Responsive, WidthProvider } from 'react-grid-layout'; -import { DashboardWidget } from '@/components/dashboards/widget'; +// import { DashboardWidget } from '@/components/dashboards/widget'; +import DashboardWidget from '@/components/dashboards/Widget'; import { FiltersType } from '@/components/Filters'; import cfg from '@/config/dashboard-grid-options'; import AutoHeightController from './AutoHeightController'; diff --git a/client/app/components/dashboards/Widget.jsx b/client/app/components/dashboards/Widget.jsx new file mode 100644 index 0000000000..bb5b792983 --- /dev/null +++ b/client/app/components/dashboards/Widget.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { get } from 'lodash'; +import { markdown } from 'markdown'; +import Button from 'antd/lib/button'; +import ExpandWidgetDialog from '@/components/dashboards/ExpandWidgetDialog'; + +class Widget extends React.Component { + static propTypes = { + widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + }; + + state = { + deleting: false, + }; + + expandWidget = () => {}; + + // eslint-disable-next-line class-methods-use-this + renderRestrictedError() { + return ( +
+
+
+

+

+ {'This widget requires access to a data source you don\'t have access to.'} +

+
+
+
+ ); + } + + renderTextbox() { + const { widget } = this.props; + if (widget.width === 0) { + return null; + } + + return ( +
+
+
+ ); + } + + renderVisualization() { + const { widget } = this.props; + + return ( +
+ ); + } + + render() { + const { widget } = this.props; + + return ( +
+ {widget.visualization && this.renderVisualization()} + {widget.restricted ? this.renderRestrictedError() : this.renderTextbox()} +
+ ); + } +} + +export default Widget; From b7cbf80596213b183b06a6e28b44936d4fc54c12 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 31 Jul 2019 11:46:32 -0300 Subject: [PATCH 04/34] Update textbox to use HtmlContent --- client/app/components/dashboards/Widget.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/app/components/dashboards/Widget.jsx b/client/app/components/dashboards/Widget.jsx index bb5b792983..651d7b4c11 100644 --- a/client/app/components/dashboards/Widget.jsx +++ b/client/app/components/dashboards/Widget.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { get } from 'lodash'; import { markdown } from 'markdown'; +import HtmlContent from '@/components/HtmlContent'; import Button from 'antd/lib/button'; import ExpandWidgetDialog from '@/components/dashboards/ExpandWidgetDialog'; @@ -41,10 +42,9 @@ class Widget extends React.Component { return (
-
+ + {markdown.toHTML(widget.text)} +
); } From 4413063ce7cfb36fdf4ee75d5956c117be184b59 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 6 Aug 2019 10:31:21 -0300 Subject: [PATCH 05/34] QueryLink migration and some updates --- client/app/components/QueryLink.jsx | 40 +++++++++++++++++++ .../dashboards/ExpandWidgetDialog.jsx | 11 +++-- client/app/lib/hooks/useQueryResult.js | 8 ++-- 3 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 client/app/components/QueryLink.jsx diff --git a/client/app/components/QueryLink.jsx b/client/app/components/QueryLink.jsx new file mode 100644 index 0000000000..e01b528dfe --- /dev/null +++ b/client/app/components/QueryLink.jsx @@ -0,0 +1,40 @@ +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') { + // link to hard-coded table tab instead of the (hidden) visualization tab + hash = 'table'; + } else { + hash = visualization.id; + } + } + + return query.getUrl(false, hash); + }; + + return ( + + {' '} + {query.name} + + ); +} + +QueryLink.propTypes = { + query: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + visualization: VisualizationType, + readOnly: PropTypes.bool, +}; + +QueryLink.defaultProps = { + visualization: null, + readOnly: false, +}; + +export default QueryLink; diff --git a/client/app/components/dashboards/ExpandWidgetDialog.jsx b/client/app/components/dashboards/ExpandWidgetDialog.jsx index bd8dac44f8..c8812654e5 100644 --- a/client/app/components/dashboards/ExpandWidgetDialog.jsx +++ b/client/app/components/dashboards/ExpandWidgetDialog.jsx @@ -1,18 +1,21 @@ - import React from 'react'; import PropTypes from 'prop-types'; -import { get } from 'lodash'; 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 ExpandWidgetDialog({ dialog, widget }) { - const visualizationName = get(widget, 'visualization.name'); return ( + + {widget.getQuery().name} + + )} width={900} footer={()} > diff --git a/client/app/lib/hooks/useQueryResult.js b/client/app/lib/hooks/useQueryResult.js index 045abd7e9f..4d75913b17 100644 --- a/client/app/lib/hooks/useQueryResult.js +++ b/client/app/lib/hooks/useQueryResult.js @@ -1,11 +1,11 @@ import { useState, useEffect } from 'react'; -import { isFunction } from 'lodash'; +import { invoke } from 'lodash'; function getQueryResultData(queryResult) { return { - columns: (queryResult && isFunction(queryResult.getColumns) && queryResult.getColumns()) || [], - rows: (queryResult && isFunction(queryResult.getData) && queryResult.getData()) || [], - filters: (queryResult && isFunction(queryResult.getFilters) && queryResult.getFilters()) || [], + columns: invoke(queryResult, 'getColumns') || [], + rows: invoke(queryResult, 'getResult') || [], + filters: invoke(queryResult, 'getFilters') || [], }; } From 1b9d00285c34d45e541e807453f7a6ffb0f35264 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 6 Aug 2019 10:50:17 -0300 Subject: [PATCH 06/34] Add visualization rendering --- client/app/components/dashboards/Widget.jsx | 53 ++++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/client/app/components/dashboards/Widget.jsx b/client/app/components/dashboards/Widget.jsx index 651d7b4c11..0cd7e6de81 100644 --- a/client/app/components/dashboards/Widget.jsx +++ b/client/app/components/dashboards/Widget.jsx @@ -1,10 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { get } from 'lodash'; +import { filter, isEmpty } from 'lodash'; import { markdown } from 'markdown'; import HtmlContent from '@/components/HtmlContent'; -import Button from 'antd/lib/button'; -import ExpandWidgetDialog from '@/components/dashboards/ExpandWidgetDialog'; +import { Parameters } from '@/components/Parameters'; +import { Timer } from '@/components/Timer'; +import QueryLink from '@/components/QueryLink'; + +import './widget.less'; class Widget extends React.Component { static propTypes = { @@ -51,11 +54,46 @@ class Widget extends React.Component { renderVisualization() { const { widget } = this.props; + const localParameters = filter( + widget.getParametersDefs(), + param => !widget.isStaticParam(param), + ); return ( -
+
+
+
+
+
+ +
+ +
+
+

+ {/* TODO: add readOnly rule */} + +

+
+ + {markdown.toHTML(widget.getQuery().description || '')} + +
+
+
+ {!isEmpty(localParameters) && ( +
+ +
+ )} +
+
+
+ +
+
+
+
); } @@ -65,7 +103,8 @@ class Widget extends React.Component { return (
{widget.visualization && this.renderVisualization()} - {widget.restricted ? this.renderRestrictedError() : this.renderTextbox()} + {(!widget.visualization && widget.restricted) && this.renderRestrictedError()} + {(!widget.visualization && !widget.restricted) && this.renderTextbox()}
); } From 673f6c8cde451f22a7e7f3cde9d069be499dd048 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 8 Aug 2019 14:03:05 -0300 Subject: [PATCH 07/34] Render widget --- .../assets/less/redash/redash-newstyle.less | 6 +- client/app/components/Timer.jsx | 4 +- client/app/components/dashboards/Widget.jsx | 112 +++++++++++++----- client/app/components/dashboards/widget.less | 19 --- .../app/visualizations/VisualizationName.jsx | 15 +-- .../app/visualizations/VisualizationName.less | 18 +++ 6 files changed, 111 insertions(+), 63 deletions(-) create mode 100644 client/app/visualizations/VisualizationName.less diff --git a/client/app/assets/less/redash/redash-newstyle.less b/client/app/assets/less/redash/redash-newstyle.less index f82457a9d4..cc96cf057e 100644 --- a/client/app/assets/less/redash/redash-newstyle.less +++ b/client/app/assets/less/redash/redash-newstyle.less @@ -464,7 +464,7 @@ body { .refresh-indicator { transition-duration: 0s; - rd-timer { + .rd-timer { display: none; } @@ -528,7 +528,7 @@ body { } } - rd-timer { + .rd-timer { font-size: 13px; display: inline-block; font-variant-numeric: tabular-nums; @@ -560,7 +560,7 @@ body { opacity: 0; } - rd-timer { + .rd-timer { transition-delay: 0s; opacity: 1; transform: translateX(0); diff --git a/client/app/components/Timer.jsx b/client/app/components/Timer.jsx index 72c5b7af12..ebaa976b74 100644 --- a/client/app/components/Timer.jsx +++ b/client/app/components/Timer.jsx @@ -1,5 +1,5 @@ +import React, { useMemo, useEffect } from 'react'; import moment from 'moment'; -import { useMemo, useEffect } from 'react'; import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; import { Moment } from '@/components/proptypes'; @@ -17,7 +17,7 @@ export function Timer({ from }) { const diff = moment.now() - startTime; const format = diff > 1000 * 60 * 60 ? 'HH:mm:ss' : 'mm:ss'; // no HH under an hour - return moment.utc(diff).format(format); + return ({moment.utc(diff).format(format)}); } Timer.propTypes = { diff --git a/client/app/components/dashboards/Widget.jsx b/client/app/components/dashboards/Widget.jsx index 0cd7e6de81..1427a74326 100644 --- a/client/app/components/dashboards/Widget.jsx +++ b/client/app/components/dashboards/Widget.jsx @@ -2,25 +2,42 @@ import React from 'react'; import PropTypes from 'prop-types'; import { filter, isEmpty } from 'lodash'; import { markdown } from 'markdown'; +import { currentUser } from '@/services/auth'; import HtmlContent from '@/components/HtmlContent'; import { Parameters } from '@/components/Parameters'; import { Timer } from '@/components/Timer'; import QueryLink from '@/components/QueryLink'; +import { FiltersType } from '@/components/Filters'; +import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer'; import './widget.less'; class Widget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types - dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + filters: FiltersType, }; - state = { - deleting: false, + static defaultProps = { + filters: [], }; expandWidget = () => {}; + refreshWidget = () => {}; + + renderRefreshIndicator() { + const { widget } = this.props; + return ( +
+
+ +
+ +
+ ); + } + // eslint-disable-next-line class-methods-use-this renderRestrictedError() { return ( @@ -37,42 +54,62 @@ class Widget extends React.Component { ); } - renderTextbox() { - const { widget } = this.props; - if (widget.width === 0) { - return null; + // eslint-disable-next-line class-methods-use-this + renderWidgetVisualization() { + const { widget, filters } = this.props; + const widgetQueryResult = widget.getQueryResult(); + const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus(); + switch (widgetStatus) { + case 'failed': + return ( +
+ {widgetQueryResult.getError() && ( +
+ Error running query: {widgetQueryResult.getError()} +
+ )} +
+ ); + case 'done': + return ( +
+ +
+ ); + default: + return ( +
+
+ +
+
+ ); } - - return ( -
- - {markdown.toHTML(widget.text)} - -
- ); } - renderVisualization() { + renderWidget() { const { widget } = this.props; + const canViewQuery = currentUser.hasPermission('view_query'); + const widgetQueryResult = widget.getQueryResult(); + const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); + const localParameters = filter( widget.getParametersDefs(), param => !widget.isStaticParam(param), ); return ( -
+
-
-
- -
- -
+ {widget.loading && this.renderRefreshIndicator()}

- {/* TODO: add readOnly rule */} - +

@@ -83,26 +120,37 @@ class Widget extends React.Component {
{!isEmpty(localParameters) && (
- +
)}
-
-
- -
-
+ {this.renderWidgetVisualization()}
); } + renderTextbox() { + const { widget } = this.props; + if (widget.width === 0) { + return null; + } + + return ( +
+ + {markdown.toHTML(widget.text)} + +
+ ); + } + render() { const { widget } = this.props; return (
- {widget.visualization && this.renderVisualization()} + {widget.visualization && this.renderWidget()} {(!widget.visualization && widget.restricted) && this.renderRestrictedError()} {(!widget.visualization && !widget.restricted) && this.renderTextbox()}
diff --git a/client/app/components/dashboards/widget.less b/client/app/components/dashboards/widget.less index 0d356d7d12..ac66763cb9 100644 --- a/client/app/components/dashboards/widget.less +++ b/client/app/components/dashboards/widget.less @@ -2,25 +2,6 @@ color: rgba(0, 0, 0, 0.5); } -visualization-name:empty + span { - color: rgba(0, 0, 0, 0.8); -} - -visualization-name { - font-size: 15px; - font-weight: 500; - color: rgba(0, 0, 0, 0.8); - - &:after { - content: "−"; - margin-left: 5px; - } - - &:empty:after { - content: none; - } -} - .th-title p.hidden-print { margin-bottom: 0; } diff --git a/client/app/visualizations/VisualizationName.jsx b/client/app/visualizations/VisualizationName.jsx index 83c67765cc..ca536961df 100644 --- a/client/app/visualizations/VisualizationName.jsx +++ b/client/app/visualizations/VisualizationName.jsx @@ -1,15 +1,16 @@ +import React from 'react'; import { react2angular } from 'react2angular'; import { VisualizationType, registeredVisualizations } from './index'; +import './VisualizationName.less'; + export function VisualizationName({ visualization }) { const config = registeredVisualizations[visualization.type]; - if (config) { - if (visualization.name !== config.name) { - return visualization.name; - } - } - - return null; + return ( + + {config && (visualization.name !== config.name) ? visualization.name : null} + + ); } VisualizationName.propTypes = { diff --git a/client/app/visualizations/VisualizationName.less b/client/app/visualizations/VisualizationName.less new file mode 100644 index 0000000000..40f0b28ebc --- /dev/null +++ b/client/app/visualizations/VisualizationName.less @@ -0,0 +1,18 @@ +.visualization-name:empty + span { + color: rgba(0, 0, 0, 0.8); +} + +.visualization-name { + font-size: 15px; + font-weight: 500; + color: rgba(0, 0, 0, 0.8); + + &:after { + content: "−"; + margin-left: 5px; + } + + &:empty:after { + content: none; + } +} From 0d9599699fb6f781d854616f8492dc7db326d12b Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sun, 11 Aug 2019 21:39:02 -0300 Subject: [PATCH 08/34] Add delete button --- .../components/dashboards/DashboardGrid.jsx | 5 +- client/app/components/dashboards/Widget.jsx | 72 +++++++++++++++---- client/app/components/dashboards/widget.less | 4 ++ client/app/lib/hooks/useQueryResult.js | 2 +- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index 224d2a0ffd..569c137e8d 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -199,8 +199,9 @@ class DashboardGrid extends React.Component { widget={widget} dashboard={dashboard} filters={this.props.filters} - deleted={() => onRemoveWidget(widget.id)} - public={this.props.isPublic} + onDelete={() => onRemoveWidget(widget.id)} + isPublic={this.props.isPublic} + canEdit={dashboard.canEdit()} />
))} diff --git a/client/app/components/dashboards/Widget.jsx b/client/app/components/dashboards/Widget.jsx index 1427a74326..094ee1b1e9 100644 --- a/client/app/components/dashboards/Widget.jsx +++ b/client/app/components/dashboards/Widget.jsx @@ -2,7 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { filter, isEmpty } from 'lodash'; import { markdown } from 'markdown'; +import Modal from 'antd/lib/modal'; import { currentUser } from '@/services/auth'; +import recordEvent from '@/services/recordEvent'; import HtmlContent from '@/components/HtmlContent'; import { Parameters } from '@/components/Parameters'; import { Timer } from '@/components/Timer'; @@ -16,16 +18,44 @@ class Widget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types filters: FiltersType, + isPublic: PropTypes.bool, + canEdit: PropTypes.bool, + onDelete: PropTypes.func, }; static defaultProps = { filters: [], + isPublic: false, + canEdit: false, + onDelete: () => {}, }; + componentDidMount() { + recordEvent('view', 'widget', this.props.widget.id); + } + expandWidget = () => {}; refreshWidget = () => {}; + deleteWidget = () => { + const { widget, onDelete } = this.props; + + const doDelete = () => { + widget.delete().then(() => onDelete({})); + }; + + Modal.confirm({ + title: 'Delete Widget', + content: `Are you sure you want to remove "${widget.getName()}" from the dashboard?`, + okText: 'Delete', + okType: 'danger', + onOk: doDelete, + maskClosable: true, + autoFocusButton: null, + }); + }; + renderRefreshIndicator() { const { widget } = this.props; return ( @@ -54,6 +84,33 @@ class Widget extends React.Component { ); } + renderWidgetHeader() { + const { widget, isPublic, canEdit } = this.props; + const canViewQuery = currentUser.hasPermission('view_query'); + return ( +
+ {(!isPublic && canEdit) && ( +
+
+ +
+
+ )} + {widget.loading && this.renderRefreshIndicator()} +
+

+ +

+
+ + {markdown.toHTML(widget.getQuery().description || '')} + +
+
+
+ ); + } + // eslint-disable-next-line class-methods-use-this renderWidgetVisualization() { const { widget, filters } = this.props; @@ -93,7 +150,6 @@ class Widget extends React.Component { renderWidget() { const { widget } = this.props; - const canViewQuery = currentUser.hasPermission('view_query'); const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); @@ -105,19 +161,7 @@ class Widget extends React.Component { return (
-
- {widget.loading && this.renderRefreshIndicator()} -
-

- -

-
- - {markdown.toHTML(widget.getQuery().description || '')} - -
-
-
+ {this.renderWidgetHeader()} {!isEmpty(localParameters) && (
diff --git a/client/app/components/dashboards/widget.less b/client/app/components/dashboards/widget.less index ac66763cb9..cc82469a10 100644 --- a/client/app/components/dashboards/widget.less +++ b/client/app/components/dashboards/widget.less @@ -52,6 +52,10 @@ .actions { position: static; + + a { + cursor: pointer; + } } } } diff --git a/client/app/lib/hooks/useQueryResult.js b/client/app/lib/hooks/useQueryResult.js index 4d75913b17..43b420c223 100644 --- a/client/app/lib/hooks/useQueryResult.js +++ b/client/app/lib/hooks/useQueryResult.js @@ -4,7 +4,7 @@ import { invoke } from 'lodash'; function getQueryResultData(queryResult) { return { columns: invoke(queryResult, 'getColumns') || [], - rows: invoke(queryResult, 'getResult') || [], + rows: invoke(queryResult, 'getData') || [], filters: invoke(queryResult, 'getFilters') || [], }; } From dd9fd35943a7cee0c6bab1fc97eb4cb8cc38542d Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sun, 11 Aug 2019 22:40:04 -0300 Subject: [PATCH 09/34] Update AutoHeight --- .../dashboards/AutoHeightController.js | 2 +- client/app/components/dashboards/Widget.jsx | 25 ++++++++++++++----- .../visualizations/VisualizationRenderer.jsx | 4 +-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/client/app/components/dashboards/AutoHeightController.js b/client/app/components/dashboards/AutoHeightController.js index d0aa8b5ddd..4763dcb999 100644 --- a/client/app/components/dashboards/AutoHeightController.js +++ b/client/app/components/dashboards/AutoHeightController.js @@ -5,7 +5,7 @@ import { includes, reduce, some } from 'lodash'; const WIDGET_SELECTOR = '[data-widgetid="{0}"]'; const WIDGET_CONTENT_SELECTOR = [ '.widget-header', // header - 'visualization-renderer', // visualization + '.visualization-renderer', // visualization '.scrollbox .alert', // error state '.spinner-container', // loading state '.tile__bottom-control', // footer diff --git a/client/app/components/dashboards/Widget.jsx b/client/app/components/dashboards/Widget.jsx index 094ee1b1e9..5e50ba3c33 100644 --- a/client/app/components/dashboards/Widget.jsx +++ b/client/app/components/dashboards/Widget.jsx @@ -5,6 +5,7 @@ import { markdown } from 'markdown'; import Modal from 'antd/lib/modal'; import { currentUser } from '@/services/auth'; import recordEvent from '@/services/recordEvent'; +import { $location } from '@/services/ng'; import HtmlContent from '@/components/HtmlContent'; import { Parameters } from '@/components/Parameters'; import { Timer } from '@/components/Timer'; @@ -31,9 +32,23 @@ class Widget extends React.Component { }; componentDidMount() { - recordEvent('view', 'widget', this.props.widget.id); + const { widget } = this.props; + recordEvent('view', 'widget', widget.id); + + if (widget.visualization) { + recordEvent('view', 'query', widget.visualization.query.id, { dashboard: true }); + recordEvent('view', 'visualization', widget.visualization.id, { dashboard: true }); + + this.loadWidget(); + } } + loadWidget = (refresh = false) => { + const { widget } = this.props; + const maxAge = $location.search().maxAge; + return widget.load(refresh, maxAge); + }; + expandWidget = () => {}; refreshWidget = () => {}; @@ -101,11 +116,9 @@ class Widget extends React.Component {

-
- - {markdown.toHTML(widget.getQuery().description || '')} - -
+ + {markdown.toHTML(widget.getQuery().description || '')} +
); diff --git a/client/app/visualizations/VisualizationRenderer.jsx b/client/app/visualizations/VisualizationRenderer.jsx index 9b6e3db69a..b5a7f86772 100644 --- a/client/app/visualizations/VisualizationRenderer.jsx +++ b/client/app/visualizations/VisualizationRenderer.jsx @@ -58,7 +58,7 @@ export function VisualizationRenderer(props) { lastOptions.current = options; return ( - +
{showFilters && }
- +
); } From ca01216954b3c832776b5452c4fa023b00814a28 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Mon, 12 Aug 2019 14:17:58 -0300 Subject: [PATCH 10/34] Add widget bottom --- client/app/components/dashboards/Widget.jsx | 63 ++++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/client/app/components/dashboards/Widget.jsx b/client/app/components/dashboards/Widget.jsx index 5e50ba3c33..994816f8d8 100644 --- a/client/app/components/dashboards/Widget.jsx +++ b/client/app/components/dashboards/Widget.jsx @@ -2,15 +2,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import { filter, isEmpty } from 'lodash'; import { markdown } from 'markdown'; +import classNames from 'classnames'; import Modal from 'antd/lib/modal'; import { currentUser } from '@/services/auth'; import recordEvent from '@/services/recordEvent'; import { $location } from '@/services/ng'; +import { formatDateTime } from '@/filters/datetime'; import HtmlContent from '@/components/HtmlContent'; import { Parameters } from '@/components/Parameters'; import { Timer } from '@/components/Timer'; +import { TimeAgo } from '@/components/TimeAgo'; import QueryLink from '@/components/QueryLink'; import { FiltersType } from '@/components/Filters'; +import ExpandWidgetDialog from '@/components/dashboards/ExpandWidgetDialog'; import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer'; import './widget.less'; @@ -31,6 +35,10 @@ class Widget extends React.Component { onDelete: () => {}, }; + state = { + refreshClickButtonId: null, + }; + componentDidMount() { const { widget } = this.props; recordEvent('view', 'widget', widget.id); @@ -49,9 +57,16 @@ class Widget extends React.Component { return widget.load(refresh, maxAge); }; - expandWidget = () => {}; + expandWidget = () => { + ExpandWidgetDialog.showModal({ widget: this.props.widget }); + }; - refreshWidget = () => {}; + refreshWidget = (refreshClickButtonId) => { + if (!this.state.refreshClickButtonId) { + this.setState({ refreshClickButtonId }); + this.loadWidget(true).finally(() => this.setState({ refreshClickButtonId: null })); + } + }; deleteWidget = () => { const { widget, onDelete } = this.props; @@ -124,6 +139,48 @@ class Widget extends React.Component { ); } + renderWidgetBottom() { + const { widget, isPublic } = this.props; + const widgetQueryResult = widget.getQueryResult(); + const updatedAt = widgetQueryResult && widgetQueryResult.getUpdatedAt(); + const { refreshClickButtonId } = this.state; + return ( +
+ {(!isPublic && !!widgetQueryResult) && ( + this.refreshWidget(1)} + data-test="RefreshButton" + > + {' '} + + + )} + + {' '}{formatDateTime(updatedAt)} + + {isPublic ? ( + + {' '} + + ) : ( + this.refreshWidget(2)} + > + + + )} + + + +
+ ); + } + // eslint-disable-next-line class-methods-use-this renderWidgetVisualization() { const { widget, filters } = this.props; @@ -182,7 +239,7 @@ class Widget extends React.Component { )}
{this.renderWidgetVisualization()} -
+ {this.renderWidgetBottom()}
); } From 7738c9e99fc665ed12afbf19424f690daa6e398d Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Mon, 12 Aug 2019 22:15:27 -0300 Subject: [PATCH 11/34] Add Drodpown button --- client/app/components/dashboards/Widget.jsx | 119 ++++++++++++++----- client/app/components/dashboards/widget.less | 9 +- 2 files changed, 95 insertions(+), 33 deletions(-) diff --git a/client/app/components/dashboards/Widget.jsx b/client/app/components/dashboards/Widget.jsx index 994816f8d8..e6c61f2288 100644 --- a/client/app/components/dashboards/Widget.jsx +++ b/client/app/components/dashboards/Widget.jsx @@ -3,7 +3,9 @@ import PropTypes from 'prop-types'; import { filter, isEmpty } from 'lodash'; import { markdown } from 'markdown'; import classNames from 'classnames'; +import Dropdown from 'antd/lib/dropdown'; import Modal from 'antd/lib/modal'; +import Menu from 'antd/lib/menu'; import { currentUser } from '@/services/auth'; import recordEvent from '@/services/recordEvent'; import { $location } from '@/services/ng'; @@ -19,6 +21,29 @@ import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer'; import './widget.less'; +function WidgetMenu(props) { + return ( + + Download as CSV File + Download as Excel File + + View Query + Edit Parameters + + Remove from Dashboard + + ); +} + +function TextboxMenu(props) { + return ( + + Edit + Remove from Dashboard + + ); +} + class Widget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types @@ -117,24 +142,50 @@ class Widget extends React.Component { renderWidgetHeader() { const { widget, isPublic, canEdit } = this.props; const canViewQuery = currentUser.hasPermission('view_query'); + + const localParameters = filter( + widget.getParametersDefs(), + param => !widget.isStaticParam(param), + ); + return ( -
- {(!isPublic && canEdit) && ( -
-
- -
+
+
+ {(!isPublic && canEdit) && ( + +
+
+ +
+
+
+
+ } + placement="bottomRight" + trigger={['click']} + > + + +
+
+
+ )} + {widget.loading && this.renderRefreshIndicator()} +
+

+ +

+ + {markdown.toHTML(widget.getQuery().description || '')} +
- )} - {widget.loading && this.renderRefreshIndicator()} -
-

- -

- - {markdown.toHTML(widget.getQuery().description || '')} -
+ {!isEmpty(localParameters) && ( +
+ +
+ )}
); } @@ -223,21 +274,9 @@ class Widget extends React.Component { const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); - const localParameters = filter( - widget.getParametersDefs(), - param => !widget.isStaticParam(param), - ); - return (
-
- {this.renderWidgetHeader()} - {!isEmpty(localParameters) && ( -
- -
- )} -
+ {this.renderWidgetHeader()} {this.renderWidgetVisualization()} {this.renderWidgetBottom()}
@@ -245,13 +284,35 @@ class Widget extends React.Component { } renderTextbox() { - const { widget } = this.props; + const { widget, isPublic, canEdit } = this.props; if (widget.width === 0) { return null; } return (
+
+ {(!isPublic && canEdit) && ( + +
+
+ +
+
+
+
+ } + placement="bottomRight" + trigger={['click']} + > + + +
+
+
+ )} +
{markdown.toHTML(widget.text)} diff --git a/client/app/components/dashboards/widget.less b/client/app/components/dashboards/widget.less index cc82469a10..e6703da066 100644 --- a/client/app/components/dashboards/widget.less +++ b/client/app/components/dashboards/widget.less @@ -52,10 +52,7 @@ .actions { position: static; - - a { - cursor: pointer; - } + cursor: pointer; } } } @@ -72,6 +69,10 @@ :last-child { margin-bottom: 0; } + + .actions { + cursor: pointer; + } } } From ffcb41980c220778d00d35961af920afc3802c1a Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 15 Aug 2019 16:15:39 -0300 Subject: [PATCH 12/34] Split Widget component --- .../components/dashboards/DashboardGrid.jsx | 2 +- ...getDialog.jsx => ExpandedWidgetDialog.jsx} | 10 +- .../components/dashboards/TextboxDialog.jsx | 1 - .../components/dashboards/dashboard-grid.less | 29 +++ .../dashboard-widget/TextboxWidget.jsx | 87 +++++++ .../VisualizationWidget.jsx} | 213 ++++++------------ .../dashboards/dashboard-widget/Widget.jsx | 67 ++++++ .../dashboards/dashboard-widget/Widget.less | 77 +++++++ .../dashboards/dashboard-widget/index.js | 3 + client/app/components/dashboards/widget.js | 5 +- client/app/components/dashboards/widget.less | 30 --- 11 files changed, 335 insertions(+), 189 deletions(-) rename client/app/components/dashboards/{ExpandWidgetDialog.jsx => ExpandedWidgetDialog.jsx} (83%) create mode 100644 client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx rename client/app/components/dashboards/{Widget.jsx => dashboard-widget/VisualizationWidget.jsx} (69%) create mode 100644 client/app/components/dashboards/dashboard-widget/Widget.jsx create mode 100644 client/app/components/dashboards/dashboard-widget/Widget.less create mode 100644 client/app/components/dashboards/dashboard-widget/index.js diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index 569c137e8d..e8a51c8bdf 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -5,7 +5,7 @@ import { react2angular } from 'react2angular'; import cx from 'classnames'; import { Responsive, WidthProvider } from 'react-grid-layout'; // import { DashboardWidget } from '@/components/dashboards/widget'; -import DashboardWidget from '@/components/dashboards/Widget'; +import DashboardWidget from '@/components/dashboards/dashboard-widget'; import { FiltersType } from '@/components/Filters'; import cfg from '@/config/dashboard-grid-options'; import AutoHeightController from './AutoHeightController'; diff --git a/client/app/components/dashboards/ExpandWidgetDialog.jsx b/client/app/components/dashboards/ExpandedWidgetDialog.jsx similarity index 83% rename from client/app/components/dashboards/ExpandWidgetDialog.jsx rename to client/app/components/dashboards/ExpandedWidgetDialog.jsx index c8812654e5..49756a1d30 100644 --- a/client/app/components/dashboards/ExpandWidgetDialog.jsx +++ b/client/app/components/dashboards/ExpandedWidgetDialog.jsx @@ -6,15 +6,15 @@ import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer'; import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; import { VisualizationName } from '@/visualizations/VisualizationName'; -function ExpandWidgetDialog({ dialog, widget }) { +function ExpandedWidgetDialog({ dialog, widget }) { return ( + <> {widget.getQuery().name} - + )} width={900} footer={()} @@ -24,9 +24,9 @@ function ExpandWidgetDialog({ dialog, widget }) { ); } -ExpandWidgetDialog.propTypes = { +ExpandedWidgetDialog.propTypes = { dialog: DialogPropType.isRequired, widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types }; -export default wrapDialog(ExpandWidgetDialog); +export default wrapDialog(ExpandedWidgetDialog); diff --git a/client/app/components/dashboards/TextboxDialog.jsx b/client/app/components/dashboards/TextboxDialog.jsx index b85509a822..a6106a5cda 100644 --- a/client/app/components/dashboards/TextboxDialog.jsx +++ b/client/app/components/dashboards/TextboxDialog.jsx @@ -14,7 +14,6 @@ import './TextboxDialog.less'; class TextboxDialog extends React.Component { static propTypes = { - dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types dialog: DialogPropType.isRequired, onConfirm: PropTypes.func.isRequired, text: PropTypes.string, diff --git a/client/app/components/dashboards/dashboard-grid.less b/client/app/components/dashboards/dashboard-grid.less index 83af1a7843..ac58381f4c 100644 --- a/client/app/components/dashboards/dashboard-grid.less +++ b/client/app/components/dashboards/dashboard-grid.less @@ -5,3 +5,32 @@ } } } + +// react-grid-layout overrides +.react-grid-item { + + // placeholder color + &.react-grid-placeholder { + border-radius: 3px; + background-color: #E0E6EB; + opacity: 0.5; + } + + // resize placeholder behind widget, the lib's default is above 🤷‍♂️ + &.resizing { + z-index: 3; + } + + // auto-height animation + &.cssTransforms:not(.resizing) { + transition-property: transform, height; // added ", height" + } + + // resize handle size + & > .react-resizable-handle::after { + width: 11px; + height: 11px; + right: 5px; + bottom: 5px; + } +} diff --git a/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx new file mode 100644 index 0000000000..e7c8c81ca1 --- /dev/null +++ b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { markdown } from 'markdown'; +import Dropdown from 'antd/lib/dropdown'; +import Menu from 'antd/lib/menu'; +import Modal from 'antd/lib/modal'; +import HtmlContent from '@/components/HtmlContent'; +import TextboxDialog from '@/components/dashboards/TextboxDialog'; + +function TextboxWidget({ widget, showDropdown, showDeleteButton, onDelete }) { + const [text, setText] = useState(widget.text); + + const editTextBox = () => { + TextboxDialog.showModal({ + text: widget.text, + onConfirm: (newText) => { + widget.text = newText; + setText(newText); + return widget.save(); + }, + }); + }; + + const deleteTextbox = () => { + Modal.confirm({ + title: 'Delete Textbox', + content: 'Are you sure you want to remove this textbox from the dashboard?', + okText: 'Delete', + okType: 'danger', + onOk: () => widget.delete().then(onDelete), + maskClosable: true, + autoFocusButton: null, + }); + }; + + const TextboxMenu = ( + + Edit + Remove from Dashboard + + ); + + return ( +
+
+ {showDeleteButton && ( +
+
+ +
+
+ )} + {showDropdown && ( +
+
+ + + +
+
+ )} +
+ + {markdown.toHTML(text || '')} + +
+ ); +} + +TextboxWidget.propTypes = { + widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + showDropdown: PropTypes.bool, + showDeleteButton: PropTypes.bool, + onDelete: PropTypes.func, +}; + +TextboxWidget.defaultProps = { + showDropdown: false, + showDeleteButton: false, + onDelete: () => {}, +}; + +export default TextboxWidget; diff --git a/client/app/components/dashboards/Widget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx similarity index 69% rename from client/app/components/dashboards/Widget.jsx rename to client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index e6c61f2288..dfd64b4249 100644 --- a/client/app/components/dashboards/Widget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -4,8 +4,8 @@ import { filter, isEmpty } from 'lodash'; import { markdown } from 'markdown'; import classNames from 'classnames'; import Dropdown from 'antd/lib/dropdown'; -import Modal from 'antd/lib/modal'; import Menu from 'antd/lib/menu'; +import Modal from 'antd/lib/modal'; import { currentUser } from '@/services/auth'; import recordEvent from '@/services/recordEvent'; import { $location } from '@/services/ng'; @@ -16,12 +16,10 @@ import { Timer } from '@/components/Timer'; import { TimeAgo } from '@/components/TimeAgo'; import QueryLink from '@/components/QueryLink'; import { FiltersType } from '@/components/Filters'; -import ExpandWidgetDialog from '@/components/dashboards/ExpandWidgetDialog'; +import ExpandedWidgetDialog from '@/components/dashboards/ExpandedWidgetDialog'; import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer'; -import './widget.less'; - -function WidgetMenu(props) { +function VisualizationWidgetMenu(props) { return ( Download as CSV File @@ -35,16 +33,7 @@ function WidgetMenu(props) { ); } -function TextboxMenu(props) { - return ( - - Edit - Remove from Dashboard - - ); -} - -class Widget extends React.Component { +class VisualizationWidget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types filters: FiltersType, @@ -66,14 +55,9 @@ class Widget extends React.Component { componentDidMount() { const { widget } = this.props; - recordEvent('view', 'widget', widget.id); - - if (widget.visualization) { - recordEvent('view', 'query', widget.visualization.query.id, { dashboard: true }); - recordEvent('view', 'visualization', widget.visualization.id, { dashboard: true }); - - this.loadWidget(); - } + recordEvent('view', 'query', widget.visualization.query.id, { dashboard: true }); + recordEvent('view', 'visualization', widget.visualization.id, { dashboard: true }); + this.loadWidget(); } loadWidget = (refresh = false) => { @@ -83,34 +67,30 @@ class Widget extends React.Component { }; expandWidget = () => { - ExpandWidgetDialog.showModal({ widget: this.props.widget }); - }; - - refreshWidget = (refreshClickButtonId) => { - if (!this.state.refreshClickButtonId) { - this.setState({ refreshClickButtonId }); - this.loadWidget(true).finally(() => this.setState({ refreshClickButtonId: null })); - } + ExpandedWidgetDialog.showModal({ widget: this.props.widget }); }; deleteWidget = () => { const { widget, onDelete } = this.props; - const doDelete = () => { - widget.delete().then(() => onDelete({})); - }; - Modal.confirm({ title: 'Delete Widget', - content: `Are you sure you want to remove "${widget.getName()}" from the dashboard?`, + content: 'Are you sure you want to remove this widget from the dashboard?', okText: 'Delete', okType: 'danger', - onOk: doDelete, + onOk: () => widget.delete().then(onDelete), maskClosable: true, autoFocusButton: null, }); }; + refreshWidget = (refreshClickButtonId) => { + if (!this.state.refreshClickButtonId) { + this.setState({ refreshClickButtonId }); + this.loadWidget(true).finally(() => this.setState({ refreshClickButtonId: null })); + } + }; + renderRefreshIndicator() { const { widget } = this.props; return ( @@ -123,23 +103,7 @@ class Widget extends React.Component { ); } - // eslint-disable-next-line class-methods-use-this - renderRestrictedError() { - return ( -
-
-
-

-

- {'This widget requires access to a data source you don\'t have access to.'} -

-
-
-
- ); - } - - renderWidgetHeader() { + renderHeader() { const { widget, isPublic, canEdit } = this.props; const canViewQuery = currentUser.hasPermission('view_query'); @@ -152,7 +116,7 @@ class Widget extends React.Component {
{(!isPublic && canEdit) && ( - + <>
@@ -161,7 +125,7 @@ class Widget extends React.Component {
} + overlay={} placement="bottomRight" trigger={['click']} > @@ -169,7 +133,7 @@ class Widget extends React.Component {
- + )} {widget.loading && this.renderRefreshIndicator()}
@@ -190,7 +154,44 @@ class Widget extends React.Component { ); } - renderWidgetBottom() { + // eslint-disable-next-line class-methods-use-this + renderVisualization() { + const { widget, filters } = this.props; + const widgetQueryResult = widget.getQueryResult(); + const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus(); + switch (widgetStatus) { + case 'failed': + return ( +
+ {widgetQueryResult.getError() && ( +
+ Error running query: {widgetQueryResult.getError()} +
+ )} +
+ ); + case 'done': + return ( +
+ +
+ ); + default: + return ( +
+
+ +
+
+ ); + } + } + + renderBottom() { const { widget, isPublic } = this.props; const widgetQueryResult = widget.getQueryResult(); const updatedAt = widgetQueryResult && widgetQueryResult.getUpdatedAt(); @@ -232,105 +233,19 @@ class Widget extends React.Component { ); } - // eslint-disable-next-line class-methods-use-this - renderWidgetVisualization() { - const { widget, filters } = this.props; - const widgetQueryResult = widget.getQueryResult(); - const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus(); - switch (widgetStatus) { - case 'failed': - return ( -
- {widgetQueryResult.getError() && ( -
- Error running query: {widgetQueryResult.getError()} -
- )} -
- ); - case 'done': - return ( -
- -
- ); - default: - return ( -
-
- -
-
- ); - } - } - - renderWidget() { + render() { const { widget } = this.props; const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); return (
- {this.renderWidgetHeader()} - {this.renderWidgetVisualization()} - {this.renderWidgetBottom()} -
- ); - } - - renderTextbox() { - const { widget, isPublic, canEdit } = this.props; - if (widget.width === 0) { - return null; - } - - return ( -
-
- {(!isPublic && canEdit) && ( - -
-
- -
-
-
-
- } - placement="bottomRight" - trigger={['click']} - > - - -
-
-
- )} -
- - {markdown.toHTML(widget.text)} - -
- ); - } - - render() { - const { widget } = this.props; - - return ( -
- {widget.visualization && this.renderWidget()} - {(!widget.visualization && widget.restricted) && this.renderRestrictedError()} - {(!widget.visualization && !widget.restricted) && this.renderTextbox()} + {this.renderHeader()} + {this.renderVisualization()} + {this.renderBottom()}
); } } -export default Widget; +export default VisualizationWidget; diff --git a/client/app/components/dashboards/dashboard-widget/Widget.jsx b/client/app/components/dashboards/dashboard-widget/Widget.jsx new file mode 100644 index 0000000000..5290a035d3 --- /dev/null +++ b/client/app/components/dashboards/dashboard-widget/Widget.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import recordEvent from '@/services/recordEvent'; +import { FiltersType } from '@/components/Filters'; +import VisualizationWidget from './VisualizationWidget'; +import TextboxWidget from './TextboxWidget'; + +import './Widget.less'; + +class Widget extends React.Component { + static propTypes = { + widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + filters: FiltersType, + isPublic: PropTypes.bool, + canEdit: PropTypes.bool, + onDelete: PropTypes.func, + }; + + static defaultProps = { + filters: [], + isPublic: false, + canEdit: false, + onDelete: () => {}, + }; + + componentDidMount() { + const { widget } = this.props; + recordEvent('view', 'widget', widget.id); + } + + // eslint-disable-next-line class-methods-use-this + renderRestrictedError() { + return ( +
+
+
+

+

+ {'This widget requires access to a data source you don\'t have access to.'} +

+
+
+
+ ); + } + + render() { + const { widget, isPublic, canEdit, onDelete } = this.props; + + return ( +
+ {widget.visualization && } + {(!widget.visualization && widget.restricted) && this.renderRestrictedError()} + {(!widget.visualization && !widget.restricted && widget.width !== 0) && ( + + )} +
+ ); + } +} + +export default Widget; diff --git a/client/app/components/dashboards/dashboard-widget/Widget.less b/client/app/components/dashboards/dashboard-widget/Widget.less new file mode 100644 index 0000000000..5c3f33d9dc --- /dev/null +++ b/client/app/components/dashboards/dashboard-widget/Widget.less @@ -0,0 +1,77 @@ +.tile .t-header .th-title a.query-link { + color: rgba(0, 0, 0, 0.5); +} + +.th-title p.hidden-print { + margin-bottom: 0; +} + +.widget-wrapper { + .body-container { + display: flex; + flex-direction: column; + align-items: stretch; + + .body-row { + flex: 0 1 auto; + } + + .body-row-auto { + flex: 1 1 auto; + } + } + + .spinner-container { + position: relative; + + .spinner { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } + } + + .dropdown-header { + padding: 0; + + .actions { + position: static; + } + } + + .t-header.widget { + .dropdown { + margin-top: -15px; + margin-right: -15px; + + .actions { + position: static; + cursor: pointer; + } + } + } + + .scrollbox:empty { + padding: 0 !important; + font-size: 1px !important; + } + + .widget-text { + :first-child { + margin-top: 0; + } + :last-child { + margin-bottom: 0; + } + + .actions { + cursor: pointer; + } + } +} diff --git a/client/app/components/dashboards/dashboard-widget/index.js b/client/app/components/dashboards/dashboard-widget/index.js new file mode 100644 index 0000000000..cfcef24b9c --- /dev/null +++ b/client/app/components/dashboards/dashboard-widget/index.js @@ -0,0 +1,3 @@ +import Widget from './Widget'; + +export default Widget; diff --git a/client/app/components/dashboards/widget.js b/client/app/components/dashboards/widget.js index b9e2c74251..2dcb7483fa 100644 --- a/client/app/components/dashboards/widget.js +++ b/client/app/components/dashboards/widget.js @@ -2,7 +2,7 @@ import { filter } from 'lodash'; import { angular2react } from 'angular2react'; import template from './widget.html'; import TextboxDialog from '@/components/dashboards/TextboxDialog'; -import ExpandWidgetDialog from '@/components/dashboards/ExpandWidgetDialog'; +import ExpandedWidgetDialog from '@/components/dashboards/ExpandedWidgetDialog'; import EditParameterMappingsDialog from '@/components/dashboards/EditParameterMappingsDialog'; import './widget.less'; @@ -13,7 +13,6 @@ function DashboardWidgetCtrl($scope, $location, $window, $rootScope, $timeout, E this.editTextBox = () => { TextboxDialog.showModal({ - dashboard: this.dashboard, text: this.widget.text, onConfirm: (text) => { this.widget.text = text; @@ -23,7 +22,7 @@ function DashboardWidgetCtrl($scope, $location, $window, $rootScope, $timeout, E }; this.expandVisualization = () => { - ExpandWidgetDialog.showModal({ widget: this.widget }); + ExpandedWidgetDialog.showModal({ widget: this.widget }); }; this.hasParameters = () => this.widget.query.getParametersDefs().length > 0; diff --git a/client/app/components/dashboards/widget.less b/client/app/components/dashboards/widget.less index e6703da066..24234431cf 100644 --- a/client/app/components/dashboards/widget.less +++ b/client/app/components/dashboards/widget.less @@ -74,34 +74,4 @@ cursor: pointer; } } -} - - -// react-grid-layout overrides -.react-grid-item { - - // placeholder color - &.react-grid-placeholder { - border-radius: 3px; - background-color: #E0E6EB; - opacity: 0.5; - } - - // resize placeholder behind widget, the lib's default is above 🤷‍♂️ - &.resizing { - z-index: 3; - } - - // auto-height animation - &.cssTransforms:not(.resizing) { - transition-property: transform, height; // added ", height" - } - - // resize handle size - & > .react-resizable-handle::after { - width: 11px; - height: 11px; - right: 5px; - bottom: 5px; - } } \ No newline at end of file From 45618c389a40245d80f8b2c32459460c397196b4 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sun, 18 Aug 2019 12:44:00 -0300 Subject: [PATCH 13/34] Update with #4056 and trigger netlify --- client/app/components/dashboards/dashboard-widget/Widget.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/app/components/dashboards/dashboard-widget/Widget.jsx b/client/app/components/dashboards/dashboard-widget/Widget.jsx index 5290a035d3..1053b5c2bf 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.jsx +++ b/client/app/components/dashboards/dashboard-widget/Widget.jsx @@ -31,8 +31,8 @@ class Widget extends React.Component { // eslint-disable-next-line class-methods-use-this renderRestrictedError() { return ( -
-
+
+

From 3ba42c05d59efb1a9f4ba1f9f1d75924841ef5d3 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sun, 18 Aug 2019 21:46:17 -0300 Subject: [PATCH 14/34] In progress: use composition --- .../components/dashboards/DashboardGrid.jsx | 42 +++--- .../dashboard-widget/TextboxWidget.jsx | 61 ++------- .../dashboard-widget/VisualizationWidget.jsx | 67 +++++----- .../dashboards/dashboard-widget/Widget.jsx | 120 +++++++++++++----- .../dashboards/dashboard-widget/index.js | 5 +- client/app/services/widget.js | 4 + .../integration/dashboard/textbox_spec.js | 2 +- 7 files changed, 162 insertions(+), 139 deletions(-) diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index e8a51c8bdf..c9cbf56cee 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -5,7 +5,7 @@ import { react2angular } from 'react2angular'; import cx from 'classnames'; import { Responsive, WidthProvider } from 'react-grid-layout'; // import { DashboardWidget } from '@/components/dashboards/widget'; -import DashboardWidget from '@/components/dashboards/dashboard-widget'; +import { VisualizationWidget, TextboxWidget } from '@/components/dashboards/dashboard-widget'; import { FiltersType } from '@/components/Filters'; import cfg from '@/config/dashboard-grid-options'; import AutoHeightController from './AutoHeightController'; @@ -169,7 +169,7 @@ class DashboardGrid extends React.Component { render() { const className = cx('dashboard-wrapper', this.props.isEditing ? 'editing-mode' : 'preview-mode'); - const { onRemoveWidget, dashboard, widgets } = this.props; + const { onRemoveWidget, filters, dashboard, isPublic, widgets } = this.props; return (

@@ -187,24 +187,26 @@ class DashboardGrid extends React.Component { onBreakpointChange={this.onBreakpointChange} breakpoints={{ [MULTI]: cfg.mobileBreakPoint, [SINGLE]: 0 }} > - {widgets.map(widget => ( -
- onRemoveWidget(widget.id)} - isPublic={this.props.isPublic} - canEdit={dashboard.canEdit()} - /> -
- ))} + {widgets.map((widget) => { + const WidgetComponent = widget.visualization ? VisualizationWidget : TextboxWidget; + return ( +
+ onRemoveWidget(widget.id)} + /> +
+ ); + })}
); diff --git a/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx index e7c8c81ca1..6ecbbdb73c 100644 --- a/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx @@ -1,13 +1,13 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { markdown } from 'markdown'; -import Dropdown from 'antd/lib/dropdown'; import Menu from 'antd/lib/menu'; -import Modal from 'antd/lib/modal'; import HtmlContent from '@/components/HtmlContent'; import TextboxDialog from '@/components/dashboards/TextboxDialog'; +import Widget from './Widget'; -function TextboxWidget({ widget, showDropdown, showDeleteButton, onDelete }) { +function TextboxWidget(props) { + const { widget } = props; const [text, setText] = useState(widget.text); const editTextBox = () => { @@ -21,66 +21,31 @@ function TextboxWidget({ widget, showDropdown, showDeleteButton, onDelete }) { }); }; - const deleteTextbox = () => { - Modal.confirm({ - title: 'Delete Textbox', - content: 'Are you sure you want to remove this textbox from the dashboard?', - okText: 'Delete', - okType: 'danger', - onOk: () => widget.delete().then(onDelete), - maskClosable: true, - autoFocusButton: null, - }); - }; + const TextboxMenuOptions = [ + Edit, + ]; - const TextboxMenu = ( - - Edit - Remove from Dashboard - - ); + if (!widget.width) { + return null; + } return ( -
-
- {showDeleteButton && ( -
-
- -
-
- )} - {showDropdown && ( -
-
- - - -
-
- )} -
+ {markdown.toHTML(text || '')} -
+ ); } TextboxWidget.propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types - showDropdown: PropTypes.bool, - showDeleteButton: PropTypes.bool, + canEdit: PropTypes.bool, onDelete: PropTypes.func, }; TextboxWidget.defaultProps = { - showDropdown: false, - showDeleteButton: false, + canEdit: false, onDelete: () => {}, }; diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index dfd64b4249..9467f3065f 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { filter, isEmpty } from 'lodash'; import { markdown } from 'markdown'; import classNames from 'classnames'; -import Dropdown from 'antd/lib/dropdown'; import Menu from 'antd/lib/menu'; import Modal from 'antd/lib/modal'; import { currentUser } from '@/services/auth'; @@ -18,18 +17,28 @@ import QueryLink from '@/components/QueryLink'; import { FiltersType } from '@/components/Filters'; import ExpandedWidgetDialog from '@/components/dashboards/ExpandedWidgetDialog'; import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer'; +import Widget from './Widget'; -function VisualizationWidgetMenu(props) { +const VisualizationWidgetMenuOptions = [ + Download as CSV File, + Download as Excel File, + , + View Query, + Edit Parameters, +]; + +function RestrictedWidget(props) { return ( - - Download as CSV File - Download as Excel File - - View Query - Edit Parameters - - Remove from Dashboard - + +
+
+

+

+ {'This widget requires access to a data source you don\'t have access to.'} +

+
+
+
); } @@ -104,7 +113,7 @@ class VisualizationWidget extends React.Component { } renderHeader() { - const { widget, isPublic, canEdit } = this.props; + const { widget } = this.props; const canViewQuery = currentUser.hasPermission('view_query'); const localParameters = filter( @@ -115,26 +124,6 @@ class VisualizationWidget extends React.Component { return (
- {(!isPublic && canEdit) && ( - <> -
-
- -
-
-
-
- } - placement="bottomRight" - trigger={['click']} - > - - -
-
- - )} {widget.loading && this.renderRefreshIndicator()}

@@ -238,13 +227,17 @@ class VisualizationWidget extends React.Component { const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); - return ( -

- {this.renderHeader()} + return !widget.restricted ? ( + {this.renderVisualization()} {this.renderBottom()} -
- ); + + ) : ; } } diff --git a/client/app/components/dashboards/dashboard-widget/Widget.jsx b/client/app/components/dashboards/dashboard-widget/Widget.jsx index 1053b5c2bf..c27d10b261 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.jsx +++ b/client/app/components/dashboards/dashboard-widget/Widget.jsx @@ -1,25 +1,82 @@ import React from 'react'; import PropTypes from 'prop-types'; +import cx from 'classnames'; +import Dropdown from 'antd/lib/dropdown'; +import Modal from 'antd/lib/modal'; +import Menu from 'antd/lib/menu'; import recordEvent from '@/services/recordEvent'; import { FiltersType } from '@/components/Filters'; -import VisualizationWidget from './VisualizationWidget'; -import TextboxWidget from './TextboxWidget'; import './Widget.less'; +function WidgetDropdownButton({ extraOptions, showDeleteOption, onDelete, ...otherProps }) { + const WidgetMenu = ( + + {extraOptions} + {(showDeleteOption && extraOptions) && } + {showDeleteOption && Remove from Dashboard} + + ); + + return ( +
+
+ + + +
+
+ ); +} + +WidgetDropdownButton.propTypes = { + extraOptions: PropTypes.node, + showDeleteOption: PropTypes.bool, + onDelete: PropTypes.func, +}; + +WidgetDropdownButton.defaultProps = { + extraOptions: null, + showDeleteOption: false, + onDelete: () => {}, +}; + +function WidgetDeleteButton({ onClick }) { + return ( +
+
+ +
+
+ ); +} + +WidgetDeleteButton.propTypes = { onClick: PropTypes.func }; +WidgetDeleteButton.defaultProps = { onClick: () => {} }; + class Widget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + className: PropTypes.string, + children: PropTypes.node, filters: FiltersType, - isPublic: PropTypes.bool, canEdit: PropTypes.bool, + isPublic: PropTypes.bool, + menuOptions: PropTypes.node, onDelete: PropTypes.func, }; static defaultProps = { + className: '', + children: null, filters: [], - isPublic: false, canEdit: false, + isPublic: false, + menuOptions: null, onDelete: () => {}, }; @@ -28,37 +85,40 @@ class Widget extends React.Component { recordEvent('view', 'widget', widget.id); } - // eslint-disable-next-line class-methods-use-this - renderRestrictedError() { - return ( -
-
-
-

-

- {'This widget requires access to a data source you don\'t have access to.'} -

-
-
-
- ); - } + 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', + onOk: () => widget.delete().then(onDelete), + maskClosable: true, + autoFocusButton: null, + }); + }; render() { - const { widget, isPublic, canEdit, onDelete } = this.props; + const { className, children, filters, canEdit, isPublic, menuOptions, onDelete, ...otherProps } = this.props; return (
- {widget.visualization && } - {(!widget.visualization && widget.restricted) && this.renderRestrictedError()} - {(!widget.visualization && !widget.restricted && widget.width !== 0) && ( - - )} +
+
+
+ {canEdit && } + {!isPublic && ( + + )} +
+
+ {children} +
); } diff --git a/client/app/components/dashboards/dashboard-widget/index.js b/client/app/components/dashboards/dashboard-widget/index.js index cfcef24b9c..98fd9d415d 100644 --- a/client/app/components/dashboards/dashboard-widget/index.js +++ b/client/app/components/dashboards/dashboard-widget/index.js @@ -1,3 +1,2 @@ -import Widget from './Widget'; - -export default Widget; +export { default as VisualizationWidget } from './VisualizationWidget'; +export { default as TextboxWidget } from './TextboxWidget'; diff --git a/client/app/services/widget.js b/client/app/services/widget.js index 8816c7762f..741418cf6d 100644 --- a/client/app/services/widget.js +++ b/client/app/services/widget.js @@ -112,6 +112,10 @@ function WidgetFactory($http, $location, Query) { return truncate(this.text, 20); } + getType() { + return this.visualization ? 'visualization' : 'textbox'; + } + load(force, maxAge) { if (!this.visualization) { return Promise.resolve(); diff --git a/client/cypress/integration/dashboard/textbox_spec.js b/client/cypress/integration/dashboard/textbox_spec.js index 18460e2b6d..d5aa8000ef 100644 --- a/client/cypress/integration/dashboard/textbox_spec.js +++ b/client/cypress/integration/dashboard/textbox_spec.js @@ -21,7 +21,7 @@ describe('Textbox', () => { }); cy.contains('button', 'Add to Dashboard').click(); cy.getByTestId('TextboxDialog').should('not.exist'); - cy.get('.textbox').should('exist'); + cy.get('.widget-text').should('exist'); }); it('removes textbox by X button', function () { From a7f6d41f72098268e8311f062f6586be0f225e5a Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 21 Aug 2019 21:08:56 -0300 Subject: [PATCH 15/34] Add header and footer --- .../dashboards/AutoHeightController.js | 1 + .../dashboard-widget/VisualizationWidget.jsx | 87 ++++++++----------- .../dashboards/dashboard-widget/Widget.jsx | 39 +++++++-- 3 files changed, 71 insertions(+), 56 deletions(-) diff --git a/client/app/components/dashboards/AutoHeightController.js b/client/app/components/dashboards/AutoHeightController.js index 4763dcb999..07c1e2e135 100644 --- a/client/app/components/dashboards/AutoHeightController.js +++ b/client/app/components/dashboards/AutoHeightController.js @@ -5,6 +5,7 @@ import { includes, reduce, some } from 'lodash'; const WIDGET_SELECTOR = '[data-widgetid="{0}"]'; const WIDGET_CONTENT_SELECTOR = [ '.widget-header', // header + '.widget-parameters', // parameters '.visualization-renderer', // visualization '.scrollbox .alert', // error state '.spinner-container', // loading state diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index 9467f3065f..eda23991c4 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -11,7 +11,6 @@ import { $location } from '@/services/ng'; import { formatDateTime } from '@/filters/datetime'; import HtmlContent from '@/components/HtmlContent'; import { Parameters } from '@/components/Parameters'; -import { Timer } from '@/components/Timer'; import { TimeAgo } from '@/components/TimeAgo'; import QueryLink from '@/components/QueryLink'; import { FiltersType } from '@/components/Filters'; @@ -42,6 +41,27 @@ function RestrictedWidget(props) { ); } +function VisualizationWidgetHeader({ widget }) { + const canViewQuery = currentUser.hasPermission('view_query'); + + return ( + <> +
+

+ +

+ + {markdown.toHTML(widget.getQuery().description || '')} + +
+ + ); +} + +VisualizationWidgetHeader.propTypes = { + widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types +}; + class VisualizationWidget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types @@ -100,49 +120,6 @@ class VisualizationWidget extends React.Component { } }; - renderRefreshIndicator() { - const { widget } = this.props; - return ( -
-
- -
- -
- ); - } - - renderHeader() { - const { widget } = this.props; - const canViewQuery = currentUser.hasPermission('view_query'); - - const localParameters = filter( - widget.getParametersDefs(), - param => !widget.isStaticParam(param), - ); - - return ( -
-
- {widget.loading && this.renderRefreshIndicator()} -
-

- -

- - {markdown.toHTML(widget.getQuery().description || '')} - -
-
- {!isEmpty(localParameters) && ( -
- -
- )} -
- ); - } - // eslint-disable-next-line class-methods-use-this renderVisualization() { const { widget, filters } = this.props; @@ -161,7 +138,7 @@ class VisualizationWidget extends React.Component { ); case 'done': return ( -
+
+ <> {(!isPublic && !!widgetQueryResult) && ( -
+ ); } @@ -226,16 +203,28 @@ class VisualizationWidget extends React.Component { const { widget } = this.props; const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); + const localParameters = filter( + widget.getParametersDefs(), + param => !widget.isStaticParam(param), + ); return !widget.restricted ? ( } + footer={this.renderBottom()} + refreshStartedAt={isRefreshing ? widget.refreshStartedAt : null} > +
+ {!isEmpty(localParameters) && ( +
+ +
+ )} +
{this.renderVisualization()} - {this.renderBottom()}
) : ; } diff --git a/client/app/components/dashboards/dashboard-widget/Widget.jsx b/client/app/components/dashboards/dashboard-widget/Widget.jsx index c27d10b261..bae78dd3f6 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.jsx +++ b/client/app/components/dashboards/dashboard-widget/Widget.jsx @@ -5,7 +5,8 @@ import Dropdown from 'antd/lib/dropdown'; import Modal from 'antd/lib/modal'; import Menu from 'antd/lib/menu'; import recordEvent from '@/services/recordEvent'; -import { FiltersType } from '@/components/Filters'; +import { Moment } from '@/components/proptypes'; +import { Timer } from '@/components/Timer'; import './Widget.less'; @@ -19,7 +20,7 @@ function WidgetDropdownButton({ extraOptions, showDeleteOption, onDelete, ...oth ); return ( -
+
+
@@ -58,14 +59,29 @@ function WidgetDeleteButton({ onClick }) { WidgetDeleteButton.propTypes = { onClick: PropTypes.func }; WidgetDeleteButton.defaultProps = { onClick: () => {} }; +function RefreshIndicator({ refreshStartedAt }) { + return ( +
+
+ +
+ +
+ ); +} + +RefreshIndicator.propTypes = { refreshStartedAt: Moment.isRequired }; + class Widget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types className: PropTypes.string, children: PropTypes.node, - filters: FiltersType, + header: PropTypes.node, + footer: PropTypes.node, canEdit: PropTypes.bool, isPublic: PropTypes.bool, + refreshStartedAt: Moment, menuOptions: PropTypes.node, onDelete: PropTypes.func, }; @@ -73,9 +89,11 @@ class Widget extends React.Component { static defaultProps = { className: '', children: null, - filters: [], + header: null, + footer: null, canEdit: false, isPublic: false, + refreshStartedAt: null, menuOptions: null, onDelete: () => {}, }; @@ -100,11 +118,11 @@ class Widget extends React.Component { }; render() { - const { className, children, filters, canEdit, isPublic, menuOptions, onDelete, ...otherProps } = this.props; + const { className, children, header, footer, canEdit, isPublic, refreshStartedAt, menuOptions } = this.props; return (
-
+
{canEdit && } @@ -115,9 +133,16 @@ class Widget extends React.Component { onDelete={this.deleteWidget} /> )} + {refreshStartedAt && } + {header}
{children} + {footer && ( +
+ {footer} +
+ )}
); From 5021b44326555ab4056c6797200ed694432fd38c Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 22 Aug 2019 08:17:54 -0300 Subject: [PATCH 16/34] Update widget actions positioning --- .../dashboards/AutoHeightController.js | 1 - .../dashboard-widget/VisualizationWidget.jsx | 43 ++++++++-------- .../dashboards/dashboard-widget/Widget.jsx | 50 +++++++++---------- .../dashboards/dashboard-widget/Widget.less | 43 +++++++--------- 4 files changed, 66 insertions(+), 71 deletions(-) diff --git a/client/app/components/dashboards/AutoHeightController.js b/client/app/components/dashboards/AutoHeightController.js index 07c1e2e135..4763dcb999 100644 --- a/client/app/components/dashboards/AutoHeightController.js +++ b/client/app/components/dashboards/AutoHeightController.js @@ -5,7 +5,6 @@ import { includes, reduce, some } from 'lodash'; const WIDGET_SELECTOR = '[data-widgetid="{0}"]'; const WIDGET_CONTENT_SELECTOR = [ '.widget-header', // header - '.widget-parameters', // parameters '.visualization-renderer', // visualization '.scrollbox .alert', // error state '.spinner-container', // loading state diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index eda23991c4..e2c6885b39 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -41,27 +41,41 @@ function RestrictedWidget(props) { ); } -function VisualizationWidgetHeader({ widget }) { +function VisualizationWidgetHeader({ widget, onParametersUpdate }) { const canViewQuery = currentUser.hasPermission('view_query'); + const localParameters = filter( + widget.getParametersDefs(), + param => !widget.isStaticParam(param), + ); return ( <> -
-

- -

- - {markdown.toHTML(widget.getQuery().description || '')} - +
+
+

+ +

+ + {markdown.toHTML(widget.getQuery().description || '')} + +
+ {!isEmpty(localParameters) && ( +
+ +
+ )} ); } VisualizationWidgetHeader.propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + onParametersUpdate: PropTypes.func, }; +VisualizationWidgetHeader.defaultProps = { onParametersUpdate: () => {} }; + class VisualizationWidget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types @@ -203,27 +217,16 @@ class VisualizationWidget extends React.Component { const { widget } = this.props; const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); - const localParameters = filter( - widget.getParametersDefs(), - param => !widget.isStaticParam(param), - ); return !widget.restricted ? ( } + header={} footer={this.renderBottom()} refreshStartedAt={isRefreshing ? widget.refreshStartedAt : null} > -
- {!isEmpty(localParameters) && ( -
- -
- )} -
{this.renderVisualization()}
) : ; diff --git a/client/app/components/dashboards/dashboard-widget/Widget.jsx b/client/app/components/dashboards/dashboard-widget/Widget.jsx index bae78dd3f6..c4fbf164ec 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.jsx +++ b/client/app/components/dashboards/dashboard-widget/Widget.jsx @@ -20,16 +20,14 @@ function WidgetDropdownButton({ extraOptions, showDeleteOption, onDelete, ...oth ); return ( -
-
- - - -
+
+ + +
); } @@ -48,10 +46,10 @@ WidgetDropdownButton.defaultProps = { function WidgetDeleteButton({ onClick }) { return ( -
-
- -
+ ); } @@ -123,19 +121,19 @@ class Widget extends React.Component { return (
+
+ {!isPublic && ( + + )} + {canEdit && } +
-
- {canEdit && } - {!isPublic && ( - - )} - {refreshStartedAt && } - {header} -
+ {refreshStartedAt && } + {header}
{children} {footer && ( diff --git a/client/app/components/dashboards/dashboard-widget/Widget.less b/client/app/components/dashboards/dashboard-widget/Widget.less index 5c3f33d9dc..1c4eed1d82 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.less +++ b/client/app/components/dashboards/dashboard-widget/Widget.less @@ -7,6 +7,25 @@ } .widget-wrapper { + .widget-actions { + position: absolute; + top: 0; + right: 0; + z-index: 1; + + .action { + font-size: 24px; + cursor: pointer; + line-height: 100%; + display: block; + padding: 4px 10px 3px; + } + + .action:hover { + background-color: rgba(0, 0, 0, 0.1); + } + } + .body-container { display: flex; flex-direction: column; @@ -37,26 +56,6 @@ } } - .dropdown-header { - padding: 0; - - .actions { - position: static; - } - } - - .t-header.widget { - .dropdown { - margin-top: -15px; - margin-right: -15px; - - .actions { - position: static; - cursor: pointer; - } - } - } - .scrollbox:empty { padding: 0 !important; font-size: 1px !important; @@ -69,9 +68,5 @@ :last-child { margin-bottom: 0; } - - .actions { - cursor: pointer; - } } } From a34ad8552a26140c02a7c14e1671c094f87694de Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 22 Aug 2019 13:05:28 -0300 Subject: [PATCH 17/34] Re-render when refreshing from widget --- .../dashboard-widget/VisualizationWidget.jsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index e2c6885b39..afc52cf6eb 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -92,6 +92,13 @@ class VisualizationWidget extends React.Component { onDelete: () => {}, }; + constructor(props) { + super(props); + const widgetQueryResult = props.widget.getQueryResult(); + const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus(); + this.state = { refreshClickButtonId: null, widgetStatus }; + } + state = { refreshClickButtonId: null, }; @@ -106,7 +113,11 @@ class VisualizationWidget extends React.Component { loadWidget = (refresh = false) => { const { widget } = this.props; const maxAge = $location.search().maxAge; - return widget.load(refresh, maxAge); + return widget.load(refresh, maxAge).then(({ status }) => { + this.setState({ widgetStatus: status }); + }).catch(() => { + this.setState({ widgetStatus: 'failed' }); + }); }; expandWidget = () => { @@ -137,8 +148,8 @@ class VisualizationWidget extends React.Component { // eslint-disable-next-line class-methods-use-this renderVisualization() { const { widget, filters } = this.props; + const { widgetStatus } = this.state; const widgetQueryResult = widget.getQueryResult(); - const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus(); switch (widgetStatus) { case 'failed': return ( From 71f6e8f6965bfb746d3fc2988a7b7a819b407ca5 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sat, 24 Aug 2019 14:53:51 -0300 Subject: [PATCH 18/34] Add workaround to force DashboardGrid re-render --- client/app/components/dashboards/DashboardGrid.jsx | 4 ++++ client/app/pages/dashboards/dashboard.html | 1 + client/app/pages/dashboards/dashboard.js | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index c9cbf56cee..85a5ef0899 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -44,6 +44,9 @@ class DashboardGrid extends React.Component { onBreakpointChange: PropTypes.func, onRemoveWidget: PropTypes.func, onLayoutChange: PropTypes.func, + // Force component update when widgets are refreshing. + // Remove this when Dashboard is migrated to React + refreshingWidgets: PropTypes.number, // eslint-disable-line react/no-unused-prop-types }; static defaultProps = { @@ -52,6 +55,7 @@ class DashboardGrid extends React.Component { onRemoveWidget: () => {}, onLayoutChange: () => {}, onBreakpointChange: () => {}, + refreshingWidgets: 0, }; static normalizeFrom(widget) { diff --git a/client/app/pages/dashboards/dashboard.html b/client/app/pages/dashboards/dashboard.html index cc89ae3e61..b9657238f6 100644 --- a/client/app/pages/dashboards/dashboard.html +++ b/client/app/pages/dashboards/dashboard.html @@ -108,6 +108,7 @@

widgets="$ctrl.dashboard.widgets" filters="$ctrl.filters" is-editing="$ctrl.layoutEditing && !$ctrl.isGridDisabled" + refreshing-widgets="$ctrl.refreshingWidgets" on-layout-change="$ctrl.onLayoutChange" on-breakpoint-change="$ctrl.onBreakpointChanged" on-remove-widget="$ctrl.removeWidget" diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index f283c3594b..09acc92b54 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -105,6 +105,7 @@ function DashboardCtrl( this.globalParameters = []; this.isDashboardOwner = false; this.filters = []; + this.refreshingWidgets = 0; this.refreshRates = clientConfig.dashboardRefreshIntervals.map(interval => ({ name: durationHumanize(interval), @@ -140,7 +141,8 @@ function DashboardCtrl( const collectFilters = (dashboard, forceRefresh) => { const queryResultPromises = _.compact(this.dashboard.widgets.map((widget) => { widget.getParametersDefs(); // Force widget to read parameters values from URL - return widget.load(forceRefresh); + this.refreshingWidgets += 1; + return widget.load(forceRefresh).finally(() => { this.refreshingWidgets -= 1; }); })); return $q.all(queryResultPromises).then((queryResults) => { From 1622f84cc4413d485fa3f2cc38597536d867381b Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sat, 24 Aug 2019 21:06:03 -0300 Subject: [PATCH 19/34] VisualizationWidgetFooter component --- .../dashboard-widget/VisualizationWidget.jsx | 126 ++++++++++-------- 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index afc52cf6eb..b321388464 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { filter, isEmpty } from 'lodash'; import { markdown } from 'markdown'; @@ -76,6 +76,64 @@ VisualizationWidgetHeader.propTypes = { VisualizationWidgetHeader.defaultProps = { onParametersUpdate: () => {} }; +function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) { + const widgetQueryResult = widget.getQueryResult(); + const updatedAt = widgetQueryResult && widgetQueryResult.getUpdatedAt(); + const [refreshClickButtonId, setRefreshClickButtonId] = useState(); + + const refreshWidget = (buttonId) => { + if (!refreshClickButtonId) { + setRefreshClickButtonId(buttonId); + onRefresh().finally(() => setRefreshClickButtonId(null)); + } + }; + + return ( + <> + {(!isPublic && !!widgetQueryResult) && ( + refreshWidget(1)} + data-test="RefreshButton" + > + {' '} + + + )} + + {' '}{formatDateTime(updatedAt)} + + {isPublic ? ( + + {' '} + + ) : ( + refreshWidget(2)} + > + + + )} + + + + + ); +} + +VisualizationWidgetFooter.propTypes = { + widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + isPublic: PropTypes.bool, + onRefresh: PropTypes.func.isRequired, + onExpand: PropTypes.func.isRequired, +}; + +VisualizationWidgetFooter.defaultProps = { isPublic: false }; + class VisualizationWidget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types @@ -96,13 +154,9 @@ class VisualizationWidget extends React.Component { super(props); const widgetQueryResult = props.widget.getQueryResult(); const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus(); - this.state = { refreshClickButtonId: null, widgetStatus }; + this.state = { widgetStatus }; } - state = { - refreshClickButtonId: null, - }; - componentDidMount() { const { widget } = this.props; recordEvent('view', 'query', widget.visualization.query.id, { dashboard: true }); @@ -138,13 +192,6 @@ class VisualizationWidget extends React.Component { }); }; - refreshWidget = (refreshClickButtonId) => { - if (!this.state.refreshClickButtonId) { - this.setState({ refreshClickButtonId }); - this.loadWidget(true).finally(() => this.setState({ refreshClickButtonId: null })); - } - }; - // eslint-disable-next-line class-methods-use-this renderVisualization() { const { widget, filters } = this.props; @@ -182,50 +229,8 @@ class VisualizationWidget extends React.Component { } } - renderBottom() { - const { widget, isPublic } = this.props; - const widgetQueryResult = widget.getQueryResult(); - const updatedAt = widgetQueryResult && widgetQueryResult.getUpdatedAt(); - const { refreshClickButtonId } = this.state; - return ( - <> - {(!isPublic && !!widgetQueryResult) && ( - this.refreshWidget(1)} - data-test="RefreshButton" - > - {' '} - - - )} - - {' '}{formatDateTime(updatedAt)} - - {isPublic ? ( - - {' '} - - ) : ( - this.refreshWidget(2)} - > - - - )} - - - - - ); - } - render() { - const { widget } = this.props; + const { widget, isPublic } = this.props; const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); @@ -235,7 +240,14 @@ class VisualizationWidget extends React.Component { className="widget-visualization" menuOptions={VisualizationWidgetMenuOptions} header={} - footer={this.renderBottom()} + footer={( + this.loadWidget(true)} + onExpand={this.expandWidget} + /> + )} refreshStartedAt={isRefreshing ? widget.refreshStartedAt : null} > {this.renderVisualization()} From a5ab07df1e64d84f66fd75762f5035dcb5c2925d Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 27 Aug 2019 10:27:06 -0300 Subject: [PATCH 20/34] VisualizationWidget menu --- .../components/dashboards/DashboardGrid.jsx | 27 ++++-- .../dashboards/ExpandedWidgetDialog.jsx | 2 +- .../dashboard-widget/VisualizationWidget.jsx | 96 +++++++++++++++---- client/app/pages/dashboards/dashboard.html | 1 + client/app/pages/dashboards/dashboard.js | 4 - client/app/services/widget.js | 7 ++ 6 files changed, 102 insertions(+), 35 deletions(-) diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index 85a5ef0899..f5041b65a0 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -4,7 +4,6 @@ import { chain, cloneDeep, find } from 'lodash'; import { react2angular } from 'react2angular'; import cx from 'classnames'; import { Responsive, WidthProvider } from 'react-grid-layout'; -// import { DashboardWidget } from '@/components/dashboards/widget'; import { VisualizationWidget, TextboxWidget } from '@/components/dashboards/dashboard-widget'; import { FiltersType } from '@/components/Filters'; import cfg from '@/config/dashboard-grid-options'; @@ -44,6 +43,7 @@ class DashboardGrid extends React.Component { onBreakpointChange: PropTypes.func, onRemoveWidget: PropTypes.func, onLayoutChange: PropTypes.func, + onParameterMappingsChange: PropTypes.func, // Force component update when widgets are refreshing. // Remove this when Dashboard is migrated to React refreshingWidgets: PropTypes.number, // eslint-disable-line react/no-unused-prop-types @@ -55,6 +55,7 @@ class DashboardGrid extends React.Component { onRemoveWidget: () => {}, onLayoutChange: () => {}, onBreakpointChange: () => {}, + onParameterMappingsChange: () => {}, refreshingWidgets: 0, }; @@ -173,7 +174,7 @@ class DashboardGrid extends React.Component { render() { const className = cx('dashboard-wrapper', this.props.isEditing ? 'editing-mode' : 'preview-mode'); - const { onRemoveWidget, filters, dashboard, isPublic, widgets } = this.props; + const { onRemoveWidget, onParameterMappingsChange, filters, dashboard, isPublic, widgets } = this.props; return (
@@ -192,7 +193,13 @@ class DashboardGrid extends React.Component { breakpoints={{ [MULTI]: cfg.mobileBreakPoint, [SINGLE]: 0 }} > {widgets.map((widget) => { - const WidgetComponent = widget.visualization ? VisualizationWidget : TextboxWidget; + const widgetProps = { + widget, + filters, + isPublic, + canEdit: dashboard.canEdit(), + onDelete: () => onRemoveWidget(widget.id), + }; return (
- onRemoveWidget(widget.id)} - /> + {widget.visualization ? ( + + ) : }
); })} diff --git a/client/app/components/dashboards/ExpandedWidgetDialog.jsx b/client/app/components/dashboards/ExpandedWidgetDialog.jsx index 49756a1d30..bfc96a84bc 100644 --- a/client/app/components/dashboards/ExpandedWidgetDialog.jsx +++ b/client/app/components/dashboards/ExpandedWidgetDialog.jsx @@ -16,7 +16,7 @@ function ExpandedWidgetDialog({ dialog, widget }) { {widget.getQuery().name} )} - width={900} + width="95%" footer={()} > diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index b321388464..d0d4fe0031 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { filter, isEmpty } from 'lodash'; +import { compact, isEmpty, invoke } from 'lodash'; import { markdown } from 'markdown'; import classNames from 'classnames'; import Menu from 'antd/lib/menu'; @@ -15,16 +15,49 @@ import { TimeAgo } from '@/components/TimeAgo'; 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'; -const VisualizationWidgetMenuOptions = [ - Download as CSV File, - Download as Excel File, - , - View Query, - Edit Parameters, -]; +function visualizationWidgetMenuOptions(widget, canEditDashboard, onParametersEdit) { + const canViewQuery = currentUser.hasPermission('view_query'); + const canEditParameters = canEditDashboard && !isEmpty(invoke(widget, 'query.getParametersDefs')); + const widgetQueryResult = widget.getQueryResult(); + const isQueryResultEmpty = !widgetQueryResult || !widgetQueryResult.isEmpty || widgetQueryResult.isEmpty(); + + const downloadLink = fileType => widgetQueryResult.getLink(widget.getQuery().id, fileType); + const downloadName = fileType => widgetQueryResult.getName(widget.getQuery().name, fileType); + return compact([ + + {!isQueryResultEmpty ? ( + + Download as CSV File + + ) : 'Download as CSV File'} + , + + {!isQueryResultEmpty ? ( + + Download as Excel File + + ) : 'Download as Excel File'} + , + ((canViewQuery || canEditParameters) && ), + canViewQuery && ( + + View Query + + ), + (canEditParameters && ( + + Edit Parameters + + )), + ]); +} function RestrictedWidget(props) { return ( @@ -41,12 +74,8 @@ function RestrictedWidget(props) { ); } -function VisualizationWidgetHeader({ widget, onParametersUpdate }) { +function VisualizationWidgetHeader({ widget, parameters, onParametersUpdate }) { const canViewQuery = currentUser.hasPermission('view_query'); - const localParameters = filter( - widget.getParametersDefs(), - param => !widget.isStaticParam(param), - ); return ( <> @@ -60,9 +89,9 @@ function VisualizationWidgetHeader({ widget, onParametersUpdate }) {

- {!isEmpty(localParameters) && ( + {!isEmpty(parameters) && (
- +
)} @@ -71,10 +100,11 @@ function VisualizationWidgetHeader({ widget, onParametersUpdate }) { VisualizationWidgetHeader.propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + parameters: PropTypes.arrayOf(PropTypes.object), onParametersUpdate: PropTypes.func, }; -VisualizationWidgetHeader.defaultProps = { onParametersUpdate: () => {} }; +VisualizationWidgetHeader.defaultProps = { onParametersUpdate: () => {}, parameters: [] }; function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) { const widgetQueryResult = widget.getQueryResult(); @@ -137,10 +167,12 @@ VisualizationWidgetFooter.defaultProps = { isPublic: false }; class VisualizationWidget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types filters: FiltersType, isPublic: PropTypes.bool, canEdit: PropTypes.bool, onDelete: PropTypes.func, + onParameterMappingsChange: PropTypes.func, }; static defaultProps = { @@ -148,13 +180,15 @@ class VisualizationWidget extends React.Component { isPublic: false, canEdit: false, onDelete: () => {}, + onParameterMappingsChange: () => {}, }; constructor(props) { super(props); const widgetQueryResult = props.widget.getQueryResult(); const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus(); - this.state = { widgetStatus }; + + this.state = { widgetStatus, localParameters: props.widget.getLocalParameters() }; } componentDidMount() { @@ -192,6 +226,21 @@ class VisualizationWidget extends React.Component { }); }; + editParameterMappings = () => { + const { widget, dashboard, onParameterMappingsChange } = this.props; + EditParameterMappingsDialog.showModal({ + dashboard, + widget, + }).result.then((valuesChanged) => { + // refresh widget if any parameter value has been updated + if (valuesChanged) { + this.refresh(); + } + onParameterMappingsChange(); + this.setState({ localParameters: widget.getLocalParameters() }); + }); + }; + // eslint-disable-next-line class-methods-use-this renderVisualization() { const { widget, filters } = this.props; @@ -230,7 +279,8 @@ class VisualizationWidget extends React.Component { } render() { - const { widget, isPublic } = this.props; + const { widget, isPublic, canEdit } = this.props; + const { localParameters } = this.state; const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); @@ -238,8 +288,14 @@ class VisualizationWidget extends React.Component { } + menuOptions={visualizationWidgetMenuOptions(widget, canEdit, this.editParameterMappings)} + header={( + + )} footer={( on-layout-change="$ctrl.onLayoutChange" on-breakpoint-change="$ctrl.onBreakpointChanged" on-remove-widget="$ctrl.removeWidget" + on-parameter-mappings-change="$ctrl.extractGlobalParameters" />
diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index 09acc92b54..60287e53b5 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -134,10 +134,6 @@ function DashboardCtrl( this.globalParameters = this.dashboard.getParametersDefs(); }; - $scope.$on('dashboard.update-parameters', () => { - this.extractGlobalParameters(); - }); - const collectFilters = (dashboard, forceRefresh) => { const queryResultPromises = _.compact(this.dashboard.widgets.map((widget) => { widget.getParametersDefs(); // Force widget to read parameters values from URL diff --git a/client/app/services/widget.js b/client/app/services/widget.js index 741418cf6d..1a604c5393 100644 --- a/client/app/services/widget.js +++ b/client/app/services/widget.js @@ -243,6 +243,13 @@ function WidgetFactory($http, $location, Query) { return this.options.parameterMappings; } + + getLocalParameters() { + return filter( + this.getParametersDefs(), + param => !this.isStaticParam(param), + ); + } } return WidgetService; From 5f66382db036db1ec7272317afa605fd3181a0b0 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 27 Aug 2019 11:30:55 -0300 Subject: [PATCH 21/34] Separate RestrictedWidget --- .../components/dashboards/DashboardGrid.jsx | 8 +++++--- .../dashboard-widget/RestrictedWidget.jsx | 19 +++++++++++++++++++ .../dashboard-widget/TextboxWidget.jsx | 6 ++---- .../dashboard-widget/VisualizationWidget.jsx | 19 ++----------------- .../dashboards/dashboard-widget/Widget.jsx | 5 +++-- .../dashboards/dashboard-widget/index.js | 1 + client/app/services/widget.js | 7 ++++++- 7 files changed, 38 insertions(+), 27 deletions(-) create mode 100644 client/app/components/dashboards/dashboard-widget/RestrictedWidget.jsx diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index f5041b65a0..704a12c317 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -4,7 +4,7 @@ import { chain, cloneDeep, find } from 'lodash'; import { react2angular } from 'react2angular'; import cx from 'classnames'; import { Responsive, WidthProvider } from 'react-grid-layout'; -import { VisualizationWidget, TextboxWidget } from '@/components/dashboards/dashboard-widget'; +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'; @@ -208,13 +208,15 @@ class DashboardGrid extends React.Component { data-test={`WidgetId${widget.id}`} className={cx('dashboard-widget-wrapper', { 'widget-auto-height-enabled': this.autoHeightCtrl.exists(widget.id) })} > - {widget.visualization ? ( + {widget.getType() === 'visualization' && ( - ) : } + )} + {widget.getType() === 'textbox' && } + {widget.getType() === 'restricted' && }
); })} diff --git a/client/app/components/dashboards/dashboard-widget/RestrictedWidget.jsx b/client/app/components/dashboards/dashboard-widget/RestrictedWidget.jsx new file mode 100644 index 0000000000..6033f08ebd --- /dev/null +++ b/client/app/components/dashboards/dashboard-widget/RestrictedWidget.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import Widget from './Widget'; + +function RestrictedWidget(props) { + return ( + +
+
+

+

+ {'This widget requires access to a data source you don\'t have access to.'} +

+
+
+
+ ); +} + +export default RestrictedWidget; diff --git a/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx index 6ecbbdb73c..9fd14a333d 100644 --- a/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx @@ -7,7 +7,7 @@ import TextboxDialog from '@/components/dashboards/TextboxDialog'; import Widget from './Widget'; function TextboxWidget(props) { - const { widget } = props; + const { widget, canEdit } = props; const [text, setText] = useState(widget.text); const editTextBox = () => { @@ -30,7 +30,7 @@ function TextboxWidget(props) { } return ( - + {markdown.toHTML(text || '')} @@ -41,12 +41,10 @@ function TextboxWidget(props) { TextboxWidget.propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types canEdit: PropTypes.bool, - onDelete: PropTypes.func, }; TextboxWidget.defaultProps = { canEdit: false, - onDelete: () => {}, }; export default TextboxWidget; diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index d0d4fe0031..bb26fb15d1 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -59,21 +59,6 @@ function visualizationWidgetMenuOptions(widget, canEditDashboard, onParametersEd ]); } -function RestrictedWidget(props) { - return ( - -
-
-

-

- {'This widget requires access to a data source you don\'t have access to.'} -

-
-
-
- ); -} - function VisualizationWidgetHeader({ widget, parameters, onParametersUpdate }) { const canViewQuery = currentUser.hasPermission('view_query'); @@ -284,7 +269,7 @@ class VisualizationWidget extends React.Component { const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); - return !widget.restricted ? ( + return ( {this.renderVisualization()} - ) : ; + ); } } diff --git a/client/app/components/dashboards/dashboard-widget/Widget.jsx b/client/app/components/dashboards/dashboard-widget/Widget.jsx index c4fbf164ec..a12aa630a0 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.jsx +++ b/client/app/components/dashboards/dashboard-widget/Widget.jsx @@ -1,6 +1,7 @@ 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'; @@ -117,12 +118,12 @@ class Widget extends React.Component { render() { const { className, children, header, footer, canEdit, isPublic, refreshStartedAt, menuOptions } = this.props; - + const showDropdownButton = !isPublic && (canEdit || !isEmpty(menuOptions)); return (
- {!isPublic && ( + {showDropdownButton && ( Date: Tue, 27 Aug 2019 13:40:03 -0300 Subject: [PATCH 22/34] Update tests --- .../dashboard-widget/VisualizationWidget.jsx | 19 ++----- .../dashboards/dashboard-widget/Widget.jsx | 8 +-- .../integration/dashboard/textbox_spec.js | 51 +++++++++++-------- .../integration/dashboard/widget_spec.js | 12 +++-- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index bb26fb15d1..88f27e9320 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -4,7 +4,6 @@ import { compact, isEmpty, invoke } from 'lodash'; import { markdown } from 'markdown'; import classNames from 'classnames'; import Menu from 'antd/lib/menu'; -import Modal from 'antd/lib/modal'; import { currentUser } from '@/services/auth'; import recordEvent from '@/services/recordEvent'; import { $location } from '@/services/ng'; @@ -193,24 +192,12 @@ class VisualizationWidget extends React.Component { }); }; + refreshWidget = () => this.loadWidget(true); + expandWidget = () => { ExpandedWidgetDialog.showModal({ widget: this.props.widget }); }; - 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', - onOk: () => widget.delete().then(onDelete), - maskClosable: true, - autoFocusButton: null, - }); - }; - editParameterMappings = () => { const { widget, dashboard, onParameterMappingsChange } = this.props; EditParameterMappingsDialog.showModal({ @@ -285,7 +272,7 @@ class VisualizationWidget extends React.Component { this.loadWidget(true)} + onRefresh={this.refreshWidget} onExpand={this.expandWidget} /> )} diff --git a/client/app/components/dashboards/dashboard-widget/Widget.jsx b/client/app/components/dashboards/dashboard-widget/Widget.jsx index a12aa630a0..1a169accd4 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.jsx +++ b/client/app/components/dashboards/dashboard-widget/Widget.jsx @@ -13,7 +13,7 @@ import './Widget.less'; function WidgetDropdownButton({ extraOptions, showDeleteOption, onDelete, ...otherProps }) { const WidgetMenu = ( - + {extraOptions} {(showDeleteOption && extraOptions) && } {showDeleteOption && Remove from Dashboard} @@ -27,7 +27,9 @@ function WidgetDropdownButton({ extraOptions, showDeleteOption, onDelete, ...oth placement="bottomRight" trigger={['click']} > - + + +
); @@ -48,7 +50,7 @@ WidgetDropdownButton.defaultProps = { function WidgetDeleteButton({ onClick }) { return ( diff --git a/client/cypress/integration/dashboard/textbox_spec.js b/client/cypress/integration/dashboard/textbox_spec.js index d5aa8000ef..ed8643ff7d 100644 --- a/client/cypress/integration/dashboard/textbox_spec.js +++ b/client/cypress/integration/dashboard/textbox_spec.js @@ -12,6 +12,10 @@ describe('Textbox', () => { }); }); + const confirmDelete = () => { + cy.get('.ant-modal .ant-btn').contains('Delete').click({ force: true }); + }; + it('adds textbox', function () { cy.visit(this.dashboardUrl); editDashboard(); @@ -31,9 +35,11 @@ describe('Textbox', () => { cy.getByTestId(elTestId) .within(() => { - cy.get('.widget-menu-remove').click(); - }) - .should('not.exist'); + cy.getByTestId('WidgetDeleteButton').click(); + }); + + confirmDelete(); + cy.getByTestId(elTestId).should('not.exist'); }); }); @@ -42,15 +48,15 @@ describe('Textbox', () => { cy.visit(this.dashboardUrl); cy.getByTestId(elTestId) .within(() => { - cy.get('.widget-menu-regular') - .click({ force: true }) - .within(() => { - cy.get('li a') - .contains('Remove From Dashboard') - .click({ force: true }); - }); - }) - .should('not.exist'); + cy.getByTestId('WidgetDropdownButton') + .click(); + }); + cy.getByTestId('WidgetDropdownButtonMenu') + .contains('Remove from Dashboard') + .click(); + + confirmDelete(); + cy.getByTestId(elTestId).should('not.exist'); }); }); @@ -70,8 +76,10 @@ describe('Textbox', () => { cy.getByTestId(elTestId1) .as('textbox1') .within(() => { - cy.get('.widget-menu-remove').click(); + cy.getByTestId('WidgetDeleteButton').click(); }); + + confirmDelete(); cy.get('@textbox1').should('not.exist'); // remove 2nd textbox and make sure it's gone @@ -79,8 +87,10 @@ describe('Textbox', () => { .as('textbox2') .within(() => { // unclickable https://github.com/getredash/redash/issues/3202 - cy.get('.widget-menu-remove').click(); + cy.getByTestId('WidgetDeleteButton').click(); }); + + confirmDelete(); cy.get('@textbox2').should('not.exist'); // <-- fails because of the bug }); }); @@ -91,15 +101,14 @@ describe('Textbox', () => { cy.getByTestId(elTestId) .as('textboxEl') .within(() => { - cy.get('.widget-menu-regular') - .click({ force: true }) - .within(() => { - cy.get('li a') - .contains('Edit') - .click({ force: true }); - }); + cy.getByTestId('WidgetDropdownButton') + .click(); }); + cy.getByTestId('WidgetDropdownButtonMenu') + .contains('Edit') + .click(); + const newContent = '[edited]'; cy.getByTestId('TextboxDialog') .should('exist') diff --git a/client/cypress/integration/dashboard/widget_spec.js b/client/cypress/integration/dashboard/widget_spec.js index 743035e559..9b304ebe39 100644 --- a/client/cypress/integration/dashboard/widget_spec.js +++ b/client/cypress/integration/dashboard/widget_spec.js @@ -12,6 +12,10 @@ describe('Widget', () => { }); }); + const confirmDelete = () => { + cy.get('.ant-modal .ant-btn').contains('Delete').click({ force: true }); + }; + it('adds widget', function () { createQuery().then(({ id: queryId }) => { cy.visit(this.dashboardUrl); @@ -32,9 +36,11 @@ describe('Widget', () => { editDashboard(); cy.getByTestId(elTestId) .within(() => { - cy.get('.widget-menu-remove').click(); - }) - .should('not.exist'); + cy.getByTestId('WidgetDeleteButton').click(); + }); + + confirmDelete(); + cy.getByTestId(elTestId).should('not.exist'); }); }); From 584200ca8e52087b9843257880b7d16f44373126 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 27 Aug 2019 14:36:06 -0300 Subject: [PATCH 23/34] Update margin for Parameters --- .../dashboards/dashboard-widget/VisualizationWidget.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index 88f27e9320..855e03d14a 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -74,7 +74,7 @@ function VisualizationWidgetHeader({ widget, parameters, onParametersUpdate }) {
{!isEmpty(parameters) && ( -
+
)} From 9f269c19f7a45e3c96b05c9a6c3175a887dc1d8a Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 27 Aug 2019 17:49:32 -0300 Subject: [PATCH 24/34] Remove widget files --- .../dashboards/dashboard-widget/Widget.less | 171 +++++++++++ client/app/components/dashboards/widget.html | 118 -------- client/app/components/dashboards/widget.js | 114 ------- client/app/components/dashboards/widget.less | 278 ------------------ 4 files changed, 171 insertions(+), 510 deletions(-) delete mode 100644 client/app/components/dashboards/widget.html delete mode 100644 client/app/components/dashboards/widget.js delete mode 100644 client/app/components/dashboards/widget.less diff --git a/client/app/components/dashboards/dashboard-widget/Widget.less b/client/app/components/dashboards/dashboard-widget/Widget.less index 1c4eed1d82..635d5fc8c7 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.less +++ b/client/app/components/dashboards/dashboard-widget/Widget.less @@ -1,3 +1,5 @@ +@import '../../../assets/less/inc/variables'; + .tile .t-header .th-title a.query-link { color: rgba(0, 0, 0, 0.5); } @@ -26,6 +28,10 @@ } } + .parameter-container { + margin: 0 15px; + } + .body-container { display: flex; flex-direction: column; @@ -70,3 +76,168 @@ } } } + +.refresh-button { + margin-left: -6px; +} + +.editing-mode { + .widget-menu-regular { + display: none; + } + .widget-menu-remove { + display: block; + } + + a.query-link { + pointer-events: none; + cursor: move; + } + + .th-title { + cursor: move; + } + + .refresh-indicator { + transition-duration: 0s; + + .rd-timer { + display: none; + } + + .refresh-indicator-mini(); + } +} + +.refresh-indicator { + font-size: 18px; + color: #86a1af; + transition: all 100ms linear; + transition-delay: 150ms; // waits for widget-menu to fade out before moving back over it + transform: translateX(22px); + position: absolute; + right: 29px; + top: 8px; + display: flex; + flex-direction: row-reverse; + + .refresh-icon { + position: relative; + + &:before { + content: ""; + position: absolute; + top: 0px; + right: 0; + width: 24px; + height: 24px; + background-color: #e8ecf0; + border-radius: 50%; + transition: opacity 100ms linear; + transition-delay: 150ms; + } + + i { + height: 24px; + width: 24px; + display: flex; + justify-content: center; + align-items: center; + } + } + + .rd-timer { + font-size: 13px; + display: inline-block; + font-variant-numeric: tabular-nums; + opacity: 0; + transform: translateX(-6px); + transition: all 100ms linear; + transition-delay: 150ms; + color: #bbbbbb; + background-color: rgba(255,255,255,.9); + padding-left: 2px; + padding-right: 1px; + margin-right: -4px; + margin-top: 2px; + } + + .widget-visualization[data-refreshing="false"] & { + display: none; + } +} + +.refresh-indicator-mini() { + font-size: 13px; + transition-delay: 0s; + color: #bbbbbb; + transform: translateY(-4px); + + .refresh-icon:before { + transition-delay: 0s; + opacity: 0; + } + + .rd-timer { + transition-delay: 0s; + opacity: 1; + transform: translateX(0); + } +} + +.tile { + .widget-menu-regular, .btn__refresh { + opacity: 0 !important; + transition: opacity 0.35s ease-in-out; + } + + .t-header { + .th-title { + padding-right: 23px; // no overlap on RefreshIndicator + + a { + color: fade(@redash-black, 80%); + font-size: 15px; + font-weight: 500; + } + } + + .query--description { + font-size: 14px; + line-height: 1.5; + font-style: italic; + + p { + margin-bottom: 0; + } + } + } + + .t-header.widget { + padding: 15px; + } + + &:hover { + .widget-menu-regular, .btn__refresh { + opacity: 1 !important; + transition: opacity 0.35s ease-in-out; + } + + .refresh-indicator { + .refresh-indicator-mini(); + } + } + + .tile__bottom-control { + padding: 10px 15px; + line-height: 2; + + a { + color: fade(@redash-black, 65%); + + &:hover { + color: fade(@redash-black, 95%); + } + } + } +} diff --git a/client/app/components/dashboards/widget.html b/client/app/components/dashboards/widget.html deleted file mode 100644 index a9f6ff7f2d..0000000000 --- a/client/app/components/dashboards/widget.html +++ /dev/null @@ -1,118 +0,0 @@ -
-
-
-
- - -
-
- -
- -
-
-

- -

-
-
-
-
- -
-
- -
-
Error running query: {{$ctrl.widget.getQueryResult().getError()}}
-
-
- -
-
-
- -
-
- -
- - - - - - - - - {{$ctrl.widget.getQueryResult().getUpdatedAt() | dateTime}} - - - - -
-
- -
-
-
-

-

- This widget requires access to a data source you don't have access to. -

-
-
-
- -
-
- - -
-
-
-
diff --git a/client/app/components/dashboards/widget.js b/client/app/components/dashboards/widget.js deleted file mode 100644 index 2dcb7483fa..0000000000 --- a/client/app/components/dashboards/widget.js +++ /dev/null @@ -1,114 +0,0 @@ -import { filter } from 'lodash'; -import { angular2react } from 'angular2react'; -import template from './widget.html'; -import TextboxDialog from '@/components/dashboards/TextboxDialog'; -import ExpandedWidgetDialog from '@/components/dashboards/ExpandedWidgetDialog'; -import EditParameterMappingsDialog from '@/components/dashboards/EditParameterMappingsDialog'; -import './widget.less'; - -export let DashboardWidget = null; // eslint-disable-line import/no-mutable-exports - -function DashboardWidgetCtrl($scope, $location, $window, $rootScope, $timeout, Events, currentUser) { - this.canViewQuery = currentUser.hasPermission('view_query'); - - this.editTextBox = () => { - TextboxDialog.showModal({ - text: this.widget.text, - onConfirm: (text) => { - this.widget.text = text; - return this.widget.save(); - }, - }); - }; - - this.expandVisualization = () => { - ExpandedWidgetDialog.showModal({ widget: this.widget }); - }; - - this.hasParameters = () => this.widget.query.getParametersDefs().length > 0; - - this.editParameterMappings = () => { - EditParameterMappingsDialog.showModal({ - dashboard: this.dashboard, - widget: this.widget, - }).result.then((valuesChanged) => { - this.localParameters = null; - - // refresh widget if any parameter value has been updated - if (valuesChanged) { - $timeout(() => this.refresh()); - } - $scope.$applyAsync(); - $rootScope.$broadcast('dashboard.update-parameters'); - }); - }; - - this.localParametersDefs = () => { - if (!this.localParameters) { - this.localParameters = filter( - this.widget.getParametersDefs(), - param => !this.widget.isStaticParam(param), - ); - } - return this.localParameters; - }; - - this.deleteWidget = () => { - if (!$window.confirm(`Are you sure you want to remove "${this.widget.getName()}" from the dashboard?`)) { - return; - } - - this.widget.delete().then(() => { - if (this.deleted) { - this.deleted({}); - } - }); - }; - - Events.record('view', 'widget', this.widget.id); - - this.load = (refresh = false) => { - const maxAge = $location.search().maxAge; - return this.widget.load(refresh, maxAge); - }; - - this.refresh = (buttonId) => { - this.refreshClickButtonId = buttonId; - this.load(true).finally(() => { - this.refreshClickButtonId = undefined; - }); - }; - - if (this.widget.visualization) { - Events.record('view', 'query', this.widget.visualization.query.id, { dashboard: true }); - Events.record('view', 'visualization', this.widget.visualization.id, { dashboard: true }); - - this.type = 'visualization'; - this.load(); - } else if (this.widget.restricted) { - this.type = 'restricted'; - } else { - this.type = 'textbox'; - } -} - -const DashboardWidgetOptions = { - template, - controller: DashboardWidgetCtrl, - bindings: { - widget: '<', - public: '<', - dashboard: '<', - filters: '<', - deleted: '<', - }, -}; - -export default function init(ngModule) { - ngModule.component('dashboardWidget', DashboardWidgetOptions); - ngModule.run(['$injector', ($injector) => { - DashboardWidget = angular2react('dashboardWidget ', DashboardWidgetOptions, $injector); - }]); -} - -init.init = true; diff --git a/client/app/components/dashboards/widget.less b/client/app/components/dashboards/widget.less deleted file mode 100644 index 13f1c34f80..0000000000 --- a/client/app/components/dashboards/widget.less +++ /dev/null @@ -1,278 +0,0 @@ -@import '../../assets/less/inc/variables'; - -.tile .t-header .th-title a.query-link { - color: rgba(0, 0, 0, 0.5); -} - -.th-title p.hidden-print { - margin-bottom: 0; -} - -.widget-wrapper { - .parameter-container { - margin: 0 15px; - } - - .body-container { - display: flex; - flex-direction: column; - align-items: stretch; - - .body-row { - flex: 0 1 auto; - } - - .body-row-auto { - flex: 1 1 auto; - } - } - - .spinner-container { - position: relative; - - .spinner { - display: flex; - align-items: center; - justify-content: center; - text-align: center; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - } - } - - .dropdown-header { - padding: 0; - - .actions { - position: static; - } - } - - .t-header.widget { - .dropdown { - margin-top: -15px; - margin-right: -15px; - - .actions { - position: static; - cursor: pointer; - } - } - } - - .scrollbox:empty { - padding: 0 !important; - font-size: 1px !important; - } - - .widget-text { - :first-child { - margin-top: 0; - } - :last-child { - margin-bottom: 0; - } - - .actions { - cursor: pointer; - } - } -} - -.editing-mode { - .widget-menu-regular { - display: none; - } - .widget-menu-remove { - display: block; - } - - a.query-link { - pointer-events: none; - cursor: move; - } - - .th-title { - cursor: move; - } - - .refresh-indicator { - transition-duration: 0s; - - .rd-timer { - display: none; - } - - .refresh-indicator-mini(); - } -} - -.refresh-indicator { - font-size: 18px; - color: #86a1af; - transition: all 100ms linear; - transition-delay: 150ms; // waits for widget-menu to fade out before moving back over it - transform: translateX(22px); - position: absolute; - right: 29px; - top: 8px; - display: flex; - flex-direction: row-reverse; - - .refresh-icon { - position: relative; - - &:before { - content: ""; - position: absolute; - top: 0px; - right: 0; - width: 24px; - height: 24px; - background-color: #e8ecf0; - border-radius: 50%; - transition: opacity 100ms linear; - transition-delay: 150ms; - } - - i { - height: 24px; - width: 24px; - display: flex; - justify-content: center; - align-items: center; - } - } - - .rd-timer { - font-size: 13px; - display: inline-block; - font-variant-numeric: tabular-nums; - opacity: 0; - transform: translateX(-6px); - transition: all 100ms linear; - transition-delay: 150ms; - color: #bbbbbb; - background-color: rgba(255,255,255,.9); - padding-left: 2px; - padding-right: 1px; - margin-right: -4px; - margin-top: 2px; - } - - .widget-visualization[data-refreshing="false"] & { - display: none; - } -} - -.refresh-indicator-mini() { - font-size: 13px; - transition-delay: 0s; - color: #bbbbbb; - transform: translateY(-4px); - - .refresh-icon:before { - transition-delay: 0s; - opacity: 0; - } - - .rd-timer { - transition-delay: 0s; - opacity: 1; - transform: translateX(0); - } -} - -.refresh-button { - margin-left: -6px; -} - -.tile { - .widget-menu-regular, .btn__refresh { - opacity: 0 !important; - transition: opacity 0.35s ease-in-out; - } - - .t-header { - .th-title { - padding-right: 23px; // no overlap on RefreshIndicator - - a { - color: fade(@redash-black, 80%); - font-size: 15px; - font-weight: 500; - } - } - - .query--description { - font-size: 14px; - line-height: 1.5; - font-style: italic; - - p { - margin-bottom: 0; - } - } - } - - .t-header.widget { - padding: 15px; - } - - &:hover { - .widget-menu-regular, .btn__refresh { - opacity: 1 !important; - transition: opacity 0.35s ease-in-out; - } - - .refresh-indicator { - .refresh-indicator-mini(); - } - } - - .tile__bottom-control { - padding: 10px 15px; - line-height: 2; - - a { - color: fade(@redash-black, 65%); - - &:hover { - color: fade(@redash-black, 95%); - } - } - } -} - - -// react-grid-layout overrides -.react-grid-item { - - // placeholder color - &.react-grid-placeholder { - border-radius: 3px; - background-color: #E0E6EB; - opacity: 0.5; - } - - // resize placeholder behind widget, the lib's default is above 🤷‍♂️ - &.resizing { - z-index: 3; - } - - // auto-height animation - &.cssTransforms:not(.resizing) { - transition-property: transform, height; // added ", height" - } - - // resize handle size - & > .react-resizable-handle::after { - width: 11px; - height: 11px; - right: 5px; - bottom: 5px; - } -} \ No newline at end of file From 7f6bcd7b185d893361ab7851aa227320c15a2960 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 28 Aug 2019 10:02:22 -0300 Subject: [PATCH 25/34] Revert "Improve sizing for Number inputs" This reverts commit a02ce8f0aaadcc930f7d40feb2ef03942109b38b. --- client/app/components/ParameterValueInput.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/components/ParameterValueInput.less b/client/app/components/ParameterValueInput.less index 25acdca106..9921c74a94 100644 --- a/client/app/components/ParameterValueInput.less +++ b/client/app/components/ParameterValueInput.less @@ -13,7 +13,7 @@ } .@{ant-prefix}-select { - min-width: 100% !important; + width: 100%; } &[data-dirty] { From 111b68e1f1f117d0569590effaaeb21570db891f Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Thu, 29 Aug 2019 11:42:26 -0300 Subject: [PATCH 26/34] Some cleanup --- .../assets/less/inc/visualizations/misc.less | 2 +- .../less/inc/visualizations/pivot-table.less | 2 +- client/app/assets/less/redash/query.less | 2 +- .../dashboard-widget/VisualizationWidget.jsx | 15 +++++---- .../dashboards/dashboard-widget/Widget.jsx | 4 +-- .../dashboards/dashboard-widget/Widget.less | 12 +++---- client/app/components/query-link.js | 32 ------------------- client/app/pages/dashboards/dashboard.less | 4 +-- .../integration/dashboard/textbox_spec.js | 10 +++--- .../integration/dashboard/widget_spec.js | 4 +-- 10 files changed, 28 insertions(+), 59 deletions(-) delete mode 100644 client/app/components/query-link.js diff --git a/client/app/assets/less/inc/visualizations/misc.less b/client/app/assets/less/inc/visualizations/misc.less index cc5600bd16..2b9376acce 100644 --- a/client/app/assets/less/inc/visualizations/misc.less +++ b/client/app/assets/less/inc/visualizations/misc.less @@ -1,4 +1,4 @@ -visualization-renderer { +.visualization-renderer { display: block; .pagination, diff --git a/client/app/assets/less/inc/visualizations/pivot-table.less b/client/app/assets/less/inc/visualizations/pivot-table.less index 7400914f47..44f11d46fc 100644 --- a/client/app/assets/less/inc/visualizations/pivot-table.less +++ b/client/app/assets/less/inc/visualizations/pivot-table.less @@ -1,3 +1,3 @@ -.pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { +.pivot-table-renderer > table, grid-renderer > div, .visualization-renderer > div { overflow: auto; } diff --git a/client/app/assets/less/redash/query.less b/client/app/assets/less/redash/query.less index 72f2b7cf5a..2ce190de00 100644 --- a/client/app/assets/less/redash/query.less +++ b/client/app/assets/less/redash/query.less @@ -350,7 +350,7 @@ a.label-tag { border-bottom: 1px solid #efefef; } - .pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { + .pivot-table-renderer > table, grid-renderer > div, .visualization-renderer > div { overflow: visible; } diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index 855e03d14a..79759e423c 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { compact, isEmpty, invoke } from 'lodash'; import { markdown } from 'markdown'; -import classNames from 'classnames'; +import cx from 'classnames'; import Menu from 'antd/lib/menu'; import { currentUser } from '@/services/auth'; import recordEvent from '@/services/recordEvent'; @@ -18,7 +18,7 @@ import EditParameterMappingsDialog from '@/components/dashboards/EditParameterMa import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer'; import Widget from './Widget'; -function visualizationWidgetMenuOptions(widget, canEditDashboard, onParametersEdit) { +function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParametersEdit }) { const canViewQuery = currentUser.hasPermission('view_query'); const canEditParameters = canEditDashboard && !isEmpty(invoke(widget, 'query.getParametersDefs')); const widgetQueryResult = widget.getQueryResult(); @@ -92,7 +92,7 @@ VisualizationWidgetHeader.defaultProps = { onParametersUpdate: () => {}, paramet function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) { const widgetQueryResult = widget.getQueryResult(); - const updatedAt = widgetQueryResult && widgetQueryResult.getUpdatedAt(); + const updatedAt = invoke(widgetQueryResult, 'getUpdatedAt'); const [refreshClickButtonId, setRefreshClickButtonId] = useState(); const refreshWidget = (buttonId) => { @@ -110,7 +110,7 @@ function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) { onClick={() => refreshWidget(1)} data-test="RefreshButton" > - {' '} + {' '} )} @@ -126,7 +126,7 @@ function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) { className="btn btn-sm btn-default pull-right hidden-print btn-transparent btn__refresh" onClick={() => refreshWidget(2)} > - + )} + {extraOptions} {(showDeleteOption && extraOptions) && } {showDeleteOption && Remove from Dashboard} diff --git a/client/app/components/dashboards/dashboard-widget/Widget.less b/client/app/components/dashboards/dashboard-widget/Widget.less index 635d5fc8c7..47b3e84e10 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.less +++ b/client/app/components/dashboards/dashboard-widget/Widget.less @@ -88,23 +88,23 @@ .widget-menu-remove { display: block; } - + a.query-link { pointer-events: none; cursor: move; } - + .th-title { cursor: move; } - + .refresh-indicator { transition-duration: 0s; - + .rd-timer { display: none; } - + .refresh-indicator-mini(); } } @@ -123,7 +123,7 @@ .refresh-icon { position: relative; - + &:before { content: ""; position: absolute; diff --git a/client/app/components/query-link.js b/client/app/components/query-link.js deleted file mode 100644 index 307675e312..0000000000 --- a/client/app/components/query-link.js +++ /dev/null @@ -1,32 +0,0 @@ -export default function init(ngModule) { - ngModule.component('queryLink', { - bindings: { - query: '<', - visualization: '<', - readonly: '<', - }, - template: ` - - - {{$ctrl.query.name}} - - `, - controller() { - this.getUrl = () => { - let hash = null; - if (this.visualization) { - if (this.visualization.type === 'TABLE') { - // link to hard-coded table tab instead of the (hidden) visualization tab - hash = 'table'; - } else { - hash = this.visualization.id; - } - } - - return this.query.getUrl(false, hash); - }; - }, - }); -} - -init.init = true; diff --git a/client/app/pages/dashboards/dashboard.less b/client/app/pages/dashboards/dashboard.less index a644de4ba8..7855654fbb 100644 --- a/client/app/pages/dashboards/dashboard.less +++ b/client/app/pages/dashboards/dashboard.less @@ -22,7 +22,7 @@ padding: 0; } - .pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { + .pivot-table-renderer > table, grid-renderer > div, .visualization-renderer > div { overflow: visible; } @@ -56,7 +56,7 @@ } .dashboard-widget-wrapper:not(.widget-auto-height-enabled) { - visualization-renderer { + .visualization-renderer { display: flex; flex-direction: column; position: absolute; diff --git a/client/cypress/integration/dashboard/textbox_spec.js b/client/cypress/integration/dashboard/textbox_spec.js index ed8643ff7d..610ac97d54 100644 --- a/client/cypress/integration/dashboard/textbox_spec.js +++ b/client/cypress/integration/dashboard/textbox_spec.js @@ -12,7 +12,7 @@ describe('Textbox', () => { }); }); - const confirmDelete = () => { + const confirmDeletionInModal = () => { cy.get('.ant-modal .ant-btn').contains('Delete').click({ force: true }); }; @@ -38,7 +38,7 @@ describe('Textbox', () => { cy.getByTestId('WidgetDeleteButton').click(); }); - confirmDelete(); + confirmDeletionInModal(); cy.getByTestId(elTestId).should('not.exist'); }); }); @@ -55,7 +55,7 @@ describe('Textbox', () => { .contains('Remove from Dashboard') .click(); - confirmDelete(); + confirmDeletionInModal(); cy.getByTestId(elTestId).should('not.exist'); }); }); @@ -79,7 +79,7 @@ describe('Textbox', () => { cy.getByTestId('WidgetDeleteButton').click(); }); - confirmDelete(); + confirmDeletionInModal(); cy.get('@textbox1').should('not.exist'); // remove 2nd textbox and make sure it's gone @@ -90,7 +90,7 @@ describe('Textbox', () => { cy.getByTestId('WidgetDeleteButton').click(); }); - confirmDelete(); + confirmDeletionInModal(); cy.get('@textbox2').should('not.exist'); // <-- fails because of the bug }); }); diff --git a/client/cypress/integration/dashboard/widget_spec.js b/client/cypress/integration/dashboard/widget_spec.js index 9b304ebe39..61970e94c6 100644 --- a/client/cypress/integration/dashboard/widget_spec.js +++ b/client/cypress/integration/dashboard/widget_spec.js @@ -12,7 +12,7 @@ describe('Widget', () => { }); }); - const confirmDelete = () => { + const confirmDeletionInModal = () => { cy.get('.ant-modal .ant-btn').contains('Delete').click({ force: true }); }; @@ -39,7 +39,7 @@ describe('Widget', () => { cy.getByTestId('WidgetDeleteButton').click(); }); - confirmDelete(); + confirmDeletionInModal(); cy.getByTestId(elTestId).should('not.exist'); }); }); From 6d6f81a5635d974a82ad418b518336313b05e858 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sat, 31 Aug 2019 11:53:22 -0300 Subject: [PATCH 27/34] Move refresh logic to the Dashboard --- client/app/components/Parameters.jsx | 2 +- .../components/dashboards/DashboardGrid.jsx | 6 +++- .../dashboard-widget/VisualizationWidget.jsx | 29 +++++-------------- client/app/pages/dashboards/dashboard.html | 1 + client/app/pages/dashboards/dashboard.js | 14 +++++---- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/client/app/components/Parameters.jsx b/client/app/components/Parameters.jsx index 285b6a6237..89ea3c9690 100644 --- a/client/app/components/Parameters.jsx +++ b/client/app/components/Parameters.jsx @@ -113,10 +113,10 @@ export class Parameters extends React.Component { this.setState(({ parameters }) => { const parametersWithPendingValues = parameters.filter(p => p.hasPendingValue); forEach(parameters, p => p.applyPendingValue()); - onValuesChange(parametersWithPendingValues); if (!disableUrlUpdate) { updateUrl(parameters); } + onValuesChange(parametersWithPendingValues); return { parameters }; }); }; diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index 704a12c317..d1ac177705 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -41,6 +41,7 @@ class DashboardGrid extends React.Component { widgets: PropTypes.arrayOf(WidgetType).isRequired, filters: FiltersType, onBreakpointChange: PropTypes.func, + onRefreshWidget: PropTypes.func, onRemoveWidget: PropTypes.func, onLayoutChange: PropTypes.func, onParameterMappingsChange: PropTypes.func, @@ -52,6 +53,7 @@ class DashboardGrid extends React.Component { static defaultProps = { isPublic: false, filters: [], + onRefreshWidget: () => {}, onRemoveWidget: () => {}, onLayoutChange: () => {}, onBreakpointChange: () => {}, @@ -174,7 +176,8 @@ class DashboardGrid extends React.Component { render() { const className = cx('dashboard-wrapper', this.props.isEditing ? 'editing-mode' : 'preview-mode'); - const { onRemoveWidget, onParameterMappingsChange, filters, dashboard, isPublic, widgets } = this.props; + const { onRefreshWidget, onRemoveWidget, onParameterMappingsChange, + filters, dashboard, isPublic, widgets } = this.props; return (
@@ -212,6 +215,7 @@ class DashboardGrid extends React.Component { onRefreshWidget(widget)} onParameterMappingsChange={onParameterMappingsChange} /> )} diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index 79759e423c..579531fb73 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -6,7 +6,6 @@ import cx from 'classnames'; import Menu from 'antd/lib/menu'; import { currentUser } from '@/services/auth'; import recordEvent from '@/services/recordEvent'; -import { $location } from '@/services/ng'; import { formatDateTime } from '@/filters/datetime'; import HtmlContent from '@/components/HtmlContent'; import { Parameters } from '@/components/Parameters'; @@ -155,6 +154,7 @@ class VisualizationWidget extends React.Component { filters: FiltersType, isPublic: PropTypes.bool, canEdit: PropTypes.bool, + onRefresh: PropTypes.func, onDelete: PropTypes.func, onParameterMappingsChange: PropTypes.func, }; @@ -163,37 +163,22 @@ class VisualizationWidget extends React.Component { filters: [], isPublic: false, canEdit: false, + onRefresh: () => {}, onDelete: () => {}, onParameterMappingsChange: () => {}, }; constructor(props) { super(props); - const widgetQueryResult = props.widget.getQueryResult(); - const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus(); - - this.state = { widgetStatus, localParameters: props.widget.getLocalParameters() }; + this.state = { localParameters: props.widget.getLocalParameters() }; } componentDidMount() { const { widget } = this.props; recordEvent('view', 'query', widget.visualization.query.id, { dashboard: true }); recordEvent('view', 'visualization', widget.visualization.id, { dashboard: true }); - this.loadWidget(); } - loadWidget = (refresh = false) => { - const { widget } = this.props; - const maxAge = $location.search().maxAge; - return widget.load(refresh, maxAge).then(({ status }) => { - this.setState({ widgetStatus: status }); - }).catch(() => { - this.setState({ widgetStatus: 'failed' }); - }); - }; - - refreshWidget = () => this.loadWidget(true); - expandWidget = () => { ExpandedWidgetDialog.showModal({ widget: this.props.widget }); }; @@ -215,8 +200,8 @@ class VisualizationWidget extends React.Component { renderVisualization() { const { widget, filters } = this.props; - const { widgetStatus } = this.state; const widgetQueryResult = widget.getQueryResult(); + const widgetStatus = widgetQueryResult && widgetQueryResult.getStatus(); switch (widgetStatus) { case 'failed': return ( @@ -250,7 +235,7 @@ class VisualizationWidget extends React.Component { } render() { - const { widget, isPublic, canEdit } = this.props; + const { widget, isPublic, canEdit, onRefresh } = this.props; const { localParameters } = this.state; const widgetQueryResult = widget.getQueryResult(); const isRefreshing = widget.loading && !!(widgetQueryResult && widgetQueryResult.getStatus()); @@ -266,14 +251,14 @@ class VisualizationWidget extends React.Component { )} footer={( )} diff --git a/client/app/pages/dashboards/dashboard.html b/client/app/pages/dashboards/dashboard.html index 3b597e1965..98da1dfcf5 100644 --- a/client/app/pages/dashboards/dashboard.html +++ b/client/app/pages/dashboards/dashboard.html @@ -111,6 +111,7 @@

refreshing-widgets="$ctrl.refreshingWidgets" on-layout-change="$ctrl.onLayoutChange" on-breakpoint-change="$ctrl.onBreakpointChanged" + on-refresh-widget="$ctrl.refreshWidget" on-remove-widget="$ctrl.removeWidget" on-parameter-mappings-change="$ctrl.extractGlobalParameters" /> diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index 2e1390538f..5651dbd739 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -134,6 +134,14 @@ function DashboardCtrl( this.globalParameters = this.dashboard.getParametersDefs(); }; + this.loadWidget = (widget, forceRefresh) => { + widget.getParametersDefs(); // Force widget to read parameters values from URL + this.refreshingWidgets += 1; + return widget.load(forceRefresh).finally(() => { this.refreshingWidgets -= 1; }); + }; + + this.refreshWidget = widget => this.loadWidget(widget, true); + const collectFilters = (dashboard, forceRefresh, updatedParameters = []) => { const affectedWidgets = updatedParameters.length > 0 ? this.dashboard.widgets.filter( widget => Object.values(widget.getParameterMappings()).filter( @@ -143,11 +151,7 @@ function DashboardCtrl( ), ) : this.dashboard.widgets; - const queryResultPromises = _.compact(affectedWidgets.map((widget) => { - widget.getParametersDefs(); // Force widget to read parameters values from URL - this.refreshingWidgets += 1; - return widget.load(forceRefresh).finally(() => { this.refreshingWidgets -= 1; }); - })); + const queryResultPromises = _.compact(affectedWidgets.map(widget => this.loadWidget(widget, forceRefresh))); return $q.all(queryResultPromises).then((queryResults) => { this.filters = collectDashboardFilters(dashboard, queryResults, $location.search()); From 83e7149f15618f3423607bbdaf809d3fbc30442c Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Sun, 1 Sep 2019 22:40:27 -0300 Subject: [PATCH 28/34] Add loadingWidgets logic to the public dashboard --- client/app/components/dashboards/DashboardGrid.jsx | 4 ++-- client/app/pages/dashboards/dashboard.html | 2 +- client/app/pages/dashboards/dashboard.js | 6 +++--- client/app/pages/dashboards/public-dashboard-page.html | 1 + client/app/pages/dashboards/public-dashboard-page.js | 6 +++++- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index d1ac177705..07a677235b 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -47,7 +47,7 @@ class DashboardGrid extends React.Component { onParameterMappingsChange: PropTypes.func, // Force component update when widgets are refreshing. // Remove this when Dashboard is migrated to React - refreshingWidgets: PropTypes.number, // eslint-disable-line react/no-unused-prop-types + loadingWidgets: PropTypes.number, // eslint-disable-line react/no-unused-prop-types }; static defaultProps = { @@ -58,7 +58,7 @@ class DashboardGrid extends React.Component { onLayoutChange: () => {}, onBreakpointChange: () => {}, onParameterMappingsChange: () => {}, - refreshingWidgets: 0, + loadingWidgets: 0, }; static normalizeFrom(widget) { diff --git a/client/app/pages/dashboards/dashboard.html b/client/app/pages/dashboards/dashboard.html index 98da1dfcf5..2bdde14eaa 100644 --- a/client/app/pages/dashboards/dashboard.html +++ b/client/app/pages/dashboards/dashboard.html @@ -108,7 +108,7 @@

widgets="$ctrl.dashboard.widgets" filters="$ctrl.filters" is-editing="$ctrl.layoutEditing && !$ctrl.isGridDisabled" - refreshing-widgets="$ctrl.refreshingWidgets" + loading-widgets="$ctrl.loadingWidgets" on-layout-change="$ctrl.onLayoutChange" on-breakpoint-change="$ctrl.onBreakpointChanged" on-refresh-widget="$ctrl.refreshWidget" diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index 5651dbd739..5a86c178a9 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -105,7 +105,7 @@ function DashboardCtrl( this.globalParameters = []; this.isDashboardOwner = false; this.filters = []; - this.refreshingWidgets = 0; + this.loadingWidgets = 0; this.refreshRates = clientConfig.dashboardRefreshIntervals.map(interval => ({ name: durationHumanize(interval), @@ -136,8 +136,8 @@ function DashboardCtrl( this.loadWidget = (widget, forceRefresh) => { widget.getParametersDefs(); // Force widget to read parameters values from URL - this.refreshingWidgets += 1; - return widget.load(forceRefresh).finally(() => { this.refreshingWidgets -= 1; }); + this.loadingWidgets += 1; + return widget.load(forceRefresh).finally(() => { this.loadingWidgets -= 1; }); }; this.refreshWidget = widget => this.loadWidget(widget, true); diff --git a/client/app/pages/dashboards/public-dashboard-page.html b/client/app/pages/dashboards/public-dashboard-page.html index 7253244634..07c3c151c8 100644 --- a/client/app/pages/dashboards/public-dashboard-page.html +++ b/client/app/pages/dashboards/public-dashboard-page.html @@ -17,6 +17,7 @@ filters="$ctrl.filters" is-editing="false" is-public="true" + loading-widgets="$ctrl.loadingWidgets" />

diff --git a/client/app/pages/dashboards/public-dashboard-page.js b/client/app/pages/dashboards/public-dashboard-page.js index 453cc01d4f..1b4480e2db 100644 --- a/client/app/pages/dashboards/public-dashboard-page.js +++ b/client/app/pages/dashboards/public-dashboard-page.js @@ -27,6 +27,7 @@ const PublicDashboardPage = { this.logoUrl = logoUrl; this.public = true; this.globalParameters = []; + this.loadingWidgets = 0; this.extractGlobalParameters = () => { this.globalParameters = this.dashboard.getParametersDefs(); @@ -38,7 +39,10 @@ const PublicDashboardPage = { loadDashboard($http, $route).then((data) => { this.dashboard = new Dashboard(data); this.dashboard.widgets = Dashboard.prepareDashboardWidgets(this.dashboard.widgets); - this.dashboard.widgets.forEach(widget => widget.load(!!refreshRate)); + this.dashboard.widgets.forEach((widget) => { + this.loadingWidgets += 1; + widget.load(!!refreshRate).finally(() => { this.loadingWidgets -= 1; }); + }); this.filters = []; // TODO: implement (@/services/dashboard.js:collectDashboardFilters) this.filtersOnChange = (allFilters) => { this.filters = allFilters; From da6e93ecc9ab910c8a4b2a18fc3e68038a04baa3 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Mon, 2 Sep 2019 18:54:31 -0300 Subject: [PATCH 29/34] Add onLoadWidget --- client/app/components/dashboards/DashboardGrid.jsx | 7 +++++-- .../dashboard-widget/VisualizationWidget.jsx | 5 ++++- client/app/pages/dashboards/dashboard.html | 1 + client/app/pages/dashboards/dashboard.js | 2 +- .../app/pages/dashboards/public-dashboard-page.html | 2 ++ .../app/pages/dashboards/public-dashboard-page.js | 13 +++++++++---- 6 files changed, 22 insertions(+), 8 deletions(-) diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index 07a677235b..672f0a38c8 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -41,6 +41,7 @@ class DashboardGrid extends React.Component { widgets: PropTypes.arrayOf(WidgetType).isRequired, filters: FiltersType, onBreakpointChange: PropTypes.func, + onLoadWidget: PropTypes.func, onRefreshWidget: PropTypes.func, onRemoveWidget: PropTypes.func, onLayoutChange: PropTypes.func, @@ -53,6 +54,7 @@ class DashboardGrid extends React.Component { static defaultProps = { isPublic: false, filters: [], + onLoadWidget: () => {}, onRefreshWidget: () => {}, onRemoveWidget: () => {}, onLayoutChange: () => {}, @@ -176,8 +178,8 @@ class DashboardGrid extends React.Component { render() { const className = cx('dashboard-wrapper', this.props.isEditing ? 'editing-mode' : 'preview-mode'); - const { onRefreshWidget, onRemoveWidget, onParameterMappingsChange, - filters, dashboard, isPublic, widgets } = this.props; + const { onLoadWidget, onRefreshWidget, onRemoveWidget, + onParameterMappingsChange, filters, dashboard, isPublic, widgets } = this.props; return (
@@ -215,6 +217,7 @@ class DashboardGrid extends React.Component { onLoadWidget(widget)} onRefresh={() => onRefreshWidget(widget)} onParameterMappingsChange={onParameterMappingsChange} /> diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index 156b8f1f5e..a0aa30308f 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -159,6 +159,7 @@ class VisualizationWidget extends React.Component { filters: FiltersType, isPublic: PropTypes.bool, canEdit: PropTypes.bool, + onLoad: PropTypes.func, onRefresh: PropTypes.func, onDelete: PropTypes.func, onParameterMappingsChange: PropTypes.func, @@ -168,6 +169,7 @@ class VisualizationWidget extends React.Component { filters: [], isPublic: false, canEdit: false, + onLoad: () => {}, onRefresh: () => {}, onDelete: () => {}, onParameterMappingsChange: () => {}, @@ -179,9 +181,10 @@ class VisualizationWidget extends React.Component { } componentDidMount() { - const { widget } = this.props; + const { widget, onLoad } = this.props; recordEvent('view', 'query', widget.visualization.query.id, { dashboard: true }); recordEvent('view', 'visualization', widget.visualization.id, { dashboard: true }); + onLoad(); } expandWidget = () => { diff --git a/client/app/pages/dashboards/dashboard.html b/client/app/pages/dashboards/dashboard.html index 2bdde14eaa..037ece4858 100644 --- a/client/app/pages/dashboards/dashboard.html +++ b/client/app/pages/dashboards/dashboard.html @@ -111,6 +111,7 @@

loading-widgets="$ctrl.loadingWidgets" on-layout-change="$ctrl.onLayoutChange" on-breakpoint-change="$ctrl.onBreakpointChanged" + on-load-widget="$ctrl.loadWidget" on-refresh-widget="$ctrl.refreshWidget" on-remove-widget="$ctrl.removeWidget" on-parameter-mappings-change="$ctrl.extractGlobalParameters" diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index 5a86c178a9..93551e2386 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -134,7 +134,7 @@ function DashboardCtrl( this.globalParameters = this.dashboard.getParametersDefs(); }; - this.loadWidget = (widget, forceRefresh) => { + this.loadWidget = (widget, forceRefresh = false) => { widget.getParametersDefs(); // Force widget to read parameters values from URL this.loadingWidgets += 1; return widget.load(forceRefresh).finally(() => { this.loadingWidgets -= 1; }); diff --git a/client/app/pages/dashboards/public-dashboard-page.html b/client/app/pages/dashboards/public-dashboard-page.html index 07c3c151c8..f2b02288bd 100644 --- a/client/app/pages/dashboards/public-dashboard-page.html +++ b/client/app/pages/dashboards/public-dashboard-page.html @@ -17,6 +17,8 @@ filters="$ctrl.filters" is-editing="false" is-public="true" + on-load-widget="$ctrl.loadWidget" + on-refresh-widget="$ctrl.refreshWidget" loading-widgets="$ctrl.loadingWidgets" />

diff --git a/client/app/pages/dashboards/public-dashboard-page.js b/client/app/pages/dashboards/public-dashboard-page.js index 1b4480e2db..2b634a5b99 100644 --- a/client/app/pages/dashboards/public-dashboard-page.js +++ b/client/app/pages/dashboards/public-dashboard-page.js @@ -35,14 +35,19 @@ const PublicDashboardPage = { const refreshRate = Math.max(30, parseFloat($location.search().refresh)); + this.loadWidget = (widget, forceRefresh = false) => { + widget.getParametersDefs(); // Force widget to read parameters values from URL + this.loadingWidgets += 1; + return widget.load(forceRefresh).finally(() => { this.loadingWidgets -= 1; }); + }; + + this.refreshWidget = widget => this.loadWidget(widget, true); + this.refreshDashboard = () => { loadDashboard($http, $route).then((data) => { this.dashboard = new Dashboard(data); this.dashboard.widgets = Dashboard.prepareDashboardWidgets(this.dashboard.widgets); - this.dashboard.widgets.forEach((widget) => { - this.loadingWidgets += 1; - widget.load(!!refreshRate).finally(() => { this.loadingWidgets -= 1; }); - }); + this.dashboard.widgets.forEach(widget => this.loadWidget(widget, !!refreshRate)); this.filters = []; // TODO: implement (@/services/dashboard.js:collectDashboardFilters) this.filtersOnChange = (allFilters) => { this.filters = allFilters; From 5b3e9b92783f918a9c0bea74910655bd3ea3430f Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Mon, 2 Sep 2019 19:18:02 -0300 Subject: [PATCH 30/34] Remove parameter from URL when empty --- client/app/services/query.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/app/services/query.js b/client/app/services/query.js index c194f2782a..f25358876d 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -349,10 +349,11 @@ export class Parameter { } toUrlParams() { + const prefix = this.urlPrefix; if (this.isEmpty) { - return {}; + return { [`${prefix}${this.name}`]: null }; } - const prefix = this.urlPrefix; + if (isDateRangeParameter(this.type) && isObject(this.value)) { return { [`${prefix}${this.name}.start`]: this.value.start, From 7f24c274c32fe910daeb58520f7765d7cc76eded Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Mon, 2 Sep 2019 23:24:28 -0300 Subject: [PATCH 31/34] Recreate widget array instead of loadingWidgets --- client/app/components/dashboards/DashboardGrid.jsx | 4 ---- client/app/pages/dashboards/dashboard.html | 1 - client/app/pages/dashboards/dashboard.js | 9 ++++++--- client/app/pages/dashboards/public-dashboard-page.html | 1 - client/app/pages/dashboards/public-dashboard-page.js | 9 ++++++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index 672f0a38c8..ee9e778be7 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -46,9 +46,6 @@ class DashboardGrid extends React.Component { onRemoveWidget: PropTypes.func, onLayoutChange: PropTypes.func, onParameterMappingsChange: PropTypes.func, - // Force component update when widgets are refreshing. - // Remove this when Dashboard is migrated to React - loadingWidgets: PropTypes.number, // eslint-disable-line react/no-unused-prop-types }; static defaultProps = { @@ -60,7 +57,6 @@ class DashboardGrid extends React.Component { onLayoutChange: () => {}, onBreakpointChange: () => {}, onParameterMappingsChange: () => {}, - loadingWidgets: 0, }; static normalizeFrom(widget) { diff --git a/client/app/pages/dashboards/dashboard.html b/client/app/pages/dashboards/dashboard.html index 037ece4858..c594824840 100644 --- a/client/app/pages/dashboards/dashboard.html +++ b/client/app/pages/dashboards/dashboard.html @@ -108,7 +108,6 @@

widgets="$ctrl.dashboard.widgets" filters="$ctrl.filters" is-editing="$ctrl.layoutEditing && !$ctrl.isGridDisabled" - loading-widgets="$ctrl.loadingWidgets" on-layout-change="$ctrl.onLayoutChange" on-breakpoint-change="$ctrl.onBreakpointChanged" on-load-widget="$ctrl.loadWidget" diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index 93551e2386..681209a79a 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -105,7 +105,6 @@ function DashboardCtrl( this.globalParameters = []; this.isDashboardOwner = false; this.filters = []; - this.loadingWidgets = 0; this.refreshRates = clientConfig.dashboardRefreshIntervals.map(interval => ({ name: durationHumanize(interval), @@ -134,10 +133,14 @@ function DashboardCtrl( this.globalParameters = this.dashboard.getParametersDefs(); }; + this.forceDashboardGridReload = () => { + this.dashboard.widgets = [...this.dashboard.widgets]; + }; + this.loadWidget = (widget, forceRefresh = false) => { widget.getParametersDefs(); // Force widget to read parameters values from URL - this.loadingWidgets += 1; - return widget.load(forceRefresh).finally(() => { this.loadingWidgets -= 1; }); + this.forceDashboardGridReload(); + return widget.load(forceRefresh).finally(this.forceDashboardGridReload); }; this.refreshWidget = widget => this.loadWidget(widget, true); diff --git a/client/app/pages/dashboards/public-dashboard-page.html b/client/app/pages/dashboards/public-dashboard-page.html index f2b02288bd..2c1d251d3f 100644 --- a/client/app/pages/dashboards/public-dashboard-page.html +++ b/client/app/pages/dashboards/public-dashboard-page.html @@ -19,7 +19,6 @@ is-public="true" on-load-widget="$ctrl.loadWidget" on-refresh-widget="$ctrl.refreshWidget" - loading-widgets="$ctrl.loadingWidgets" />

diff --git a/client/app/pages/dashboards/public-dashboard-page.js b/client/app/pages/dashboards/public-dashboard-page.js index 2b634a5b99..416b884a91 100644 --- a/client/app/pages/dashboards/public-dashboard-page.js +++ b/client/app/pages/dashboards/public-dashboard-page.js @@ -27,7 +27,6 @@ const PublicDashboardPage = { this.logoUrl = logoUrl; this.public = true; this.globalParameters = []; - this.loadingWidgets = 0; this.extractGlobalParameters = () => { this.globalParameters = this.dashboard.getParametersDefs(); @@ -35,10 +34,14 @@ const PublicDashboardPage = { const refreshRate = Math.max(30, parseFloat($location.search().refresh)); + this.forceDashboardGridReload = () => { + this.dashboard.widgets = [...this.dashboard.widgets]; + }; + this.loadWidget = (widget, forceRefresh = false) => { widget.getParametersDefs(); // Force widget to read parameters values from URL - this.loadingWidgets += 1; - return widget.load(forceRefresh).finally(() => { this.loadingWidgets -= 1; }); + this.forceDashboardGridReload(); + return widget.load(forceRefresh).finally(this.forceDashboardGridReload); }; this.refreshWidget = widget => this.loadWidget(widget, true); From 4716d4b31936850068b0271086ede5f77a564bf5 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 3 Sep 2019 14:18:59 -0300 Subject: [PATCH 32/34] Add comment about re-rendering + whitespace missing --- client/app/components/dashboards/ExpandedWidgetDialog.jsx | 2 +- client/app/pages/dashboards/dashboard.js | 2 ++ client/app/pages/dashboards/public-dashboard-page.js | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/app/components/dashboards/ExpandedWidgetDialog.jsx b/client/app/components/dashboards/ExpandedWidgetDialog.jsx index bfc96a84bc..095607aea6 100644 --- a/client/app/components/dashboards/ExpandedWidgetDialog.jsx +++ b/client/app/components/dashboards/ExpandedWidgetDialog.jsx @@ -12,7 +12,7 @@ function ExpandedWidgetDialog({ dialog, widget }) { {...dialog.props} title={( <> - + {' '} {widget.getQuery().name} )} diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index 681209a79a..8346d929de 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -133,6 +133,8 @@ function DashboardCtrl( this.globalParameters = this.dashboard.getParametersDefs(); }; + // ANGULAR_REMOVE_ME This forces Widgets re-rendering + // use state when Dashboard is migrated to React this.forceDashboardGridReload = () => { this.dashboard.widgets = [...this.dashboard.widgets]; }; diff --git a/client/app/pages/dashboards/public-dashboard-page.js b/client/app/pages/dashboards/public-dashboard-page.js index 416b884a91..6152a9ea5e 100644 --- a/client/app/pages/dashboards/public-dashboard-page.js +++ b/client/app/pages/dashboards/public-dashboard-page.js @@ -34,6 +34,8 @@ const PublicDashboardPage = { const refreshRate = Math.max(30, parseFloat($location.search().refresh)); + // ANGULAR_REMOVE_ME This forces Widgets re-rendering + // use state when PublicDashboard is migrated to React this.forceDashboardGridReload = () => { this.dashboard.widgets = [...this.dashboard.widgets]; }; From cd69e161c3ed060df8393d28b6a76d37766aa154 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 11 Sep 2019 10:23:04 -0300 Subject: [PATCH 33/34] CR changes --- .../components/dashboards/DashboardGrid.jsx | 8 ++++-- .../dashboard-widget/TextboxWidget.jsx | 2 +- .../dashboard-widget/VisualizationWidget.jsx | 28 +++++++++++++++++-- .../dashboards/dashboard-widget/Widget.jsx | 20 ++----------- client/app/services/widget.js | 24 ++++++++++------ 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/client/app/components/dashboards/DashboardGrid.jsx b/client/app/components/dashboards/DashboardGrid.jsx index ee9e778be7..c16c85e65d 100644 --- a/client/app/components/dashboards/DashboardGrid.jsx +++ b/client/app/components/dashboards/DashboardGrid.jsx @@ -8,6 +8,7 @@ import { VisualizationWidget, TextboxWidget, RestrictedWidget } from '@/componen 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'; @@ -201,6 +202,7 @@ class DashboardGrid extends React.Component { canEdit: dashboard.canEdit(), onDelete: () => onRemoveWidget(widget.id), }; + const { type } = widget; return (
- {widget.getType() === 'visualization' && ( + {type === WidgetTypeEnum.VISUALIZATION && ( )} - {widget.getType() === 'textbox' && } - {widget.getType() === 'restricted' && } + {type === WidgetTypeEnum.TEXTBOX && } + {type === WidgetTypeEnum.RESTRICTED && }
); })} diff --git a/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx index 9fd14a333d..6ab76a7ea9 100644 --- a/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/TextboxWidget.jsx @@ -31,7 +31,7 @@ function TextboxWidget(props) { return ( - + {markdown.toHTML(text || '')} diff --git a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx index 9f5c30f096..ddf431c12f 100644 --- a/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/VisualizationWidget.jsx @@ -10,6 +10,8 @@ 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'; @@ -57,11 +59,25 @@ function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParameters ]); } -function VisualizationWidgetHeader({ widget, parameters, onParametersUpdate }) { +function RefreshIndicator({ refreshStartedAt }) { + return ( +
+
+ +
+ +
+ ); +} + +RefreshIndicator.propTypes = { refreshStartedAt: Moment.isRequired }; + +function VisualizationWidgetHeader({ widget, refreshStartedAt, parameters, onParametersUpdate }) { const canViewQuery = currentUser.hasPermission('view_query'); return ( <> +

@@ -83,11 +99,16 @@ function VisualizationWidgetHeader({ widget, parameters, onParametersUpdate }) { VisualizationWidgetHeader.propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + refreshStartedAt: Moment, parameters: PropTypes.arrayOf(PropTypes.object), onParametersUpdate: PropTypes.func, }; -VisualizationWidgetHeader.defaultProps = { onParametersUpdate: () => {}, parameters: [] }; +VisualizationWidgetHeader.defaultProps = { + refreshStartedAt: null, + onParametersUpdate: () => {}, + parameters: [], +}; function VisualizationWidgetFooter({ widget, isPublic, onRefresh, onExpand }) { const widgetQueryResult = widget.getQueryResult(); @@ -259,6 +280,7 @@ class VisualizationWidget extends React.Component { header={( @@ -271,7 +293,7 @@ class VisualizationWidget extends React.Component { onExpand={this.expandWidget} /> )} - refreshStartedAt={isRefreshing ? widget.refreshStartedAt : null} + data-refreshing={isRefreshing} > {this.renderVisualization()} diff --git a/client/app/components/dashboards/dashboard-widget/Widget.jsx b/client/app/components/dashboards/dashboard-widget/Widget.jsx index 3f75672aac..780e6487b1 100644 --- a/client/app/components/dashboards/dashboard-widget/Widget.jsx +++ b/client/app/components/dashboards/dashboard-widget/Widget.jsx @@ -7,7 +7,6 @@ import Modal from 'antd/lib/modal'; import Menu from 'antd/lib/menu'; import recordEvent from '@/services/recordEvent'; import { Moment } from '@/components/proptypes'; -import { Timer } from '@/components/Timer'; import './Widget.less'; @@ -60,19 +59,6 @@ function WidgetDeleteButton({ onClick }) { WidgetDeleteButton.propTypes = { onClick: PropTypes.func }; WidgetDeleteButton.defaultProps = { onClick: () => {} }; -function RefreshIndicator({ refreshStartedAt }) { - return ( -

-
- -
- -
- ); -} - -RefreshIndicator.propTypes = { refreshStartedAt: Moment.isRequired }; - class Widget extends React.Component { static propTypes = { widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types @@ -119,11 +105,12 @@ class Widget extends React.Component { }; render() { - const { className, children, header, footer, canEdit, isPublic, refreshStartedAt, menuOptions } = this.props; + const { widget, className, children, header, footer, canEdit, isPublic, + onDelete, menuOptions, ...otherProps } = this.props; const showDropdownButton = !isPublic && (canEdit || !isEmpty(menuOptions)); return (
-
+
{showDropdownButton && ( }
- {refreshStartedAt && } {header}
{children} diff --git a/client/app/services/widget.js b/client/app/services/widget.js index ed22a670ed..1cd5da4f14 100644 --- a/client/app/services/widget.js +++ b/client/app/services/widget.js @@ -5,6 +5,12 @@ import { registeredVisualizations } from '@/visualizations'; export let Widget = null; // eslint-disable-line import/no-mutable-exports +export const WidgetTypeEnum = { + TEXTBOX: 'textbox', + VISUALIZATION: 'visualization', + RESTRICTED: 'restricted', +}; + function calculatePositionOptions(widget) { widget.width = 1; // Backward compatibility, user on back-end @@ -93,6 +99,15 @@ function WidgetFactory($http, $location, Query) { } } + get type() { + if (this.visualization) { + return WidgetTypeEnum.VISUALIZATION; + } else if (this.restricted) { + return WidgetTypeEnum.RESTRICTED; + } + return WidgetTypeEnum.TEXTBOX; + } + getQuery() { if (!this.query && this.visualization) { this.query = new Query(this.visualization.query); @@ -112,15 +127,6 @@ function WidgetFactory($http, $location, Query) { return truncate(this.text, 20); } - getType() { - if (this.visualization) { - return 'visualization'; - } else if (this.restricted) { - return 'restricted'; - } - return 'textbox'; - } - load(force, maxAge) { if (!this.visualization) { return Promise.resolve(); From 80897e432f41f2531778f5f1bcefe58752194c5a Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Wed, 11 Sep 2019 12:30:23 -0300 Subject: [PATCH 34/34] Use plain html instead of string syntax Co-Authored-By: Ran Byron --- .../components/dashboards/dashboard-widget/RestrictedWidget.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/components/dashboards/dashboard-widget/RestrictedWidget.jsx b/client/app/components/dashboards/dashboard-widget/RestrictedWidget.jsx index 6033f08ebd..01e0b5c7d6 100644 --- a/client/app/components/dashboards/dashboard-widget/RestrictedWidget.jsx +++ b/client/app/components/dashboards/dashboard-widget/RestrictedWidget.jsx @@ -8,7 +8,7 @@ function RestrictedWidget(props) {

- {'This widget requires access to a data source you don\'t have access to.'} + This widget requires access to a data source you don't have access to.