diff --git a/superset-frontend/src/components/Form/FormLabel.tsx b/superset-frontend/src/components/Form/FormLabel.tsx
index cb4cba0793..3e0faea0e9 100644
--- a/superset-frontend/src/components/Form/FormLabel.tsx
+++ b/superset-frontend/src/components/Form/FormLabel.tsx
@@ -40,6 +40,7 @@ const RequiredLabel = styled.label`
margin-bottom: ${({ theme }) => theme.gridUnit}px;
&::after {
display: inline-block;
+ margin-left: ${({ theme }) => theme.gridUnit}px;
color: ${({ theme }) => theme.colors.error.base};
font-size: ${({ theme }) => theme.typography.sizes.m}px;
content: '*';
diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.stories.tsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.stories.tsx
new file mode 100644
index 0000000000..6061848d85
--- /dev/null
+++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.stories.tsx
@@ -0,0 +1,79 @@
+/**
+ * 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, { useState } from 'react';
+import LabeledErrorBoundInput, {
+ LabeledErrorBoundInputProps,
+} from './LabeledErrorBoundInput';
+
+export default {
+ title: 'LabeledErrorBoundInput',
+ component: LabeledErrorBoundInput,
+};
+
+export const InteractiveLabeledErrorBoundInput = ({
+ name,
+ value,
+ placeholder,
+ type,
+ id,
+}: LabeledErrorBoundInputProps) => {
+ const [currentValue, setCurrentValue] = useState(value);
+
+ const validateFunctionality: (value: any) => string = value => {
+ setCurrentValue(value.target.value);
+ if (value.target.value.includes('success')) {
+ return 'success';
+ }
+ return 'error';
+ };
+
+ return (
+
+ );
+};
+
+InteractiveLabeledErrorBoundInput.args = {
+ name: 'Username',
+ placeholder: 'Example placeholder text...',
+ id: 1,
+};
+
+InteractiveLabeledErrorBoundInput.argTypes = {
+ type: {
+ defaultValue: 'textbox',
+ control: {
+ type: 'select',
+ options: ['textbox', 'checkbox', 'radio'],
+ },
+ },
+};
diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.test.jsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.test.jsx
new file mode 100644
index 0000000000..15f956fd57
--- /dev/null
+++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.test.jsx
@@ -0,0 +1,61 @@
+/**
+ * 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 { render, screen } from 'spec/helpers/testing-library';
+import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput';
+
+const defaultProps = {
+ id: 1,
+ label: 'Username',
+ name: 'Username',
+ validationMethods: () => {},
+ errorMessage: '',
+ helpText: 'This is a line of example help text',
+ value: '',
+ placeholder: 'Example placeholder text...',
+ type: 'textbox',
+};
+
+describe('LabeledErrorBoundInput', () => {
+ it('renders a LabeledErrorBoundInput normally, without an error', () => {
+ render();
+
+ const label = screen.getByText(/username/i);
+ const textboxInput = screen.getByRole('textbox');
+ const helperText = screen.getByText('This is a line of example help text');
+
+ expect(label).toBeVisible();
+ expect(textboxInput).toBeVisible();
+ expect(helperText).toBeVisible();
+ });
+
+ it('renders a LabeledErrorBoundInput with an error', () => {
+ // Pass an error into props, causing errorText to replace helperText
+ defaultProps.errorMessage = 'Example error message';
+ render();
+
+ const label = screen.getByText(/username/i);
+ const textboxInput = screen.getByRole('textbox');
+ const errorText = screen.getByText(/example error message/i);
+
+ expect(label).toBeVisible();
+ expect(textboxInput).toBeVisible();
+ expect(errorText).toBeVisible();
+ });
+});
diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx
new file mode 100644
index 0000000000..8569b554a0
--- /dev/null
+++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx
@@ -0,0 +1,89 @@
+/**
+ * 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 { Input } from 'antd';
+import { styled, css, SupersetTheme } from '@superset-ui/core';
+import FormItem from './FormItem';
+import FormLabel from './FormLabel';
+
+export interface LabeledErrorBoundInputProps {
+ label?: string;
+ validationMethods:
+ | { onBlur: (value: any) => string }
+ | { onChange: (value: any) => string };
+ errorMessage: string | null;
+ helpText?: string;
+ required?: boolean;
+ id?: string;
+ [x: string]: any;
+}
+
+const StyledInput = styled(Input)`
+ margin: 8px 0;
+`;
+
+const alertIconStyles = (theme: SupersetTheme, hasError: boolean) => css`
+ .ant-form-item-children-icon {
+ display: none;
+ }
+ ${hasError &&
+ `.ant-form-item-control-input-content {
+ position: relative;
+
+ &:after {
+ content: ' ';
+ display: inline-block;
+ background: ${theme.colors.error.base};
+ mask: url('/images/icons/error.svg');
+ mask-size: cover;
+ width: ${theme.gridUnit * 4}px;
+ height: ${theme.gridUnit * 4}px;
+ position: absolute;
+ right: 7px;
+ top: 15px;
+ }
+ }`}
+`;
+
+const LabeledErrorBoundInput = ({
+ label,
+ validationMethods,
+ errorMessage,
+ helpText,
+ required = false,
+ id,
+ ...props
+}: LabeledErrorBoundInputProps) => (
+ <>
+
+ {label}
+
+ alertIconStyles(theme, !!errorMessage)}
+ validateTrigger={Object.keys(validationMethods)}
+ validateStatus={errorMessage ? 'error' : 'success'}
+ help={errorMessage || helpText}
+ hasFeedback={!!errorMessage}
+ >
+
+
+ >
+);
+
+export default LabeledErrorBoundInput;
diff --git a/superset-frontend/src/components/Form/index.tsx b/superset-frontend/src/components/Form/index.tsx
index f0734a254c..7d7a60745d 100644
--- a/superset-frontend/src/components/Form/index.tsx
+++ b/superset-frontend/src/components/Form/index.tsx
@@ -19,5 +19,6 @@
import Form from './Form';
import FormItem from './FormItem';
import FormLabel from './FormLabel';
+import LabeledErrorBoundInput from './LabeledErrorBoundInput';
-export { Form, FormItem, FormLabel };
+export { Form, FormItem, FormLabel, LabeledErrorBoundInput };