Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ingestion) Build out UI form for Snowflake Managed Ingestion #5391

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { getSourceConfigs, jsonToYaml, yamlToJson } from '../utils';
import { YamlEditor } from './YamlEditor';
import { ANTD_GRAY } from '../../../entity/shared/constants';
import { IngestionSourceBuilderStep } from './steps';
import RecipeBuilder from './RecipeBuilder';
import { CONNECTORS_WITH_FORM } from './RecipeForm/utils';

const LOOKML_DOC_LINK = 'https://datahubproject.io/docs/generated/ingestion/sources/looker#module-lookml';

Expand Down Expand Up @@ -37,17 +39,19 @@ const ControlsContainer = styled.div`
export const DefineRecipeStep = ({ state, updateState, goTo, prev }: StepProps) => {
const existingRecipeJson = state.config?.recipe;
const existingRecipeYaml = existingRecipeJson && jsonToYaml(existingRecipeJson);
const { type } = state;
const sourceConfigs = getSourceConfigs(type as string);

const [stagedRecipeYml, setStagedRecipeYml] = useState(existingRecipeYaml || '');
const [stagedRecipeYml, setStagedRecipeYml] = useState(existingRecipeYaml || sourceConfigs.placeholderRecipe);

useEffect(() => {
setStagedRecipeYml(existingRecipeYaml || '');
if (existingRecipeYaml) {
setStagedRecipeYml(existingRecipeYaml);
}
}, [existingRecipeYaml]);

const [stepComplete, setStepComplete] = useState(false);

const { type } = state;
const sourceConfigs = getSourceConfigs(type as string);
const isEditing: boolean = prev === undefined;
const displayRecipe = stagedRecipeYml || sourceConfigs.placeholderRecipe;
const sourceDisplayName = sourceConfigs.displayName;
Expand Down Expand Up @@ -85,6 +89,19 @@ export const DefineRecipeStep = ({ state, updateState, goTo, prev }: StepProps)
goTo(IngestionSourceBuilderStep.CREATE_SCHEDULE);
};

if (type && CONNECTORS_WITH_FORM.has(type)) {
return (
<RecipeBuilder
type={type}
isEditing={isEditing}
displayRecipe={displayRecipe}
setStagedRecipe={setStagedRecipeYml}
onClickNext={onClickNext}
goToPrevious={prev}
/>
);
}

return (
<>
<Section>
Expand Down
103 changes: 103 additions & 0 deletions datahub-web-react/src/app/ingest/source/builder/RecipeBuilder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Button, message } from 'antd';
import React, { useState } from 'react';
import YAML from 'yamljs';
import { CodeOutlined, FormOutlined } from '@ant-design/icons';
import styled from 'styled-components/macro';
import { ANTD_GRAY } from '../../../entity/shared/constants';
import { YamlEditor } from './YamlEditor';
import RecipeForm from './RecipeForm/RecipeForm';

export const ControlsContainer = styled.div`
display: flex;
justify-content: space-between;
margin-top: 8px;
`;

const BorderedSection = styled.div`
display: flex;
flex-direction: column;
padding-bottom: 16px;
border: solid ${ANTD_GRAY[4]} 0.5px;
`;

const StyledButton = styled(Button)<{ isSelected: boolean }>`
${(props) =>
props.isSelected &&
`
color: #1890ff;
&:focus {
color: #1890ff;
}
`}
`;

const ButtonsWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
`;

interface Props {
type: string;
isEditing: boolean;
displayRecipe: string;
setStagedRecipe: (recipe: string) => void;
onClickNext: () => void;
goToPrevious?: () => void;
}

