Skip to content

Commit

Permalink
Migrate Widget component to React (#4020)
Browse files Browse the repository at this point in the history
* Improve sizing for Number inputs

Co-Authored-By: Ran Byron <[email protected]>

* Migrate WidgetDialog

* Start migrating Widget

* Update textbox to use HtmlContent

* QueryLink migration and some updates

* Add visualization rendering

* Render widget

* Add delete button

* Update AutoHeight

* Add widget bottom

* Add Drodpown button

* Split Widget component

* Update with #4056 and trigger netlify

* In progress: use composition

* Add header and footer

* Update widget actions positioning

* Re-render when refreshing from widget

* Add workaround to force DashboardGrid re-render

* VisualizationWidgetFooter component

* VisualizationWidget menu

* Separate RestrictedWidget

* Update tests

* Update margin for Parameters

* Remove widget files

* Revert "Improve sizing for Number inputs"

This reverts commit a02ce8f.

* Some cleanup

* Move refresh logic to the Dashboard

* Add loadingWidgets logic to the public dashboard

* Add onLoadWidget

* Remove parameter from URL when empty

* Recreate widget array instead of loadingWidgets

* Add comment about re-rendering + whitespace missing

* CR changes

* Use plain html instead of string syntax

Co-Authored-By: Ran Byron <[email protected]>
  • Loading branch information
2 people authored and arikfr committed Sep 20, 2019
1 parent e8d40bb commit cb654b3
Show file tree
Hide file tree
Showing 35 changed files with 837 additions and 473 deletions.
2 changes: 1 addition & 1 deletion client/app/assets/less/inc/visualizations/misc.less
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
visualization-renderer {
.visualization-renderer {
display: block;

.pagination,
Expand Down
2 changes: 1 addition & 1 deletion client/app/assets/less/inc/visualizations/pivot-table.less
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.pivot-table-renderer > table,
visualization-renderer > .visualization-renderer-wrapper {
.visualization-renderer > .visualization-renderer-wrapper {
overflow: auto;
}
2 changes: 1 addition & 1 deletion client/app/assets/less/redash/query.less
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ a.label-tag {
}

.pivot-table-renderer > table,
visualization-renderer > .visualization-renderer-wrapper {
.visualization-renderer > .visualization-renderer-wrapper {
overflow: visible;
}

Expand Down
2 changes: 1 addition & 1 deletion client/app/components/Parameters.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
});
};
Expand Down
40 changes: 40 additions & 0 deletions client/app/components/QueryLink.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<a href={readOnly ? null : getUrl()} className="query-link">
<VisualizationName visualization={visualization} />{' '}
<span>{query.name}</span>
</a>
);
}

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;
4 changes: 2 additions & 2 deletions client/app/components/Timer.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (<span className="rd-timer">{moment.utc(diff).format(format)}</span>);
}

Timer.propTypes = {
Expand Down
2 changes: 1 addition & 1 deletion client/app/components/dashboards/AutoHeightController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 41 additions & 19 deletions client/app/components/dashboards/DashboardGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ 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, RestrictedWidget } from '@/components/dashboards/dashboard-widget';
import { FiltersType } from '@/components/Filters';
import cfg from '@/config/dashboard-grid-options';
import AutoHeightController from './AutoHeightController';
import { WidgetTypeEnum } from '@/services/widget';

import 'react-grid-layout/css/styles.css';
import './dashboard-grid.less';
Expand Down Expand Up @@ -41,16 +42,22 @@ 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,
onParameterMappingsChange: PropTypes.func,
};

static defaultProps = {
isPublic: false,
filters: [],
onLoadWidget: () => {},
onRefreshWidget: () => {},
onRemoveWidget: () => {},
onLayoutChange: () => {},
onBreakpointChange: () => {},
onParameterMappingsChange: () => {},
};

