2022-08-19 15:58:31 -04:00
|
|
|
import { PlusOutlined, RedoOutlined } from '@ant-design/icons';
|
2022-08-16 19:01:26 -04:00
|
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
2022-05-16 13:24:56 -07:00
|
|
|
import * as QueryString from 'query-string';
|
|
|
|
import { useLocation } from 'react-router';
|
2022-08-19 15:58:31 -04:00
|
|
|
import { Button, message, Modal, Pagination, Select } from 'antd';
|
2022-01-27 10:33:12 -08:00
|
|
|
import styled from 'styled-components';
|
|
|
|
import {
|
|
|
|
useCreateIngestionExecutionRequestMutation,
|
|
|
|
useCreateIngestionSourceMutation,
|
|
|
|
useDeleteIngestionSourceMutation,
|
|
|
|
useListIngestionSourcesQuery,
|
|
|
|
useUpdateIngestionSourceMutation,
|
|
|
|
} from '../../../graphql/ingestion.generated';
|
|
|
|
import { Message } from '../../shared/Message';
|
|
|
|
import TabToolbar from '../../entity/shared/components/styled/TabToolbar';
|
|
|
|
import { IngestionSourceBuilderModal } from './builder/IngestionSourceBuilderModal';
|
2022-08-29 19:11:59 -04:00
|
|
|
import { CLI_EXECUTOR_ID } from './utils';
|
2022-01-27 10:33:12 -08:00
|
|
|
import { DEFAULT_EXECUTOR_ID, SourceBuilderState } from './builder/types';
|
2022-08-19 15:58:31 -04:00
|
|
|
import { IngestionSource, UpdateIngestionSourceInput } from '../../../types.generated';
|
2022-05-16 13:24:56 -07:00
|
|
|
import { SearchBar } from '../../search/SearchBar';
|
|
|
|
import { useEntityRegistry } from '../../useEntityRegistry';
|
2022-08-29 19:11:59 -04:00
|
|
|
import { ExecutionDetailsModal } from './executions/ExecutionRequestDetailsModal';
|
2022-08-19 15:58:31 -04:00
|
|
|
import RecipeViewerModal from './RecipeViewerModal';
|
|
|
|
import IngestionSourceTable from './IngestionSourceTable';
|
2022-08-25 03:50:52 +05:30
|
|
|
import { scrollToTop } from '../../shared/searchUtils';
|
2022-08-29 19:11:59 -04:00
|
|
|
import useRefreshIngestionData from './executions/useRefreshIngestionData';
|
|
|
|
import { isExecutionRequestActive } from './executions/IngestionSourceExecutionList';
|
2022-01-27 10:33:12 -08:00
|
|
|
|
|
|
|
const SourceContainer = styled.div``;
|
|
|
|
|
|
|
|
const SourcePaginationContainer = styled.div`
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
`;
|
|
|
|
|
2022-08-19 15:58:31 -04:00
|
|
|
const StyledSelect = styled(Select)`
|
|
|
|
margin-right: 15px;
|
|
|
|
min-width: 75px;
|
2022-01-27 10:33:12 -08:00
|
|
|
`;
|
|
|
|
|
2022-08-19 15:58:31 -04:00
|
|
|
const FilterWrapper = styled.div`
|
2022-01-27 10:33:12 -08:00
|
|
|
display: flex;
|
|
|
|
`;
|
|
|
|
|
2022-08-19 15:58:31 -04:00
|
|
|
export enum IngestionSourceType {
|
|
|
|
ALL,
|
|
|
|
UI,
|
|
|
|
CLI,
|
|
|
|
}
|
2022-08-08 14:33:57 -07:00
|
|
|
|
2022-08-19 15:58:31 -04:00
|
|
|
export function shouldIncludeSource(source: any, sourceFilter: IngestionSourceType) {
|
|
|
|
if (sourceFilter === IngestionSourceType.CLI) {
|
|
|
|
return source.config.executorId === CLI_EXECUTOR_ID;
|
|
|
|
}
|
|
|
|
if (sourceFilter === IngestionSourceType.UI) {
|
|
|
|
return source.config.executorId !== CLI_EXECUTOR_ID;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
2022-03-04 11:51:31 -08:00
|
|
|
|
2022-01-27 10:33:12 -08:00
|
|
|
const DEFAULT_PAGE_SIZE = 25;
|
|
|
|
|
|
|
|
const removeExecutionsFromIngestionSource = (source) => {
|
|
|
|
if (source) {
|
|
|
|
return {
|
|
|
|
name: source.name,
|
|
|
|
type: source.type,
|
|
|
|
schedule: source.schedule,
|
|
|
|
config: source.config,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const IngestionSourceList = () => {
|
2022-05-16 13:24:56 -07:00
|
|
|
const entityRegistry = useEntityRegistry();
|
|
|
|
const location = useLocation();
|
|
|
|
const params = QueryString.parse(location.search, { arrayFormat: 'comma' });
|
|
|
|
const paramsQuery = (params?.query as string) || undefined;
|
|
|
|
const [query, setQuery] = useState<undefined | string>(undefined);
|
|
|
|
useEffect(() => setQuery(paramsQuery), [paramsQuery]);
|
|
|
|
|
2022-01-27 10:33:12 -08:00
|
|
|
const [page, setPage] = useState(1);
|
|
|
|
|
|
|
|
const pageSize = DEFAULT_PAGE_SIZE;
|
|
|
|
const start = (page - 1) * pageSize;
|
|
|
|
|
|
|
|
const [isBuildingSource, setIsBuildingSource] = useState<boolean>(false);
|
2022-08-19 15:58:31 -04:00
|
|
|
const [isViewingRecipe, setIsViewingRecipe] = useState<boolean>(false);
|
2022-01-27 10:33:12 -08:00
|
|
|
const [focusSourceUrn, setFocusSourceUrn] = useState<undefined | string>(undefined);
|
2022-08-08 14:33:57 -07:00
|
|
|
const [focusExecutionUrn, setFocusExecutionUrn] = useState<undefined | string>(undefined);
|
2022-01-27 10:33:12 -08:00
|
|
|
const [lastRefresh, setLastRefresh] = useState(0);
|
|
|
|
// Set of removed urns used to account for eventual consistency
|
|
|
|
const [removedUrns, setRemovedUrns] = useState<string[]>([]);
|
2022-08-19 15:58:31 -04:00
|
|
|
const [sourceFilter, setSourceFilter] = useState(IngestionSourceType.ALL);
|
2022-01-27 10:33:12 -08:00
|
|
|
|
|
|
|
// Ingestion Source Queries
|
|
|
|
const { loading, error, data, refetch } = useListIngestionSourcesQuery({
|
|
|
|
variables: {
|
|
|
|
input: {
|
|
|
|
start,
|
|
|
|
count: pageSize,
|
2022-05-16 13:24:56 -07:00
|
|
|
query,
|
2022-01-27 10:33:12 -08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
const [createIngestionSource] = useCreateIngestionSourceMutation();
|
|
|
|
const [updateIngestionSource] = useUpdateIngestionSourceMutation();
|
|
|
|
|
|
|
|
// Execution Request queries
|
|
|
|
const [createExecutionRequestMutation] = useCreateIngestionExecutionRequestMutation();
|
|
|
|
const [removeIngestionSourceMutation] = useDeleteIngestionSourceMutation();
|
|
|
|
|
|
|
|
const totalSources = data?.listIngestionSources?.total || 0;
|
|
|
|
const sources = data?.listIngestionSources?.ingestionSources || [];
|
2022-08-19 15:58:31 -04:00
|
|
|
const filteredSources = sources.filter(
|
|
|
|
(source) => !removedUrns.includes(source.urn) && shouldIncludeSource(source, sourceFilter),
|
|
|
|
) as IngestionSource[];
|
2022-01-27 10:33:12 -08:00
|
|
|
const focusSource =
|
|
|
|
(focusSourceUrn && filteredSources.find((source) => source.urn === focusSourceUrn)) || undefined;
|
|
|
|
|
2022-08-16 19:01:26 -04:00
|
|
|
const onRefresh = useCallback(() => {
|
2022-08-05 19:36:05 -04:00
|
|
|
refetch();
|
|
|
|
// Used to force a re-render of the child execution request list.
|
2022-08-22 14:12:44 -04:00
|
|
|
setLastRefresh(new Date().getTime());
|
2022-08-16 19:01:26 -04:00
|
|
|
}, [refetch]);
|
|
|
|
|
2022-08-29 19:11:59 -04:00
|
|
|
function hasActiveExecution() {
|
|
|
|
return !!filteredSources.find((source) =>
|
|
|
|
source.executions?.executionRequests.find((request) => isExecutionRequestActive(request)),
|
2022-08-16 19:01:26 -04:00
|
|
|
);
|
2022-08-29 19:11:59 -04:00
|
|
|
}
|
|
|
|
useRefreshIngestionData(onRefresh, hasActiveExecution);
|
2022-08-05 19:36:05 -04:00
|
|
|
|
|
|
|
const executeIngestionSource = (urn: string) => {
|
|
|
|
createExecutionRequestMutation({
|
|
|
|
variables: {
|
|
|
|
input: {
|
|
|
|
ingestionSourceUrn: urn,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
message.success({
|
|
|
|
content: `Successfully submitted ingestion execution request!`,
|
|
|
|
duration: 3,
|
|
|
|
});
|
2022-08-16 19:01:26 -04:00
|
|
|
setTimeout(() => onRefresh(), 3000);
|
2022-08-05 19:36:05 -04:00
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
message.destroy();
|
|
|
|
message.error({
|
|
|
|
content: `Failed to submit ingestion execution request!: \n ${e.message || ''}`,
|
|
|
|
duration: 3,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-01-27 10:33:12 -08:00
|
|
|
const onCreateOrUpdateIngestionSourceSuccess = () => {
|
|
|
|
setTimeout(() => refetch(), 2000);
|
|
|
|
setIsBuildingSource(false);
|
|
|
|
setFocusSourceUrn(undefined);
|
|
|
|
};
|
|
|
|
|
2022-08-05 19:36:05 -04:00
|
|
|
const createOrUpdateIngestionSource = (
|
|
|
|
input: UpdateIngestionSourceInput,
|
|
|
|
resetState: () => void,
|
|
|
|
shouldRun?: boolean,
|
|
|
|
) => {
|
2022-01-27 10:33:12 -08:00
|
|
|
if (focusSourceUrn) {
|
|
|
|
// Update:
|
|
|
|
updateIngestionSource({ variables: { urn: focusSourceUrn as string, input } })
|
|
|
|
.then(() => {
|
|
|
|
message.success({
|
|
|
|
content: `Successfully updated ingestion source!`,
|
|
|
|
duration: 3,
|
|
|
|
});
|
|
|
|
onCreateOrUpdateIngestionSourceSuccess();
|
|
|
|
resetState();
|
2022-08-05 19:36:05 -04:00
|
|
|
if (shouldRun) executeIngestionSource(focusSourceUrn);
|
2022-01-27 10:33:12 -08:00
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
message.destroy();
|
|
|
|
message.error({
|
|
|
|
content: `Failed to update ingestion source!: \n ${e.message || ''}`,
|
|
|
|
duration: 3,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Create
|
|
|
|
createIngestionSource({ variables: { input } })
|
2022-08-05 19:36:05 -04:00
|
|
|
.then((result) => {
|
|
|
|
message.loading({ content: 'Loading...', duration: 2 });
|
|
|
|
setTimeout(() => {
|
|
|
|
refetch();
|
|
|
|
message.success({
|
|
|
|
content: `Successfully created ingestion source!`,
|
|
|
|
duration: 3,
|
|
|
|
});
|
|
|
|
if (shouldRun && result.data?.createIngestionSource) {
|
|
|
|
executeIngestionSource(result.data.createIngestionSource);
|
|
|
|
}
|
|
|
|
}, 2000);
|
2022-01-27 10:33:12 -08:00
|
|
|
setIsBuildingSource(false);
|
|
|
|
setFocusSourceUrn(undefined);
|
|
|
|
resetState();
|
|
|
|
// onCreateOrUpdateIngestionSourceSuccess();
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
|
|
|
message.destroy();
|
|
|
|
message.error({
|
|
|
|
content: `Failed to create ingestion source!: \n ${e.message || ''}`,
|
|
|
|
duration: 3,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const onChangePage = (newPage: number) => {
|
2022-08-25 03:50:52 +05:30
|
|
|
scrollToTop();
|
2022-01-27 10:33:12 -08:00
|
|
|
setPage(newPage);
|
|
|
|
};
|
|
|
|
|
|
|
|
const deleteIngestionSource = async (urn: string) => {
|
|
|
|
removeIngestionSourceMutation({
|
|
|
|
variables: { urn },
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
message.success({ content: 'Removed ingestion source.', duration: 2 });
|
|
|
|
const newRemovedUrns = [...removedUrns, urn];
|
|
|
|
setRemovedUrns(newRemovedUrns);
|
|
|
|
setTimeout(function () {
|
|
|
|
refetch?.();
|
|
|
|
}, 3000);
|
|
|
|
})
|
|
|
|
.catch((e: unknown) => {
|
|
|
|
message.destroy();
|
|
|
|
if (e instanceof Error) {
|
|
|
|
message.error({ content: `Failed to remove ingestion source: \n ${e.message || ''}`, duration: 3 });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-08-05 19:36:05 -04:00
|
|
|
const onSubmit = (recipeBuilderState: SourceBuilderState, resetState: () => void, shouldRun?: boolean) => {
|
2022-01-27 10:33:12 -08:00
|
|
|
createOrUpdateIngestionSource(
|
|
|
|
{
|
|
|
|
type: recipeBuilderState.type as string,
|
|
|
|
name: recipeBuilderState.name as string,
|
|
|
|
config: {
|
|
|
|
recipe: recipeBuilderState.config?.recipe as string,
|
|
|
|
version:
|
|
|
|
(recipeBuilderState.config?.version?.length &&
|
|
|
|
(recipeBuilderState.config?.version as string)) ||
|
|
|
|
undefined,
|
|
|
|
executorId:
|
|
|
|
(recipeBuilderState.config?.executorId?.length &&
|
|
|
|
(recipeBuilderState.config?.executorId as string)) ||
|
|
|
|
DEFAULT_EXECUTOR_ID,
|
2022-08-29 16:39:14 -04:00
|
|
|
debugMode: recipeBuilderState.config?.debugMode || false,
|
2022-01-27 10:33:12 -08:00
|
|
|
},
|
|
|
|
schedule: recipeBuilderState.schedule && {
|
|
|
|
interval: recipeBuilderState.schedule?.interval as string,
|
|
|
|
timezone: recipeBuilderState.schedule?.timezone as string,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
resetState,
|
2022-08-05 19:36:05 -04:00
|
|
|
shouldRun,
|
2022-01-27 10:33:12 -08:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onEdit = (urn: string) => {
|
|
|
|
setIsBuildingSource(true);
|
|
|
|
setFocusSourceUrn(urn);
|
|
|
|
};
|
|
|
|
|
2022-08-19 15:58:31 -04:00
|
|
|
const onView = (urn: string) => {
|
|
|
|
setIsViewingRecipe(true);
|
|
|
|
setFocusSourceUrn(urn);
|
|
|
|
};
|
|
|
|
|
2022-01-27 10:33:12 -08:00
|
|
|
const onExecute = (urn: string) => {
|
|
|
|
Modal.confirm({
|
|
|
|
title: `Confirm Source Execution`,
|
|
|
|
content: "Click 'Execute' to run this ingestion source.",
|
|
|
|
onOk() {
|
|
|
|
executeIngestionSource(urn);
|
|
|
|
},
|
|
|
|
onCancel() {},
|
|
|
|
okText: 'Execute',
|
|
|
|
maskClosable: true,
|
|
|
|
closable: true,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const onDelete = (urn: string) => {
|
|
|
|
Modal.confirm({
|
|
|
|
title: `Confirm Ingestion Source Removal`,
|
|
|
|
content: `Are you sure you want to remove this ingestion source? Removing will terminate any scheduled ingestion runs.`,
|
|
|
|
onOk() {
|
|
|
|
deleteIngestionSource(urn);
|
|
|
|
},
|
|
|
|
onCancel() {},
|
|
|
|
okText: 'Yes',
|
|
|
|
maskClosable: true,
|
|
|
|
closable: true,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const onCancel = () => {
|
|
|
|
setIsBuildingSource(false);
|
2022-08-19 15:58:31 -04:00
|
|
|
setIsViewingRecipe(false);
|
2022-01-27 10:33:12 -08:00
|
|
|
setFocusSourceUrn(undefined);
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{!data && loading && <Message type="loading" content="Loading ingestion sources..." />}
|
2022-08-28 20:08:25 -07:00
|
|
|
{error && (
|
|
|
|
<Message type="error" content="Failed to load ingestion sources! An unexpected error occurred." />
|
|
|
|
)}
|
2022-01-27 10:33:12 -08:00
|
|
|
<SourceContainer>
|
|
|
|
<TabToolbar>
|
|
|
|
<div>
|
|
|
|
<Button type="text" onClick={() => setIsBuildingSource(true)}>
|
|
|
|
<PlusOutlined /> Create new source
|
|
|
|
</Button>
|
|
|
|
<Button type="text" onClick={onRefresh}>
|
|
|
|
<RedoOutlined /> Refresh
|
|
|
|
</Button>
|
|
|
|
</div>
|
2022-08-19 15:58:31 -04:00
|
|
|
<FilterWrapper>
|
|
|
|
<StyledSelect
|
|
|
|
value={sourceFilter}
|
|
|
|
onChange={(selection) => setSourceFilter(selection as IngestionSourceType)}
|
|
|
|
>
|
|
|
|
<Select.Option value={IngestionSourceType.ALL}>All</Select.Option>
|
|
|
|
<Select.Option value={IngestionSourceType.UI}>UI</Select.Option>
|
|
|
|
<Select.Option value={IngestionSourceType.CLI}>CLI</Select.Option>
|
|
|
|
</StyledSelect>
|
|
|
|
|
|
|
|
<SearchBar
|
|
|
|
initialQuery={query || ''}
|
|
|
|
placeholderText="Search sources..."
|
|
|
|
suggestions={[]}
|
|
|
|
style={{
|
|
|
|
maxWidth: 220,
|
|
|
|
padding: 0,
|
|
|
|
}}
|
|
|
|
inputStyle={{
|
|
|
|
height: 32,
|
|
|
|
fontSize: 12,
|
|
|
|
}}
|
|
|
|
onSearch={() => null}
|
|
|
|
onQueryChange={(q) => setQuery(q)}
|
|
|
|
entityRegistry={entityRegistry}
|
2022-09-22 22:32:51 +05:30
|
|
|
hideRecommendations
|
2022-08-19 15:58:31 -04:00
|
|
|
/>
|
|
|
|
</FilterWrapper>
|
2022-01-27 10:33:12 -08:00
|
|
|
</TabToolbar>
|
2022-08-19 15:58:31 -04:00
|
|
|
<IngestionSourceTable
|
|
|
|
lastRefresh={lastRefresh}
|
|
|
|
sources={filteredSources || []}
|
|
|
|
setFocusExecutionUrn={setFocusExecutionUrn}
|
|
|
|
onExecute={onExecute}
|
|
|
|
onEdit={onEdit}
|
|
|
|
onView={onView}
|
|
|
|
onDelete={onDelete}
|
|
|
|
onRefresh={onRefresh}
|
2022-01-27 10:33:12 -08:00
|
|
|
/>
|
|
|
|
<SourcePaginationContainer>
|
|
|
|
<Pagination
|
|
|
|
style={{ margin: 40 }}
|
|
|
|
current={page}
|
|
|
|
pageSize={pageSize}
|
|
|
|
total={totalSources}
|
|
|
|
showLessItems
|
|
|
|
onChange={onChangePage}
|
|
|
|
showSizeChanger={false}
|
|
|
|
/>
|
|
|
|
</SourcePaginationContainer>
|
|
|
|
</SourceContainer>
|
|
|
|
<IngestionSourceBuilderModal
|
|
|
|
initialState={removeExecutionsFromIngestionSource(focusSource)}
|
|
|
|
visible={isBuildingSource}
|
|
|
|
onSubmit={onSubmit}
|
|
|
|
onCancel={onCancel}
|
|
|
|
/>
|
2022-08-19 15:58:31 -04:00
|
|
|
{isViewingRecipe && <RecipeViewerModal recipe={focusSource?.config.recipe} onCancel={onCancel} />}
|
2022-08-08 14:33:57 -07:00
|
|
|
{focusExecutionUrn && (
|
|
|
|
<ExecutionDetailsModal
|
|
|
|
urn={focusExecutionUrn}
|
|
|
|
visible
|
|
|
|
onClose={() => setFocusExecutionUrn(undefined)}
|
|
|
|
/>
|
|
|
|
)}
|
2022-01-27 10:33:12 -08:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|