Skip to content

Commit

Permalink
feat(ingestion) Build out UI form for Snowflake Managed Ingestion (da…
Browse files Browse the repository at this point in the history
  • Loading branch information
chriscollins3456 authored and maggiehays committed Aug 1, 2022
1 parent d134ee8 commit fe17418
Show file tree
Hide file tree
Showing 8 changed files with 841 additions and 45 deletions.
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.`);
}
}

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

0 comments on commit fe17418

Please sign in to comment.