static normalizeFrom(widget) {
Expand Down Expand Up @@ -168,7 +175,8 @@ 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 { onLoadWidget, onRefreshWidget, onRemoveWidget,
onParameterMappingsChange, filters, dashboard, isPublic, widgets } = this.props;

return (
<div className={className}>
Expand All @@ -186,23 +194,37 @@ class DashboardGrid extends React.Component {
onBreakpointChange={this.onBreakpointChange}
breakpoints={{ [MULTI]: cfg.mobileBreakPoint, [SINGLE]: 0 }}
>
{widgets.map(widget => (
<div
key={widget.id}
data-grid={DashboardGrid.normalizeFrom(widget)}
data-widgetid={widget.id}
data-test={`WidgetId${widget.id}`}
className={cx('dashboard-widget-wrapper', { 'widget-auto-height-enabled': this.autoHeightCtrl.exists(widget.id) })}
>
<DashboardWidget
widget={widget}
dashboard={dashboard}
filters={this.props.filters}
deleted={() => onRemoveWidget(widget.id)}
public={this.props.isPublic}
/>
</div>
))}
{widgets.map((widget) => {
const widgetProps = {
widget,
filters,
isPublic,
canEdit: dashboard.canEdit(),
onDelete: () => onRemoveWidget(widget.id),
};
const { type } = widget;
return (
<div
key={widget.id}
data-grid={DashboardGrid.normalizeFrom(widget)}
data-widgetid={widget.id}
data-test={`WidgetId${widget.id}`}
className={cx('dashboard-widget-wrapper', { 'widget-auto-height-enabled': this.autoHeightCtrl.exists(widget.id) })}
>
{type === WidgetTypeEnum.VISUALIZATION && (
<VisualizationWidget
{...widgetProps}
dashboard={dashboard}
onLoad={() => onLoadWidget(widget)}
onRefresh={() => onRefreshWidget(widget)}
onParameterMappingsChange={onParameterMappingsChange}
/>
)}
{type === WidgetTypeEnum.TEXTBOX && <TextboxWidget {...widgetProps} />}
{type === WidgetTypeEnum.RESTRICTED && <RestrictedWidget widget={widget} />}
</div>
);
})}
</ResponsiveGridLayout>
</div>
);
Expand Down
36 changes: 36 additions & 0 deletions client/app/components/dashboards/ExpandedWidgetDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Modal from 'antd/lib/modal';
import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { VisualizationName } from '@/visualizations/VisualizationName';

function ExpandedWidgetDialog({ dialog, widget }) {
return (
<Modal
{...dialog.props}
title={(
<>
<VisualizationName visualization={widget.visualization} />{' '}
<span>{widget.getQuery().name}</span>
</>
)}
width="95%"
footer={(<Button onClick={dialog.dismiss}>Close</Button>)}
>
<VisualizationRenderer
visualization={widget.visualization}
queryResult={widget.getQueryResult()}
context="widget"
/>
</Modal>
);
}

ExpandedWidgetDialog.propTypes = {
dialog: DialogPropType.isRequired,
widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};

export default wrapDialog(ExpandedWidgetDialog);
1 change: 0 additions & 1 deletion client/app/components/dashboards/TextboxDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions client/app/components/dashboards/dashboard-grid.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import Widget from './Widget';

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

export default RestrictedWidget;
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { markdown } from 'markdown';
import Menu from 'antd/lib/menu';
import HtmlContent from '@/components/HtmlContent';
import TextboxDialog from '@/components/dashboards/TextboxDialog';
import Widget from './Widget';

function TextboxWidget(props) {
const { widget, canEdit } = props;
const [text, setText] = useState(widget.text);

const editTextBox = () => {
TextboxDialog.showModal({
text: widget.text,
onConfirm: (newText) => {
widget.text = newText;
setText(newText);
return widget.save();
},
});
};

const TextboxMenuOptions = [
<Menu.Item key="edit" onClick={editTextBox}>Edit</Menu.Item>,
];

if (!widget.width) {
return null;
}

return (
<Widget {...props} menuOptions={canEdit ? TextboxMenuOptions : null} className="widget-text">
<HtmlContent className="body-row-auto scrollbox t-body p-15 markdown">
{markdown.toHTML(text || '')}
</HtmlContent>
</Widget>
);
}

TextboxWidget.propTypes = {
widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
canEdit: PropTypes.bool,
};

TextboxWidget.defaultProps = {
canEdit: false,
};

export default TextboxWidget;
Loading

0 comments on commit cb654b3

Please sign in to comment.