-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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) Add frontend connection test for Snowflake #5520
Changes from all commits
fec7e01
8f87bc3
afd59ee
490081f
91ab833
341ad79
77f0f3b
855a46d
13ada49
14fef4c
543aa1b
fc7db7e
b6d6869
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -133,6 +133,29 @@ type ExecutionRequestResult { | |
""" | ||
report: String | ||
|
||
""" | ||
A structured report for this Execution Request | ||
""" | ||
structuredReport: StructuredReport | ||
|
||
} | ||
|
||
""" | ||
A flexible carrier for structured results of an execution request. | ||
""" | ||
type StructuredReport { | ||
""" | ||
The type of the structured report. (e.g. INGESTION_REPORT, TEST_CONNECTION_REPORT, etc.) | ||
""" | ||
type: String! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the future we may actually want to use an enum here! This doesn't seem to be changing super often There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. totally agreed! yeah once we lock it in I'd like to be more strict on types. it seems to be pretty much there though |
||
""" | ||
The serialized value of the structured report | ||
""" | ||
serializedValue: String! | ||
""" | ||
The content-type of the serialized value (e.g. application/json, application/json;gzip etc.) | ||
""" | ||
contentType: String! | ||
} | ||
|
||
""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { CheckOutlined, CloseOutlined, QuestionCircleOutlined } from '@ant-design/icons'; | ||
import { Tooltip } from 'antd'; | ||
import React from 'react'; | ||
import { green, red } from '@ant-design/colors'; | ||
import styled from 'styled-components/macro'; | ||
import { ANTD_GRAY } from '../../../../../entity/shared/constants'; | ||
|
||
const CapabilityWrapper = styled.div` | ||
align-items: center; | ||
display: flex; | ||
margin: 10px 0; | ||
`; | ||
|
||
const CapabilityName = styled.span` | ||
color: ${ANTD_GRAY[8]}; | ||
font-size: 18px; | ||
margin-right: 12px; | ||
`; | ||
|
||
const CapabilityMessage = styled.span<{ success: boolean }>` | ||
color: ${(props) => (props.success ? `${green[6]}` : `${red[5]}`)}; | ||
font-size: 12px; | ||
flex: 1; | ||
padding-left: 4px; | ||
`; | ||
|
||
const StyledQuestion = styled(QuestionCircleOutlined)` | ||
color: rgba(0, 0, 0, 0.45); | ||
margin-left: 4px; | ||
`; | ||
|
||
export const StyledCheck = styled(CheckOutlined)` | ||
color: ${green[6]}; | ||
margin-right: 15px; | ||
`; | ||
|
||
export const StyledClose = styled(CloseOutlined)` | ||
color: ${red[5]}; | ||
margin-right: 15px; | ||
`; | ||
|
||
const NumberWrapper = styled.span` | ||
margin-right: 8px; | ||
`; | ||
|
||
interface Props { | ||
success: boolean; | ||
capability: string; | ||
displayMessage: string | null; | ||
tooltipMessage: string | null; | ||
number?: number; | ||
} | ||
|
||
function ConnectionCapabilityView({ success, capability, displayMessage, tooltipMessage, number }: Props) { | ||
return ( | ||
<CapabilityWrapper> | ||
<CapabilityName> | ||
{success ? <StyledCheck /> : <StyledClose />} | ||
{number ? <NumberWrapper>{number}.</NumberWrapper> : ''} | ||
{capability} | ||
</CapabilityName> | ||
<CapabilityMessage success={success}> | ||
{displayMessage} | ||
{tooltipMessage && ( | ||
<Tooltip overlay={tooltipMessage}> | ||
<StyledQuestion /> | ||
</Tooltip> | ||
)} | ||
</CapabilityMessage> | ||
</CapabilityWrapper> | ||
); | ||
} | ||
|
||
export default ConnectionCapabilityView; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { CheckCircleOutlined } from '@ant-design/icons'; | ||
import { Button, message } from 'antd'; | ||
import React, { useEffect, useState } from 'react'; | ||
import { green } from '@ant-design/colors'; | ||
import { | ||
useCreateTestConnectionRequestMutation, | ||
useGetIngestionExecutionRequestLazyQuery, | ||
} from '../../../../../../graphql/ingestion.generated'; | ||
import { FAILURE, getSourceConfigs, RUNNING, yamlToJson } from '../../../utils'; | ||
import { TestConnectionResult } from './types'; | ||
import TestConnectionModal from './TestConnectionModal'; | ||
|
||
export function getRecipeJson(recipeYaml: string) { | ||
// Convert the recipe into it's json representation, and catch + report exceptions while we do it. | ||
let recipeJson; | ||
try { | ||
recipeJson = yamlToJson(recipeYaml); | ||
} catch (e) { | ||
const messageText = (e as any).parsedLine | ||
? `Please fix line ${(e as any).parsedLine} in your recipe.` | ||
: 'Please check your recipe configuration.'; | ||
message.warn(`Found invalid YAML. ${messageText}`); | ||
return null; | ||
} | ||
return recipeJson; | ||
} | ||
|
||
interface Props { | ||
type: string; | ||
recipe: string; | ||
} | ||
|
||
function TestConnectionButton(props: Props) { | ||
const { type, recipe } = props; | ||
const [isLoading, setIsLoading] = useState(false); | ||
const [isModalVisible, setIsModalVisible] = useState(false); | ||
const [pollingInterval, setPollingInterval] = useState<null | NodeJS.Timeout>(null); | ||
const [testConnectionResult, setTestConnectionResult] = useState<null | TestConnectionResult>(null); | ||
const [createTestConnectionRequest, { data: requestData }] = useCreateTestConnectionRequestMutation(); | ||
const [getIngestionExecutionRequest, { data: resultData, loading }] = useGetIngestionExecutionRequestLazyQuery(); | ||
|
||
const sourceConfigs = getSourceConfigs(type); | ||
|
||
useEffect(() => { | ||
if (requestData && requestData.createTestConnectionRequest) { | ||
const interval = setInterval( | ||
() => | ||
getIngestionExecutionRequest({ | ||
variables: { urn: requestData.createTestConnectionRequest as string }, | ||
}), | ||
2000, | ||
); | ||
setIsLoading(true); | ||
setIsModalVisible(true); | ||
setPollingInterval(interval); | ||
} | ||
}, [requestData, getIngestionExecutionRequest]); | ||
|
||
useEffect(() => { | ||
if (!loading && resultData) { | ||
const result = resultData.executionRequest?.result; | ||
if (result && result.status !== RUNNING) { | ||
if (result.status === FAILURE) { | ||
message.error( | ||
'Something went wrong with your connection test. Please check your recipe and try again.', | ||
); | ||
setIsModalVisible(false); | ||
} | ||
if (result.structuredReport) { | ||
const testConnectionReport = JSON.parse(result.structuredReport.serializedValue); | ||
setTestConnectionResult(testConnectionReport); | ||
} | ||
if (pollingInterval) clearInterval(pollingInterval); | ||
setIsLoading(false); | ||
} | ||
} | ||
}, [resultData, pollingInterval, loading]); | ||
|
||
useEffect(() => { | ||
if (!isModalVisible && pollingInterval) { | ||
clearInterval(pollingInterval); | ||
} | ||
}, [isModalVisible, pollingInterval]); | ||
|
||
function testConnection() { | ||
const recipeJson = getRecipeJson(recipe); | ||
if (recipeJson) { | ||
createTestConnectionRequest({ variables: { input: { recipe: recipeJson } } }) | ||
.then((res) => | ||
getIngestionExecutionRequest({ | ||
variables: { urn: res.data?.createTestConnectionRequest as string }, | ||
}), | ||
) | ||
.catch(() => { | ||
message.error( | ||
'There was an unexpected error when trying to test your connection. Please try again.', | ||
); | ||
}); | ||
|
||
setIsLoading(true); | ||
setIsModalVisible(true); | ||
} | ||
} | ||
|
||
const internalFailure = !!testConnectionResult?.internal_failure; | ||
const basicConnectivityFailure = testConnectionResult?.basic_connectivity?.capable === false; | ||
const testConnectionFailed = internalFailure || basicConnectivityFailure; | ||
|
||
return ( | ||
<> | ||
<Button onClick={testConnection}> | ||
<CheckCircleOutlined style={{ color: green[5] }} /> | ||
Test Connection | ||
</Button> | ||
{isModalVisible && ( | ||
<TestConnectionModal | ||
isLoading={isLoading} | ||
testConnectionFailed={testConnectionFailed} | ||
sourceConfig={sourceConfigs} | ||
testConnectionResult={testConnectionResult} | ||
hideModal={() => setIsModalVisible(false)} | ||
/> | ||
)} | ||
</> | ||
); | ||
} | ||
|
||
export default TestConnectionButton; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will use the latest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct - Shirshanka just pushed a change to use latest if we don't supply a cli version