From 341d85589c7a3349003c1b274a30b18c7ff7ddf6 Mon Sep 17 00:00:00 2001 From: Adam Dobrawy <ad-m@users.noreply.github.com> Date: Mon, 31 Jan 2022 19:03:39 +0100 Subject: [PATCH] refactor: upgrade ControlHeader to TSX & FC and add storybook (#18188) * refactor: upgrade ControlHeader to TSX & FC and add storybook * fix: Add story for error in ControlHeader * apply review comments --- .../src/explore/components/ControlHeader.jsx | 174 ------------------ .../components/ControlHeader.stories.tsx | 77 ++++++++ .../src/explore/components/ControlHeader.tsx | 158 ++++++++++++++++ 3 files changed, 235 insertions(+), 174 deletions(-) delete mode 100644 superset-frontend/src/explore/components/ControlHeader.jsx create mode 100644 superset-frontend/src/explore/components/ControlHeader.stories.tsx create mode 100644 superset-frontend/src/explore/components/ControlHeader.tsx diff --git a/superset-frontend/src/explore/components/ControlHeader.jsx b/superset-frontend/src/explore/components/ControlHeader.jsx deleted file mode 100644 index b78733135db25..0000000000000 --- a/superset-frontend/src/explore/components/ControlHeader.jsx +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { t, css, withTheme } from '@superset-ui/core'; -import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; -import { Tooltip } from 'src/components/Tooltip'; -import { FormLabel } from 'src/components/Form'; -import Icons from 'src/components/Icons'; - -const propTypes = { - name: PropTypes.string, - label: PropTypes.node, - description: PropTypes.node, - validationErrors: PropTypes.array, - renderTrigger: PropTypes.bool, - rightNode: PropTypes.node, - leftNode: PropTypes.node, - onClick: PropTypes.func, - hovered: PropTypes.bool, - tooltipOnClick: PropTypes.func, - warning: PropTypes.string, - danger: PropTypes.string, -}; - -const defaultProps = { - validationErrors: [], - renderTrigger: false, - hovered: false, - name: undefined, -}; - -class ControlHeader extends React.Component { - renderOptionalIcons() { - if (this.props.hovered) { - return ( - <span - css={theme => css` - position: absolute; - top: 50%; - right: 0; - padding-left: ${theme.gridUnit}px; - transform: translate(100%, -50%); - white-space: nowrap; - `} - > - {this.props.description && ( - <span> - <InfoTooltipWithTrigger - label={t('description')} - tooltip={this.props.description} - placement="top" - onClick={this.props.tooltipOnClick} - />{' '} - </span> - )} - {this.props.renderTrigger && ( - <span> - <InfoTooltipWithTrigger - label={t('bolt')} - tooltip={t('Changing this control takes effect instantly')} - placement="top" - icon="bolt" - />{' '} - </span> - )} - </span> - ); - } - return null; - } - - render() { - if (!this.props.label) { - return null; - } - const labelClass = - this.props.validationErrors.length > 0 ? 'text-danger' : ''; - - const { theme } = this.props; - - return ( - <div className="ControlHeader" data-test={`${this.props.name}-header`}> - <div className="pull-left"> - <FormLabel - css={{ - marginBottom: 0, - position: 'relative', - }} - > - {this.props.leftNode && <span>{this.props.leftNode}</span>} - <span - role="button" - tabIndex={0} - onClick={this.props.onClick} - className={labelClass} - style={{ cursor: this.props.onClick ? 'pointer' : '' }} - > - {this.props.label} - </span>{' '} - {this.props.warning && ( - <span> - <Tooltip - id="error-tooltip" - placement="top" - title={this.props.warning} - > - <Icons.AlertSolid - iconColor={theme.colors.alert.base} - iconSize="s" - /> - </Tooltip>{' '} - </span> - )} - {this.props.danger && ( - <span> - <Tooltip - id="error-tooltip" - placement="top" - title={this.props.danger} - > - <Icons.ErrorSolid - iconColor={theme.colors.error.base} - iconSize="s" - /> - </Tooltip>{' '} - </span> - )} - {this.props.validationErrors.length > 0 && ( - <span> - <Tooltip - id="error-tooltip" - placement="top" - title={this.props.validationErrors.join(' ')} - > - <Icons.ErrorSolid - iconColor={theme.colors.error.base} - iconSize="s" - /> - </Tooltip>{' '} - </span> - )} - {this.renderOptionalIcons()} - </FormLabel> - </div> - {this.props.rightNode && ( - <div className="pull-right">{this.props.rightNode}</div> - )} - <div className="clearfix" /> - </div> - ); - } -} - -ControlHeader.propTypes = propTypes; -ControlHeader.defaultProps = defaultProps; - -export default withTheme(ControlHeader); diff --git a/superset-frontend/src/explore/components/ControlHeader.stories.tsx b/superset-frontend/src/explore/components/ControlHeader.stories.tsx new file mode 100644 index 0000000000000..af9df9dece8d3 --- /dev/null +++ b/superset-frontend/src/explore/components/ControlHeader.stories.tsx @@ -0,0 +1,77 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import ControlHeader, { ControlHeaderProps } from './ControlHeader'; + +export default { + title: 'ControlHeader', + component: ControlHeader, +}; + +const options: { + [key: string]: ControlHeaderProps; +} = { + label: { + label: 'Control label', + }, + warning: { + label: 'Control warning', + warning: 'Example of warning message', + }, + error: { + label: 'Control error', + validationErrors: ['Something is wrong'], + }, +}; + +export const ControlHeaderGallery = () => ( + <> + {Object.entries(options).map(([name, props]) => ( + <> + <h4>{name}</h4> + <ControlHeader {...props} /> + </> + ))} + </> +); + +export const InteractiveControlHeader = (props: ControlHeaderProps) => ( + <ControlHeader {...props} /> +); + +InteractiveControlHeader.args = { + label: 'example label', + description: 'example description', + warning: 'example warning', + renderTrigger: false, + hovered: false, +}; + +InteractiveControlHeader.argTypes = { + tooltipOnClick: { action: 'tooltipOnClick' }, + onClick: { action: 'onClick' }, +}; + +InteractiveControlHeader.story = { + parameters: { + knobs: { + disable: true, + }, + }, +}; diff --git a/superset-frontend/src/explore/components/ControlHeader.tsx b/superset-frontend/src/explore/components/ControlHeader.tsx new file mode 100644 index 0000000000000..ce240704b5d3f --- /dev/null +++ b/superset-frontend/src/explore/components/ControlHeader.tsx @@ -0,0 +1,158 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { FC, ReactNode } from 'react'; +import { t, css, useTheme } from '@superset-ui/core'; +import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; +import { Tooltip } from 'src/components/Tooltip'; +import { FormLabel } from 'src/components/Form'; +import Icons from 'src/components/Icons'; + +type ValidationError = string; + +export type ControlHeaderProps = { + name?: string; + label?: ReactNode; + description?: ReactNode; + validationErrors?: ValidationError[]; + renderTrigger?: boolean; + rightNode?: ReactNode; + leftNode?: ReactNode; + onClick?: () => void; + hovered?: boolean; + tooltipOnClick?: () => void; + warning?: string; + danger?: string; +}; + +const ControlHeader: FC<ControlHeaderProps> = ({ + name, + label, + description, + validationErrors = [], + renderTrigger = false, + rightNode, + leftNode, + onClick, + hovered = false, + tooltipOnClick = () => {}, + warning, + danger, +}) => { + const { gridUnit, colors } = useTheme(); + + if (!label) { + return null; + } + + const renderOptionalIcons = () => { + if (!hovered) { + return null; + } + + return ( + <span + css={() => css` + position: absolute; + top: 50%; + right: 0; + padding-left: ${gridUnit}px; + transform: translate(100%, -50%); + white-space: nowrap; + `} + > + {description && ( + <span> + <InfoTooltipWithTrigger + label={t('description')} + tooltip={description} + placement="top" + onClick={tooltipOnClick} + />{' '} + </span> + )} + {renderTrigger && ( + <span> + <InfoTooltipWithTrigger + label={t('bolt')} + tooltip={t('Changing this control takes effect instantly')} + placement="top" + icon="bolt" + />{' '} + </span> + )} + </span> + ); + }; + + const labelClass = validationErrors?.length > 0 ? 'text-danger' : ''; + + return ( + <div className="ControlHeader" data-test={`${name}-header`}> + <div className="pull-left"> + <FormLabel + css={{ + marginBottom: 0, + position: 'relative', + }} + > + {leftNode && <span>{leftNode}</span>} + <span + role="button" + tabIndex={0} + onClick={onClick} + className={labelClass} + style={{ cursor: onClick ? 'pointer' : '' }} + > + {label} + </span>{' '} + {warning && ( + <span> + <Tooltip id="error-tooltip" placement="top" title={warning}> + <Icons.AlertSolid iconColor={colors.alert.base} iconSize="s" /> + </Tooltip>{' '} + </span> + )} + {danger && ( + <span> + <Tooltip id="error-tooltip" placement="top" title={danger}> + <Icons.ErrorSolid iconColor={colors.error.base} iconSize="s" /> + </Tooltip>{' '} + </span> + )} + {validationErrors?.length > 0 && ( + <span> + <Tooltip + id="error-tooltip" + placement="top" + title={validationErrors?.join(' ')} + > + <Icons.ErrorSolid iconColor={colors.error.base} iconSize="s" /> + </Tooltip>{' '} + </span> + )} + {renderOptionalIcons()} + </FormLabel> + </div> + {rightNode && <div className="pull-right">{rightNode}</div>} + <div className="clearfix" /> + </div> + ); +}; + +export default ControlHeader;