diff --git a/datahub-web-react/src/app/ingest/ManageIngestionPage.tsx b/datahub-web-react/src/app/ingest/ManageIngestionPage.tsx index b0ead86dcb..0ed2da831a 100644 --- a/datahub-web-react/src/app/ingest/ManageIngestionPage.tsx +++ b/datahub-web-react/src/app/ingest/ManageIngestionPage.tsx @@ -1,4 +1,6 @@ -import { Tabs, Typography } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { Button, PageTitle } from '@components'; +import { Tabs } from 'antd'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; import styled from 'styled-components'; @@ -8,44 +10,53 @@ import { SecretsList } from '@app/ingest/secret/SecretsList'; import { IngestionSourceList } from '@app/ingest/source/IngestionSourceList'; import { TabType } from '@app/ingest/types'; import { OnboardingTour } from '@app/onboarding/OnboardingTour'; -import { - INGESTION_CREATE_SOURCE_ID, - INGESTION_REFRESH_SOURCES_ID, -} from '@app/onboarding/config/IngestionOnboardingConfig'; +import { INGESTION_CREATE_SOURCE_ID } from '@app/onboarding/config/IngestionOnboardingConfig'; +import { NoPageFound } from '@app/shared/NoPageFound'; import { useAppConfig } from '@app/useAppConfig'; import { useShowNavBarRedesign } from '@app/useShowNavBarRedesign'; const PageContainer = styled.div<{ $isShowNavBarRedesign?: boolean }>` - padding-top: 20px; + padding-top: 16px; + padding-right: 16px; background-color: white; + height: 100%; + display: flex; + flex-direction: column; border-radius: ${(props) => props.$isShowNavBarRedesign ? props.theme.styles['border-radius-navbar-redesign'] : '8px'}; ${(props) => props.$isShowNavBarRedesign && ` - overflow: hidden; margin: 5px; box-shadow: ${props.theme.styles['box-shadow-navbar-redesign']}; - height: 100%; `} `; const PageHeaderContainer = styled.div` && { - padding-left: 24px; + padding-left: 20px; + padding-right: 20px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; } `; -const PageTitle = styled(Typography.Title)` - && { - margin-bottom: 12px; - } +const TitleContainer = styled.div` + flex: 1; +`; + +const HeaderActionsContainer = styled.div` + display: flex; + justify-content: flex-end; `; const StyledTabs = styled(Tabs)` &&& .ant-tabs-nav { margin-bottom: 0; - padding-left: 28px; + padding-left: 20px; } `; @@ -54,26 +65,42 @@ const Tab = styled(Tabs.TabPane)` line-height: 22px; `; -const ListContainer = styled.div``; +const ListContainer = styled.div` + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +`; export const ManageIngestionPage = () => { /** * Determines which view should be visible: ingestion sources or secrets. */ - const me = useUserContext(); - const { config, loaded } = useAppConfig(); + const { platformPrivileges, loaded: loadedPlatformPrivileges } = useUserContext(); + const { config, loaded: loadedAppConfig } = useAppConfig(); const isIngestionEnabled = config?.managedIngestionConfig?.enabled; - const showIngestionTab = isIngestionEnabled && me && me.platformPrivileges?.manageIngestion; - const showSecretsTab = isIngestionEnabled && me && me.platformPrivileges?.manageSecrets; - const [selectedTab, setSelectedTab] = useState(TabType.Sources); + const showIngestionTab = isIngestionEnabled && platformPrivileges?.manageIngestion; + const showSecretsTab = isIngestionEnabled && platformPrivileges?.manageSecrets; + + // undefined == not loaded, null == no permissions + const [selectedTab, setSelectedTab] = useState(); + + const [showCreateSourceModal, setShowCreateSourceModal] = useState(false); + const [showCreateSecretModal, setShowCreateSecretModal] = useState(false); const isShowNavBarRedesign = useShowNavBarRedesign(); // defaultTab might not be calculated correctly on mount, if `config` or `me` haven't been loaded yet useEffect(() => { - if (loaded && me.loaded && !showIngestionTab && selectedTab === TabType.Sources) { - setSelectedTab(TabType.Secrets); + if (loadedAppConfig && loadedPlatformPrivileges && selectedTab === undefined) { + if (showIngestionTab) { + setSelectedTab(TabType.Sources); + } else if (showSecretsTab) { + setSelectedTab(TabType.Secrets); + } else { + setSelectedTab(null); + } } - }, [loaded, me.loaded, showIngestionTab, selectedTab]); + }, [loadedAppConfig, loadedPlatformPrivileges, showIngestionTab, showSecretsTab, selectedTab]); const history = useHistory(); const onSwitchTab = (newTab: string, options?: { clearQueryParams: boolean }) => { @@ -84,19 +111,61 @@ export const ManageIngestionPage = () => { } }; - const TabTypeToListComponent = { - [TabType.Sources]: , - [TabType.Secrets]: , + const handleCreateSource = () => { + setShowCreateSourceModal(true); }; + const handleCreateSecret = () => { + setShowCreateSecretModal(true); + }; + + const TabTypeToListComponent = { + [TabType.Sources]: ( + + ), + [TabType.Secrets]: ( + + ), + }; + + if (selectedTab === undefined) { + return <>; // loading + } + if (selectedTab === null) { + return ; + } + return ( - + - Manage Data Sources - - Configure and schedule syncs to import data from your data sources - + + + + + {selectedTab === TabType.Sources && showIngestionTab && ( + + )} + + {selectedTab === TabType.Secrets && showSecretsTab && ( + + )} + ( +
+ +
+); + +type TableDataType = { + urn: string; + name: string; + description: string | null; +}; const DEFAULT_PAGE_SIZE = 25; -export const SecretsList = () => { +type Props = { + showCreateModal: boolean; + setShowCreateModal: (show: boolean) => void; +}; + +const SearchContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +const StyledTabToolbar = styled(TabToolbar)` + padding: 16px 20px; + height: auto; + &&& { + padding: 8px 20px; + height: auto; + box-shadow: none; + border-bottom: none; + } +`; + +const StyledSearchBar = styled(SearchBar)` + width: 220px; +`; + +const PaginationContainer = styled.div` + display: flex; + justify-content: center; + margin-top: 16px; +`; + +const TableContainer = styled.div` + padding-left: 16px; +`; + +export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateModal: setIsCreatingSecret }: Props) => { const isShowNavBarRedesign = useShowNavBarRedesign(); - const entityRegistry = useEntityRegistry(); const location = useLocation(); const params = QueryString.parse(location.search, { arrayFormat: 'comma' }); const paramsQuery = (params?.query as string) || undefined; @@ -64,8 +121,6 @@ export const SecretsList = () => { const pageSize = DEFAULT_PAGE_SIZE; const start = (page - 1) * pageSize; - // Whether or not there is an urn to show in the modal - const [isCreatingSecret, setIsCreatingSecret] = useState(false); const [editSecret, setEditSecret] = useState(undefined); const [deleteSecretMutation] = useDeleteSecretMutation(); @@ -106,9 +161,10 @@ export const SecretsList = () => { setPage(newPage); }; - const debouncedSetQuery = debounce((newQuery: string | undefined) => { - setQuery(newQuery); - }, ONE_SECOND_IN_MS); + const handleSearch = (value: string) => { + setPage(1); + setQuery(value); + }; const onSubmit = (state: SecretBuilderState, resetBuilderState: () => void) => { createSecretMutation({ @@ -140,18 +196,21 @@ export const SecretsList = () => { .catch((e) => { message.destroy(); message.error({ - content: `Failed to update secret!: \n ${e.message || ''}`, + content: `Failed to update Secret!: \n ${e.message || ''}`, duration: 3, }); }); }; + const onUpdate = (state: SecretBuilderState, resetBuilderState: () => void) => { + const secretValue = state.value || ''; + updateSecretMutation({ variables: { input: { urn: state.urn as string, name: state.name as string, - value: state.value as string, + value: secretValue, description: state.description as string, }, }, @@ -214,100 +273,85 @@ export const SecretsList = () => { const tableColumns = [ { title: 'Name', - dataIndex: 'name', key: 'name', - render: (name: string) => {name}, + render: (record: TableDataType) => {record.name}, + sorter: (a: TableDataType, b: TableDataType) => a.name.localeCompare(b.name), }, { title: 'Description', - dataIndex: 'description', key: 'description', - render: (description: any) => { - return <>{description || No description}; + render: (record: TableDataType) => { + return <>{record.description || No description}; }, }, { title: '', - dataIndex: '', - key: 'x', - render: (_, record: any) => ( + key: 'actions', + render: (record: TableDataType) => ( - - + + ), }, ]; - const tableData = secrets?.map((secret) => ({ - urn: secret.urn, - name: secret.name, - description: secret.description, - })); - - const FinalStyledTable = isShowNavBarRedesign ? StyledTableWithNavBarRedesign : StyledTable; + const tableData = + secrets?.map((secret) => ({ + urn: secret.urn, + name: secret.name, + description: secret.description || null, + })) || []; return ( <> {!data && loading && } {error && message.error({ content: `Failed to load secrets! \n ${error.message || ''}`, duration: 3 })}
- -
- -
- null} - onQueryChange={(q) => { - setPage(1); - debouncedSetQuery(q); - }} - entityRegistry={entityRegistry} - hideRecommendations - /> -
- , - }} - pagination={false} - /> - - - + + + handleSearch(value)} + /> + + + {tableData.length === 0 ? ( + + ) : ( + <> + + + + + + + + )} { return undefined; }; -export const IngestionSourceList = () => { - const entityRegistry = useEntityRegistry(); +type Props = { + showCreateModal?: boolean; + setShowCreateModal?: (show: boolean) => void; +}; + +export const IngestionSourceList = ({ showCreateModal, setShowCreateModal }: Props) => { const location = useLocation(); const params = QueryString.parse(location.search, { arrayFormat: 'comma' }); - const paramsQuery = (params?.query as string) || undefined; + const paramsQuery = (params?.[INGESTION_TAB_QUERY_PARAMS.searchQuery] as string) || undefined; + const [query, setQuery] = useState(undefined); const searchInputRef = useRef(null); // highlight search input if user arrives with a query preset for salience @@ -111,6 +150,14 @@ export const IngestionSourceList = () => { const [sort, setSort] = useState(); const [hideSystemSources, setHideSystemSources] = useState(true); + // Add a useEffect to handle the showCreateModal prop + useEffect(() => { + if (showCreateModal && setShowCreateModal) { + setIsBuildingSource(true); + setShowCreateModal(false); + } + }, [showCreateModal, setShowCreateModal]); + // When source filter changes, reset page to 1 useEffect(() => { setPage(1); @@ -125,6 +172,7 @@ export const IngestionSourceList = () => { const filters = hideSystemSources ? [{ field: 'sourceType', values: [SYSTEM_INTERNAL_SOURCE_TYPE], negated: true }] : [{ field: 'sourceType', values: [SYSTEM_INTERNAL_SOURCE_TYPE] }]; + if (sourceFilter !== IngestionSourceType.ALL) { filters.push({ field: 'sourceExecutorId', @@ -165,9 +213,10 @@ export const IngestionSourceList = () => { setLastRefresh(new Date().getTime()); }, [refetch]); - const debouncedSetQuery = debounce((newQuery: string | undefined) => { - setQuery(newQuery); - }, ONE_SECOND_IN_MS); + const handleSearch = (value: string) => { + setPage(1); + setQuery(value); + }; function hasActiveExecution() { return !!filteredSources.find((source) => @@ -412,67 +461,49 @@ export const IngestionSourceList = () => { )} - -
- - -
- - setSourceFilter(selection as IngestionSourceType)} - > - All - UI - CLI - - - null} - onQueryChange={(q) => { - setPage(1); - debouncedSetQuery(q); - }} - entityRegistry={entityRegistry} - hideRecommendations - /> - -
- - - + + + + handleSearch(value)} + /> + setSourceFilter(Number(values[0]))} + showClear={false} + width={60} + /> + + + + + + + + + + + { onChange={onChangePage} showSizeChanger={false} /> - +
( +
+ +
+); + +type TableDataType = { + urn: string; + name: string; + description: string | null; +}; const DEFAULT_PAGE_SIZE = 25; @@ -56,8 +99,6 @@ interface Props { } export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateModal: setIsCreatingSecret }: Props) => { - const isShowNavBarRedesign = useShowNavBarRedesign(); - const entityRegistry = useEntityRegistry(); const location = useLocation(); const params = QueryString.parse(location.search, { arrayFormat: 'comma' }); const paramsQuery = (params?.query as string) || undefined; @@ -109,9 +150,10 @@ export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateMo setPage(newPage); }; - const debouncedSetQuery = debounce((newQuery: string | undefined) => { - setQuery(newQuery); - }, ONE_SECOND_IN_MS); + const handleSearch = (value: string) => { + setPage(1); + setQuery(value); + }; const onSubmit = (state: SecretBuilderState, resetBuilderState: () => void) => { createSecretMutation({ @@ -217,101 +259,114 @@ export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateMo const tableColumns = [ { title: 'Name', - dataIndex: 'name', key: 'name', - render: (name: string) => {name}, + render: (record: TableDataType) => ( + + {record.name} + + ), + sorter: (a: TableDataType, b: TableDataType) => a.name.localeCompare(b.name), }, { title: 'Description', - dataIndex: 'description', key: 'description', - render: (description: any) => { - return <>{description || No description}; + render: (record: TableDataType) => { + return ( + + {record.description || 'No description'} + + ); }, + width: '75%', }, { title: '', - dataIndex: '', - key: 'x', - render: (_, record: any) => ( - - - - + key: 'actions', + render: (record: TableDataType) => ( + + + + ), + width: '100px', }, ]; - const tableData = secrets?.map((secret) => ({ - urn: secret.urn, - name: secret.name, - description: secret.description, - })); - - const FinalStyledTable = isShowNavBarRedesign ? StyledTableWithNavBarRedesign : StyledTable; + const tableData = + secrets?.map((secret) => ({ + urn: secret.urn, + name: secret.name, + description: secret.description || null, + })) || []; return ( <> {!data && loading && } {error && message.error({ content: `Failed to load secrets! \n ${error.message || ''}`, duration: 3 })} -
- -
- -
- null} - onQueryChange={(q) => { - setPage(1); - debouncedSetQuery(q); - }} - entityRegistry={entityRegistry} - hideRecommendations - /> -
- , - }} - pagination={false} - /> - - - -
+ + + + handleSearch(value)} + /> + + + {tableData.length === 0 ? ( + + ) : ( + <> + +
+ + + + )} +