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;