mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-12 09:23:52 +00:00
feat(ui/ingest): add analytics events (#14557)
This commit is contained in:
parent
22e382d1be
commit
6044d8d298
@ -194,4 +194,17 @@ yarn type-check
|
|||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
yarn test
|
yarn test
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
yarn test path/to/file.test.tsx --run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Writing Tests - Best Practices & Common Pitfalls
|
||||||
|
|
||||||
|
### Test Setup Essentials
|
||||||
|
|
||||||
|
**Always use the existing test infrastructure:**
|
||||||
|
|
||||||
|
- Use `TestPageContainer` from `@utils/test-utils/TestPageContainer` - it provides all necessary providers
|
||||||
|
- Use `MockedProvider` from `@apollo/client/testing` for GraphQL components
|
||||||
|
- Use Vitest with React Testing Library
|
||||||
|
|||||||
@ -75,6 +75,9 @@ export enum EventType {
|
|||||||
CreateGlossaryEntityEvent,
|
CreateGlossaryEntityEvent,
|
||||||
CreateDomainEvent,
|
CreateDomainEvent,
|
||||||
MoveDomainEvent,
|
MoveDomainEvent,
|
||||||
|
IngestionTestConnectionEvent,
|
||||||
|
IngestionExecutionResultViewedEvent,
|
||||||
|
IngestionSourceConfigurationImpressionEvent,
|
||||||
CreateIngestionSourceEvent,
|
CreateIngestionSourceEvent,
|
||||||
UpdateIngestionSourceEvent,
|
UpdateIngestionSourceEvent,
|
||||||
DeleteIngestionSourceEvent,
|
DeleteIngestionSourceEvent,
|
||||||
@ -610,18 +613,43 @@ export interface MoveDomainEvent extends BaseEvent {
|
|||||||
|
|
||||||
// Managed Ingestion Events
|
// Managed Ingestion Events
|
||||||
|
|
||||||
|
export interface IngestionTestConnectionEvent extends BaseEvent {
|
||||||
|
type: EventType.IngestionTestConnectionEvent;
|
||||||
|
sourceType: string;
|
||||||
|
sourceUrn?: string;
|
||||||
|
outcome?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngestionExecutionResultViewedEvent extends BaseEvent {
|
||||||
|
type: EventType.IngestionExecutionResultViewedEvent;
|
||||||
|
executionUrn: string;
|
||||||
|
executionStatus: string;
|
||||||
|
viewedSection: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngestionSourceConfigurationImpressionEvent extends BaseEvent {
|
||||||
|
type: EventType.IngestionSourceConfigurationImpressionEvent;
|
||||||
|
viewedSection: 'SELECT_TEMPLATE' | 'DEFINE_RECIPE' | 'CREATE_SCHEDULE' | 'NAME_SOURCE';
|
||||||
|
sourceType?: string;
|
||||||
|
sourceUrn?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateIngestionSourceEvent extends BaseEvent {
|
export interface CreateIngestionSourceEvent extends BaseEvent {
|
||||||
type: EventType.CreateIngestionSourceEvent;
|
type: EventType.CreateIngestionSourceEvent;
|
||||||
sourceType: string;
|
sourceType: string;
|
||||||
|
sourceUrn?: string;
|
||||||
interval?: string;
|
interval?: string;
|
||||||
numOwners?: number;
|
numOwners?: number;
|
||||||
|
outcome?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateIngestionSourceEvent extends BaseEvent {
|
export interface UpdateIngestionSourceEvent extends BaseEvent {
|
||||||
type: EventType.UpdateIngestionSourceEvent;
|
type: EventType.UpdateIngestionSourceEvent;
|
||||||
sourceType: string;
|
sourceType: string;
|
||||||
|
sourceUrn: string;
|
||||||
interval?: string;
|
interval?: string;
|
||||||
numOwners?: number;
|
numOwners?: number;
|
||||||
|
outcome?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteIngestionSourceEvent extends BaseEvent {
|
export interface DeleteIngestionSourceEvent extends BaseEvent {
|
||||||
@ -630,6 +658,8 @@ export interface DeleteIngestionSourceEvent extends BaseEvent {
|
|||||||
|
|
||||||
export interface ExecuteIngestionSourceEvent extends BaseEvent {
|
export interface ExecuteIngestionSourceEvent extends BaseEvent {
|
||||||
type: EventType.ExecuteIngestionSourceEvent;
|
type: EventType.ExecuteIngestionSourceEvent;
|
||||||
|
sourceType?: string;
|
||||||
|
sourceUrn?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Find a way to use this event
|
// TODO: Find a way to use this event
|
||||||
@ -1216,4 +1246,7 @@ export type Event =
|
|||||||
| WelcomeToDataHubModalInteractEvent
|
| WelcomeToDataHubModalInteractEvent
|
||||||
| WelcomeToDataHubModalExitEvent
|
| WelcomeToDataHubModalExitEvent
|
||||||
| WelcomeToDataHubModalClickViewDocumentationEvent
|
| WelcomeToDataHubModalClickViewDocumentationEvent
|
||||||
| ProductTourButtonClickEvent;
|
| ProductTourButtonClickEvent
|
||||||
|
| IngestionTestConnectionEvent
|
||||||
|
| IngestionExecutionResultViewedEvent
|
||||||
|
| IngestionSourceConfigurationImpressionEvent;
|
||||||
|
|||||||
@ -256,6 +256,8 @@ export const IngestionSourceList = ({ showCreateModal, setShowCreateModal }: Pro
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
analytics.event({
|
analytics.event({
|
||||||
type: EventType.ExecuteIngestionSourceEvent,
|
type: EventType.ExecuteIngestionSourceEvent,
|
||||||
|
sourceType: focusSource?.type,
|
||||||
|
sourceUrn: focusSource?.urn,
|
||||||
});
|
});
|
||||||
message.success({
|
message.success({
|
||||||
content: `Successfully submitted ingestion execution request!`,
|
content: `Successfully submitted ingestion execution request!`,
|
||||||
@ -308,6 +310,7 @@ export const IngestionSourceList = ({ showCreateModal, setShowCreateModal }: Pro
|
|||||||
analytics.event({
|
analytics.event({
|
||||||
type: EventType.UpdateIngestionSourceEvent,
|
type: EventType.UpdateIngestionSourceEvent,
|
||||||
sourceType: input.type,
|
sourceType: input.type,
|
||||||
|
sourceUrn: focusSourceUrn,
|
||||||
interval: input.schedule?.interval,
|
interval: input.schedule?.interval,
|
||||||
});
|
});
|
||||||
message.success({
|
message.success({
|
||||||
@ -359,6 +362,7 @@ export const IngestionSourceList = ({ showCreateModal, setShowCreateModal }: Pro
|
|||||||
analytics.event({
|
analytics.event({
|
||||||
type: EventType.CreateIngestionSourceEvent,
|
type: EventType.CreateIngestionSourceEvent,
|
||||||
sourceType: input.type,
|
sourceType: input.type,
|
||||||
|
sourceUrn: newSource.urn,
|
||||||
interval: input.schedule?.interval,
|
interval: input.schedule?.interval,
|
||||||
});
|
});
|
||||||
message.success({
|
message.success({
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import { Icon, Modal, Pill } from '@components';
|
import { Icon, Modal, Pill } from '@components';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { Tab, Tabs } from '@components/components/Tabs/Tabs';
|
import { Tab, Tabs } from '@components/components/Tabs/Tabs';
|
||||||
|
|
||||||
|
import analytics, { EventType } from '@app/analytics';
|
||||||
import { LogsTab } from '@app/ingestV2/executions/components/LogsTab';
|
import { LogsTab } from '@app/ingestV2/executions/components/LogsTab';
|
||||||
import { RecipeTab } from '@app/ingestV2/executions/components/RecipeTab';
|
import { RecipeTab } from '@app/ingestV2/executions/components/RecipeTab';
|
||||||
import { SummaryTab } from '@app/ingestV2/executions/components/SummaryTab';
|
import { SummaryTab } from '@app/ingestV2/executions/components/SummaryTab';
|
||||||
@ -38,6 +39,30 @@ export const ExecutionDetailsModal = ({ urn, open, onClose }: Props) => {
|
|||||||
const status = getIngestionSourceStatus(result);
|
const status = getIngestionSourceStatus(result);
|
||||||
const [selectedTab, setSelectedTab] = useState<TabType>(TabType.Summary);
|
const [selectedTab, setSelectedTab] = useState<TabType>(TabType.Summary);
|
||||||
|
|
||||||
|
const sendAnalyticsTabViewedEvent = useCallback(
|
||||||
|
(tab: TabType) => {
|
||||||
|
if (!result) return;
|
||||||
|
analytics.event({
|
||||||
|
type: EventType.IngestionExecutionResultViewedEvent,
|
||||||
|
executionUrn: urn,
|
||||||
|
executionStatus: status || 'UNKNOWN',
|
||||||
|
viewedSection: tab,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[result, urn, status],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectTab = (tab: TabType) => {
|
||||||
|
setSelectedTab(tab);
|
||||||
|
sendAnalyticsTabViewedEvent(tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
sendAnalyticsTabViewedEvent(TabType.Summary);
|
||||||
|
}
|
||||||
|
}, [open, urn, status, sendAnalyticsTabViewedEvent]);
|
||||||
|
|
||||||
const ResultIcon = status && getExecutionRequestStatusIcon(status);
|
const ResultIcon = status && getExecutionRequestStatusIcon(status);
|
||||||
const resultColor = status ? getExecutionRequestStatusDisplayColor(status) : 'gray';
|
const resultColor = status ? getExecutionRequestStatusDisplayColor(status) : 'gray';
|
||||||
const titlePill = status && ResultIcon && (
|
const titlePill = status && ResultIcon && (
|
||||||
@ -106,7 +131,7 @@ export const ExecutionDetailsModal = ({ urn, open, onClose }: Props) => {
|
|||||||
<Tabs
|
<Tabs
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
selectedTab={selectedTab}
|
selectedTab={selectedTab}
|
||||||
onChange={(tab) => setSelectedTab(tab as TabType)}
|
onChange={(tab) => selectTab(tab as TabType)}
|
||||||
getCurrentUrl={() => window.location.pathname}
|
getCurrentUrl={() => window.location.pathname}
|
||||||
scrollToTopOnChange
|
scrollToTopOnChange
|
||||||
maxHeight="80vh"
|
maxHeight="80vh"
|
||||||
|
|||||||
@ -272,6 +272,8 @@ export const IngestionSourceList = ({
|
|||||||
|
|
||||||
const focusSource = finalSources.find((s) => s.urn === focusSourceUrn);
|
const focusSource = finalSources.find((s) => s.urn === focusSourceUrn);
|
||||||
const isLastPage = totalSources <= pageSize * page;
|
const isLastPage = totalSources <= pageSize * page;
|
||||||
|
// this is required when the ingestion source has not been created
|
||||||
|
const [selectedSourceType, setSelectedSourceType] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sources = (data?.listIngestionSources?.ingestionSources || []) as IngestionSource[];
|
const sources = (data?.listIngestionSources?.ingestionSources || []) as IngestionSource[];
|
||||||
@ -392,8 +394,10 @@ export const IngestionSourceList = ({
|
|||||||
analytics.event({
|
analytics.event({
|
||||||
type: EventType.UpdateIngestionSourceEvent,
|
type: EventType.UpdateIngestionSourceEvent,
|
||||||
sourceType: input.type,
|
sourceType: input.type,
|
||||||
|
sourceUrn: focusSourceUrn,
|
||||||
interval: input.schedule?.interval,
|
interval: input.schedule?.interval,
|
||||||
numOwners: owners?.length,
|
numOwners: owners?.length,
|
||||||
|
outcome: shouldRun ? 'save_and_run' : 'save',
|
||||||
});
|
});
|
||||||
message.success({
|
message.success({
|
||||||
content: `Successfully updated ingestion source!`,
|
content: `Successfully updated ingestion source!`,
|
||||||
@ -460,8 +464,10 @@ export const IngestionSourceList = ({
|
|||||||
analytics.event({
|
analytics.event({
|
||||||
type: EventType.CreateIngestionSourceEvent,
|
type: EventType.CreateIngestionSourceEvent,
|
||||||
sourceType: input.type,
|
sourceType: input.type,
|
||||||
|
sourceUrn: newSource.urn,
|
||||||
interval: input.schedule?.interval,
|
interval: input.schedule?.interval,
|
||||||
numOwners: ownersToAdd?.length,
|
numOwners: ownersToAdd?.length,
|
||||||
|
outcome: shouldRun ? 'save_and_run' : 'save',
|
||||||
});
|
});
|
||||||
message.success({
|
message.success({
|
||||||
content: `Successfully created ingestion source!`,
|
content: `Successfully created ingestion source!`,
|
||||||
@ -696,6 +702,8 @@ export const IngestionSourceList = ({
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}}
|
}}
|
||||||
selectedSource={focusSource}
|
selectedSource={focusSource}
|
||||||
|
selectedSourceType={selectedSourceType}
|
||||||
|
setSelectedSourceType={setSelectedSourceType}
|
||||||
loading={isModalWaiting}
|
loading={isModalWaiting}
|
||||||
/>
|
/>
|
||||||
{isViewingRecipe && <RecipeViewerModal recipe={focusSource?.config?.recipe} onCancel={onCancel} />}
|
{isViewingRecipe && <RecipeViewerModal recipe={focusSource?.config?.recipe} onCancel={onCancel} />}
|
||||||
|
|||||||
@ -39,7 +39,15 @@ const ControlsContainer = styled.div`
|
|||||||
/**
|
/**
|
||||||
* The step for defining a recipe
|
* The step for defining a recipe
|
||||||
*/
|
*/
|
||||||
export const DefineRecipeStep = ({ state, updateState, goTo, prev, ingestionSources }: StepProps) => {
|
export const DefineRecipeStep = ({
|
||||||
|
state,
|
||||||
|
updateState,
|
||||||
|
goTo,
|
||||||
|
prev,
|
||||||
|
ingestionSources,
|
||||||
|
selectedSource,
|
||||||
|
setSelectedSourceType,
|
||||||
|
}: StepProps) => {
|
||||||
const existingRecipeJson = state.config?.recipe;
|
const existingRecipeJson = state.config?.recipe;
|
||||||
const existingRecipeYaml = existingRecipeJson && jsonToYaml(existingRecipeJson);
|
const existingRecipeYaml = existingRecipeJson && jsonToYaml(existingRecipeJson);
|
||||||
const { type } = state;
|
const { type } = state;
|
||||||
@ -96,6 +104,7 @@ export const DefineRecipeStep = ({ state, updateState, goTo, prev, ingestionSour
|
|||||||
updateState(newState);
|
updateState(newState);
|
||||||
|
|
||||||
goTo(IngestionSourceBuilderStep.CREATE_SCHEDULE);
|
goTo(IngestionSourceBuilderStep.CREATE_SCHEDULE);
|
||||||
|
setSelectedSourceType?.(newState.type);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type && CONNECTORS_WITH_FORM.has(type)) {
|
if (type && CONNECTORS_WITH_FORM.has(type)) {
|
||||||
@ -109,6 +118,7 @@ export const DefineRecipeStep = ({ state, updateState, goTo, prev, ingestionSour
|
|||||||
setStagedRecipe={setStagedRecipeYml}
|
setStagedRecipe={setStagedRecipeYml}
|
||||||
onClickNext={onClickNext}
|
onClickNext={onClickNext}
|
||||||
goToPrevious={prev}
|
goToPrevious={prev}
|
||||||
|
selectedSource={selectedSource}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import { LoadingOutlined } from '@ant-design/icons';
|
|||||||
import { Modal } from '@components';
|
import { Modal } from '@components';
|
||||||
import { Spin, Steps } from 'antd';
|
import { Spin, Steps } from 'antd';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import analytics, { EventType } from '@app/analytics';
|
||||||
import { CreateScheduleStep } from '@app/ingestV2/source/builder/CreateScheduleStep';
|
import { CreateScheduleStep } from '@app/ingestV2/source/builder/CreateScheduleStep';
|
||||||
import { DefineRecipeStep } from '@app/ingestV2/source/builder/DefineRecipeStep';
|
import { DefineRecipeStep } from '@app/ingestV2/source/builder/DefineRecipeStep';
|
||||||
import { NameSourceStep } from '@app/ingestV2/source/builder/NameSourceStep';
|
import { NameSourceStep } from '@app/ingestV2/source/builder/NameSourceStep';
|
||||||
@ -58,6 +59,8 @@ type Props = {
|
|||||||
sourceRefetch?: () => Promise<any>;
|
sourceRefetch?: () => Promise<any>;
|
||||||
selectedSource?: IngestionSource;
|
selectedSource?: IngestionSource;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
selectedSourceType?: string;
|
||||||
|
setSelectedSourceType?: (sourceType: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IngestionSourceBuilderModal = ({
|
export const IngestionSourceBuilderModal = ({
|
||||||
@ -68,6 +71,8 @@ export const IngestionSourceBuilderModal = ({
|
|||||||
sourceRefetch,
|
sourceRefetch,
|
||||||
selectedSource,
|
selectedSource,
|
||||||
loading,
|
loading,
|
||||||
|
selectedSourceType,
|
||||||
|
setSelectedSourceType,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const isEditing = initialState !== undefined;
|
const isEditing = initialState !== undefined;
|
||||||
const titleText = isEditing ? 'Edit Data Source' : 'Connect Data Source';
|
const titleText = isEditing ? 'Edit Data Source' : 'Connect Data Source';
|
||||||
@ -84,20 +89,46 @@ export const IngestionSourceBuilderModal = ({
|
|||||||
|
|
||||||
const ingestionSources = JSON.parse(JSON.stringify(sourcesJson)); // TODO: replace with call to server once we have access to dynamic list of sources
|
const ingestionSources = JSON.parse(JSON.stringify(sourcesJson)); // TODO: replace with call to server once we have access to dynamic list of sources
|
||||||
|
|
||||||
// Reset the ingestion builder modal state when the modal is re-opened.
|
const sendAnalyticsStepViewedEvent = useCallback(
|
||||||
const prevInitialState = useRef(initialState);
|
(step: IngestionSourceBuilderStep) => {
|
||||||
useEffect(() => {
|
if (open) {
|
||||||
if (!isEqual(prevInitialState.current, initialState)) {
|
analytics.event({
|
||||||
setIngestionBuilderState(initialState || {});
|
type: EventType.IngestionSourceConfigurationImpressionEvent,
|
||||||
|
viewedSection: step,
|
||||||
|
sourceType: selectedSource?.type || selectedSourceType,
|
||||||
|
sourceUrn: selectedSource?.urn,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
prevInitialState.current = initialState;
|
},
|
||||||
}, [initialState]);
|
[selectedSource?.type, selectedSource?.urn, selectedSourceType, open],
|
||||||
|
);
|
||||||
|
|
||||||
// Reset the step stack to the initial step when the modal is re-opened.
|
// Reset the modal state when initialState changes or modal opens
|
||||||
useEffect(() => setStepStack([initialStep]), [initialStep]);
|
const prevInitialState = useRef(initialState);
|
||||||
|
const prevOpen = useRef(open);
|
||||||
|
useEffect(() => {
|
||||||
|
const stateChanged = !isEqual(prevInitialState.current, initialState);
|
||||||
|
const modalOpened = !prevOpen.current && open;
|
||||||
|
|
||||||
|
if (stateChanged) {
|
||||||
|
setIngestionBuilderState(initialState || {});
|
||||||
|
setStepStack([initialStep]);
|
||||||
|
setSelectedSourceType?.('');
|
||||||
|
prevInitialState.current = initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire event when modal opens
|
||||||
|
if (modalOpened) {
|
||||||
|
setStepStack([initialStep]); // Ensure correct step when modal opens
|
||||||
|
sendAnalyticsStepViewedEvent(initialStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevOpen.current = open;
|
||||||
|
}, [initialState, initialStep, open, sendAnalyticsStepViewedEvent, setSelectedSourceType]);
|
||||||
|
|
||||||
const goTo = (step: IngestionSourceBuilderStep) => {
|
const goTo = (step: IngestionSourceBuilderStep) => {
|
||||||
setStepStack([...stepStack, step]);
|
setStepStack([...stepStack, step]);
|
||||||
|
sendAnalyticsStepViewedEvent(step);
|
||||||
};
|
};
|
||||||
|
|
||||||
const prev = () => {
|
const prev = () => {
|
||||||
@ -151,6 +182,8 @@ export const IngestionSourceBuilderModal = ({
|
|||||||
ingestionSources={ingestionSources}
|
ingestionSources={ingestionSources}
|
||||||
sourceRefetch={sourceRefetch}
|
sourceRefetch={sourceRefetch}
|
||||||
selectedSource={selectedSource}
|
selectedSource={selectedSource}
|
||||||
|
selectedSourceType={selectedSourceType}
|
||||||
|
setSelectedSourceType={setSelectedSourceType}
|
||||||
/>
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import { CSV, LOOKER, LOOK_ML } from '@app/ingestV2/source/builder/constants';
|
|||||||
import { SourceBuilderState, SourceConfig } from '@app/ingestV2/source/builder/types';
|
import { SourceBuilderState, SourceConfig } from '@app/ingestV2/source/builder/types';
|
||||||
import { Button } from '@src/alchemy-components';
|
import { Button } from '@src/alchemy-components';
|
||||||
|
|
||||||
|
import { IngestionSource } from '@types';
|
||||||
|
|
||||||
export const ControlsContainer = styled.div`
|
export const ControlsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -61,13 +63,23 @@ interface Props {
|
|||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
displayRecipe: string;
|
displayRecipe: string;
|
||||||
sourceConfigs?: SourceConfig;
|
sourceConfigs?: SourceConfig;
|
||||||
|
selectedSource?: IngestionSource;
|
||||||
setStagedRecipe: (recipe: string) => void;
|
setStagedRecipe: (recipe: string) => void;
|
||||||
onClickNext: () => void;
|
onClickNext: () => void;
|
||||||
goToPrevious?: () => void;
|
goToPrevious?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RecipeBuilder(props: Props) {
|
function RecipeBuilder(props: Props) {
|
||||||
const { state, isEditing, displayRecipe, sourceConfigs, setStagedRecipe, onClickNext, goToPrevious } = props;
|
const {
|
||||||
|
state,
|
||||||
|
isEditing,
|
||||||
|
displayRecipe,
|
||||||
|
sourceConfigs,
|
||||||
|
setStagedRecipe,
|
||||||
|
onClickNext,
|
||||||
|
goToPrevious,
|
||||||
|
selectedSource,
|
||||||
|
} = props;
|
||||||
const { type } = state;
|
const { type } = state;
|
||||||
const [isViewingForm, setIsViewingForm] = useState(true);
|
const [isViewingForm, setIsViewingForm] = useState(true);
|
||||||
const [hideDocsHint, setHideDocsHint] = useState(false);
|
const [hideDocsHint, setHideDocsHint] = useState(false);
|
||||||
@ -119,6 +131,7 @@ function RecipeBuilder(props: Props) {
|
|||||||
{isViewingForm && (
|
{isViewingForm && (
|
||||||
<RecipeForm
|
<RecipeForm
|
||||||
state={state}
|
state={state}
|
||||||
|
selectedSource={selectedSource}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
displayRecipe={displayRecipe}
|
displayRecipe={displayRecipe}
|
||||||
sourceConfigs={sourceConfigs}
|
sourceConfigs={sourceConfigs}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { jsonToYaml } from '@app/ingestV2/source/utils';
|
|||||||
import { RequiredFieldForm } from '@app/shared/form/RequiredFieldForm';
|
import { RequiredFieldForm } from '@app/shared/form/RequiredFieldForm';
|
||||||
|
|
||||||
import { useListSecretsQuery } from '@graphql/ingestion.generated';
|
import { useListSecretsQuery } from '@graphql/ingestion.generated';
|
||||||
|
import { IngestionSource } from '@types';
|
||||||
|
|
||||||
export const ControlsContainer = styled.div`
|
export const ControlsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -102,10 +103,20 @@ interface Props {
|
|||||||
setStagedRecipe: (recipe: string) => void;
|
setStagedRecipe: (recipe: string) => void;
|
||||||
onClickNext: () => void;
|
onClickNext: () => void;
|
||||||
goToPrevious?: () => void;
|
goToPrevious?: () => void;
|
||||||
|
selectedSource?: IngestionSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RecipeForm(props: Props) {
|
function RecipeForm(props: Props) {
|
||||||
const { state, isEditing, displayRecipe, sourceConfigs, setStagedRecipe, onClickNext, goToPrevious } = props;
|
const {
|
||||||
|
state,
|
||||||
|
isEditing,
|
||||||
|
displayRecipe,
|
||||||
|
sourceConfigs,
|
||||||
|
setStagedRecipe,
|
||||||
|
onClickNext,
|
||||||
|
goToPrevious,
|
||||||
|
selectedSource,
|
||||||
|
} = props;
|
||||||
const { type } = state;
|
const { type } = state;
|
||||||
const version = state.config?.version;
|
const version = state.config?.version;
|
||||||
const { fields, advancedFields, filterFields, filterSectionTooltip, advancedSectionTooltip, defaultOpenSections } =
|
const { fields, advancedFields, filterFields, filterSectionTooltip, advancedSectionTooltip, defaultOpenSections } =
|
||||||
@ -171,6 +182,7 @@ function RecipeForm(props: Props) {
|
|||||||
recipe={displayRecipe}
|
recipe={displayRecipe}
|
||||||
sourceConfigs={sourceConfigs}
|
sourceConfigs={sourceConfigs}
|
||||||
version={version}
|
version={version}
|
||||||
|
selectedSource={selectedSource}
|
||||||
/>
|
/>
|
||||||
</TestConnectionWrapper>
|
</TestConnectionWrapper>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { CheckCircleOutlined } from '@ant-design/icons';
|
|||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import analytics, { EventType } from '@app/analytics';
|
||||||
import { EXECUTION_REQUEST_STATUS_FAILURE, EXECUTION_REQUEST_STATUS_RUNNING } from '@app/ingestV2/executions/constants';
|
import { EXECUTION_REQUEST_STATUS_FAILURE, EXECUTION_REQUEST_STATUS_RUNNING } from '@app/ingestV2/executions/constants';
|
||||||
import TestConnectionModal from '@app/ingestV2/source/builder/RecipeForm/TestConnection/TestConnectionModal';
|
import TestConnectionModal from '@app/ingestV2/source/builder/RecipeForm/TestConnection/TestConnectionModal';
|
||||||
import { TestConnectionResult } from '@app/ingestV2/source/builder/RecipeForm/TestConnection/types';
|
import { TestConnectionResult } from '@app/ingestV2/source/builder/RecipeForm/TestConnection/types';
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
useCreateTestConnectionRequestMutation,
|
useCreateTestConnectionRequestMutation,
|
||||||
useGetIngestionExecutionRequestLazyQuery,
|
useGetIngestionExecutionRequestLazyQuery,
|
||||||
} from '@graphql/ingestion.generated';
|
} from '@graphql/ingestion.generated';
|
||||||
|
import { ExecutionRequestResult, IngestionSource } from '@types';
|
||||||
|
|
||||||
export function getRecipeJson(recipeYaml: string) {
|
export function getRecipeJson(recipeYaml: string) {
|
||||||
// Convert the recipe into it's json representation, and catch + report exceptions while we do it.
|
// Convert the recipe into it's json representation, and catch + report exceptions while we do it.
|
||||||
@ -29,18 +31,33 @@ export function getRecipeJson(recipeYaml: string) {
|
|||||||
return recipeJson;
|
return recipeJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSourceTypeFromRecipeJson(recipeJson: string) {
|
||||||
|
const recipe = JSON.parse(recipeJson);
|
||||||
|
return recipe.source.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBasicConnectivityFromResult(result: ExecutionRequestResult) {
|
||||||
|
if (!result?.structuredReport?.serializedValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const resultJson = JSON.parse(result.structuredReport.serializedValue);
|
||||||
|
return resultJson?.basic_connectivity?.capable;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
recipe: string;
|
recipe: string;
|
||||||
sourceConfigs?: SourceConfig;
|
sourceConfigs?: SourceConfig;
|
||||||
version?: string | null;
|
version?: string | null;
|
||||||
|
selectedSource?: IngestionSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TestConnectionButton(props: Props) {
|
function TestConnectionButton(props: Props) {
|
||||||
const { recipe, sourceConfigs, version } = props;
|
const { recipe, sourceConfigs, version, selectedSource } = props;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [pollingInterval, setPollingInterval] = useState<null | NodeJS.Timeout>(null);
|
const [pollingInterval, setPollingInterval] = useState<null | NodeJS.Timeout>(null);
|
||||||
const [testConnectionResult, setTestConnectionResult] = useState<null | TestConnectionResult>(null);
|
const [testConnectionResult, setTestConnectionResult] = useState<null | TestConnectionResult>(null);
|
||||||
|
const [hasEmittedAnalytics, setHasEmittedAnalytics] = useState(false);
|
||||||
const [createTestConnectionRequest, { data: requestData }] = useCreateTestConnectionRequestMutation();
|
const [createTestConnectionRequest, { data: requestData }] = useCreateTestConnectionRequestMutation();
|
||||||
const [getIngestionExecutionRequest, { data: resultData, loading }] = useGetIngestionExecutionRequestLazyQuery();
|
const [getIngestionExecutionRequest, { data: resultData, loading }] = useGetIngestionExecutionRequestLazyQuery();
|
||||||
|
|
||||||
@ -63,6 +80,18 @@ function TestConnectionButton(props: Props) {
|
|||||||
if (!loading && resultData) {
|
if (!loading && resultData) {
|
||||||
const result = resultData.executionRequest?.result;
|
const result = resultData.executionRequest?.result;
|
||||||
if (result && result.status !== EXECUTION_REQUEST_STATUS_RUNNING) {
|
if (result && result.status !== EXECUTION_REQUEST_STATUS_RUNNING) {
|
||||||
|
const recipeJson = getRecipeJson(recipe);
|
||||||
|
if (recipeJson && !hasEmittedAnalytics) {
|
||||||
|
const basicConnectivity = getBasicConnectivityFromResult(result);
|
||||||
|
analytics.event({
|
||||||
|
type: EventType.IngestionTestConnectionEvent,
|
||||||
|
sourceType: getSourceTypeFromRecipeJson(recipeJson),
|
||||||
|
sourceUrn: selectedSource?.urn,
|
||||||
|
outcome: basicConnectivity ? 'completed' : 'failed',
|
||||||
|
});
|
||||||
|
setHasEmittedAnalytics(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (result.status === EXECUTION_REQUEST_STATUS_FAILURE) {
|
if (result.status === EXECUTION_REQUEST_STATUS_FAILURE) {
|
||||||
message.error(
|
message.error(
|
||||||
'Something went wrong with your connection test. Please check your recipe and try again.',
|
'Something went wrong with your connection test. Please check your recipe and try again.',
|
||||||
@ -77,7 +106,7 @@ function TestConnectionButton(props: Props) {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [resultData, pollingInterval, loading]);
|
}, [resultData, pollingInterval, loading, recipe, selectedSource?.urn, hasEmittedAnalytics]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isModalVisible && pollingInterval) {
|
if (!isModalVisible && pollingInterval) {
|
||||||
@ -88,6 +117,7 @@ function TestConnectionButton(props: Props) {
|
|||||||
function testConnection() {
|
function testConnection() {
|
||||||
const recipeJson = getRecipeJson(recipe);
|
const recipeJson = getRecipeJson(recipe);
|
||||||
if (recipeJson) {
|
if (recipeJson) {
|
||||||
|
setHasEmittedAnalytics(false);
|
||||||
createTestConnectionRequest({ variables: { input: { recipe: recipeJson, version } } })
|
createTestConnectionRequest({ variables: { input: { recipe: recipeJson, version } } })
|
||||||
.then((res) =>
|
.then((res) =>
|
||||||
getIngestionExecutionRequest({
|
getIngestionExecutionRequest({
|
||||||
|
|||||||
@ -82,7 +82,14 @@ function SourceOption({ source, onClick }: SourceOptionProps) {
|
|||||||
/**
|
/**
|
||||||
* Component responsible for selecting the mechanism for constructing a new Ingestion Source
|
* Component responsible for selecting the mechanism for constructing a new Ingestion Source
|
||||||
*/
|
*/
|
||||||
export const SelectTemplateStep = ({ state, updateState, goTo, cancel, ingestionSources }: StepProps) => {
|
export const SelectTemplateStep = ({
|
||||||
|
state,
|
||||||
|
updateState,
|
||||||
|
goTo,
|
||||||
|
cancel,
|
||||||
|
ingestionSources,
|
||||||
|
setSelectedSourceType,
|
||||||
|
}: StepProps) => {
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
|
||||||
const onSelectTemplate = (type: string) => {
|
const onSelectTemplate = (type: string) => {
|
||||||
@ -93,6 +100,7 @@ export const SelectTemplateStep = ({ state, updateState, goTo, cancel, ingestion
|
|||||||
};
|
};
|
||||||
updateState(newState);
|
updateState(newState);
|
||||||
goTo(IngestionSourceBuilderStep.DEFINE_RECIPE);
|
goTo(IngestionSourceBuilderStep.DEFINE_RECIPE);
|
||||||
|
setSelectedSourceType?.(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredSources = ingestionSources.filter(
|
const filteredSources = ingestionSources.filter(
|
||||||
|
|||||||
@ -38,6 +38,10 @@ export type StepProps = {
|
|||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
sourceRefetch?: () => Promise<any>;
|
sourceRefetch?: () => Promise<any>;
|
||||||
selectedSource?: IngestionSource;
|
selectedSource?: IngestionSource;
|
||||||
|
// This is not same as selectedSource
|
||||||
|
// This is required when the ingestion source has not been created
|
||||||
|
selectedSourceType?: string;
|
||||||
|
setSelectedSourceType?: (sourceType: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StringMapEntryInput = {
|
export type StringMapEntryInput = {
|
||||||
|
|||||||
@ -55,6 +55,9 @@ public enum DataHubUsageEventType {
|
|||||||
CREATE_GLOSSARY_ENTITY_EVENT("CreateGlossaryEntityEvent"),
|
CREATE_GLOSSARY_ENTITY_EVENT("CreateGlossaryEntityEvent"),
|
||||||
CREATE_DOMAIN_EVENT("CreateDomainEvent"),
|
CREATE_DOMAIN_EVENT("CreateDomainEvent"),
|
||||||
MOVE_DOMAIN_EVENT("MoveDomainEvent"),
|
MOVE_DOMAIN_EVENT("MoveDomainEvent"),
|
||||||
|
INGESTION_TEST_CONNECTION_EVENT("IngestionTestConnectionEvent"),
|
||||||
|
INGESTION_EXECUTION_RESULT_VIEWED_EVENT("IngestionExecutionResultViewedEvent"),
|
||||||
|
INGESTION_SOURCE_CONFIGURATION_IMPRESSION_EVENT("IngestionSourceConfigurationImpressionEvent"),
|
||||||
CREATE_INGESTION_SOURCE_EVENT("CreateIngestionSourceEvent"),
|
CREATE_INGESTION_SOURCE_EVENT("CreateIngestionSourceEvent"),
|
||||||
UPDATE_INGESTION_SOURCE_EVENT("UpdateIngestionSourceEvent"),
|
UPDATE_INGESTION_SOURCE_EVENT("UpdateIngestionSourceEvent"),
|
||||||
DELETE_INGESTION_SOURCE_EVENT("DeleteIngestionSourceEvent"),
|
DELETE_INGESTION_SOURCE_EVENT("DeleteIngestionSourceEvent"),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user