function RecipeBuilder(props: Props) {
const { type, isEditing, displayRecipe, setStagedRecipe, onClickNext, goToPrevious } = props;

const [isViewingForm, setIsViewingForm] = useState(true);

function switchViews(isFormView: boolean) {
try {
YAML.parse(displayRecipe);
setIsViewingForm(isFormView);
} catch (e) {
const messageText = (e as any).parsedLine
? `Fix line ${(e as any).parsedLine} in your recipe`
: 'Please fix your recipe';
message.warn(`Found invalid YAML. ${messageText} in order to switch views.`);
Comment on lines +59 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

}
}

return (
<div>
<ButtonsWrapper>
<StyledButton type="text" isSelected={isViewingForm} onClick={() => switchViews(true)}>
<FormOutlined /> Form
</StyledButton>
<StyledButton type="text" isSelected={!isViewingForm} onClick={() => switchViews(false)}>
<CodeOutlined /> YAML
</StyledButton>
</ButtonsWrapper>
{isViewingForm && (
<RecipeForm
type={type}
isEditing={isEditing}
displayRecipe={displayRecipe}
setStagedRecipe={setStagedRecipe}
onClickNext={onClickNext}
goToPrevious={goToPrevious}
/>
)}
{!isViewingForm && (
<>
<BorderedSection>
<YamlEditor initialText={displayRecipe} onChange={setStagedRecipe} />
</BorderedSection>
<ControlsContainer>
<Button disabled={isEditing} onClick={goToPrevious}>
Previous
</Button>
<Button onClick={onClickNext}>Next</Button>
</ControlsContainer>
</>
)}
</div>
);
}

export default RecipeBuilder;
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Button, Checkbox, Form, Input, Tooltip } from 'antd';
import React from 'react';
import styled from 'styled-components/macro';
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { FieldType, RecipeField } from './utils';
import { ANTD_GRAY } from '../../../../entity/shared/constants';

const Label = styled.div`
font-weight: bold;
padding-bottom: 8px;
`;

const StyledButton = styled(Button)`
color: ${ANTD_GRAY[7]};
width: 80%;
`;

const StyledQuestion = styled(QuestionCircleOutlined)`
color: rgba(0, 0, 0, 0.45);
margin-left: 4px;
`;

const StyledRemoveIcon = styled(MinusCircleOutlined)`
font-size: 14px;
margin-left: 10px;
`;

const StyledFormItem = styled(Form.Item)<{ alignLeft: boolean; removeMargin: boolean }>`
margin-bottom: ${(props) => (props.removeMargin ? '0' : '16px')};

${(props) =>
props.alignLeft &&
`
.ant-form-item {
flex-direction: row;

}

.ant-form-item-label {
padding: 0;
margin-right: 10px;
}
`}
`;

const ListWrapper = styled.div<{ removeMargin: boolean }>`
margin-bottom: ${(props) => (props.removeMargin ? '0' : '16px')};
`;

interface ListFieldProps {
field: RecipeField;
removeMargin?: boolean;
}

function ListField({ field, removeMargin }: ListFieldProps) {
return (
<Form.List name={field.name}>
{(fields, { add, remove }) => (
<ListWrapper removeMargin={!!removeMargin}>
<Label>
{field.label}
<Tooltip overlay={field.tooltip}>
<StyledQuestion />
</Tooltip>
</Label>
{fields.map((item) => (
<Form.Item key={item.fieldKey} style={{ marginBottom: '10px' }}>
<Form.Item {...item} noStyle>
<Input style={{ width: '80%' }} />
</Form.Item>
<StyledRemoveIcon onClick={() => remove(item.name)} />
</Form.Item>
))}
<StyledButton type="dashed" onClick={() => add()} style={{ width: '80%' }} icon={<PlusOutlined />}>
Add pattern
</StyledButton>
</ListWrapper>
)}
</Form.List>
);
}

interface Props {
field: RecipeField;
removeMargin?: boolean;
}

function FormField(props: Props) {
const { field, removeMargin } = props;

if (field.type === FieldType.LIST) return <ListField field={field} removeMargin={removeMargin} />;

const isBoolean = field.type === FieldType.BOOLEAN;
const input = isBoolean ? <Checkbox /> : <Input />;
const valuePropName = isBoolean ? 'checked' : 'value';
const getValueFromEvent = isBoolean ? undefined : (e) => (e.target.value === '' ? null : e.target.value);

return (
<StyledFormItem
style={isBoolean ? { flexDirection: 'row', alignItems: 'center' } : {}}
label={field.label}
name={field.name}
tooltip={field.tooltip}
rules={field.rules || undefined}
valuePropName={valuePropName}
getValueFromEvent={getValueFromEvent}
alignLeft={isBoolean}
removeMargin={!!removeMargin}
>
{input}
</StyledFormItem>
);
}

export default FormField;
Loading