mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-25 00:48:45 +00:00
feat(ingestion): update the layout of secrets tab (#13627)
This commit is contained in:
parent
de20d4ec20
commit
d2f9db2d9d
@ -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>(TabType.Sources);
|
||||
const showIngestionTab = isIngestionEnabled && platformPrivileges?.manageIngestion;
|
||||
const showSecretsTab = isIngestionEnabled && platformPrivileges?.manageSecrets;
|
||||
|
||||
// undefined == not loaded, null == no permissions
|
||||
const [selectedTab, setSelectedTab] = useState<TabType | undefined | null>();
|
||||
|
||||
const [showCreateSourceModal, setShowCreateSourceModal] = useState<boolean>(false);
|
||||
const [showCreateSecretModal, setShowCreateSecretModal] = useState<boolean>(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]: <IngestionSourceList />,
|
||||
[TabType.Secrets]: <SecretsList />,
|
||||
const handleCreateSource = () => {
|
||||
setShowCreateSourceModal(true);
|
||||
};
|
||||
|
||||
const handleCreateSecret = () => {
|
||||
setShowCreateSecretModal(true);
|
||||
};
|
||||
|
||||
const TabTypeToListComponent = {
|
||||
[TabType.Sources]: (
|
||||
<IngestionSourceList
|
||||
showCreateModal={showCreateSourceModal}
|
||||
setShowCreateModal={setShowCreateSourceModal}
|
||||
/>
|
||||
),
|
||||
[TabType.Secrets]: (
|
||||
<SecretsList showCreateModal={showCreateSecretModal} setShowCreateModal={setShowCreateSecretModal} />
|
||||
),
|
||||
};
|
||||
|
||||
if (selectedTab === undefined) {
|
||||
return <></>; // loading
|
||||
}
|
||||
if (selectedTab === null) {
|
||||
return <NoPageFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer $isShowNavBarRedesign={isShowNavBarRedesign}>
|
||||
<OnboardingTour stepIds={[INGESTION_CREATE_SOURCE_ID, INGESTION_REFRESH_SOURCES_ID]} />
|
||||
<OnboardingTour stepIds={[INGESTION_CREATE_SOURCE_ID]} />
|
||||
<PageHeaderContainer>
|
||||
<PageTitle level={3}>Manage Data Sources</PageTitle>
|
||||
<Typography.Paragraph type="secondary">
|
||||
Configure and schedule syncs to import data from your data sources
|
||||
</Typography.Paragraph>
|
||||
<TitleContainer>
|
||||
<PageTitle
|
||||
title="Manage Data Sources"
|
||||
subTitle="Configure and schedule syncs to import data from your data sources"
|
||||
/>
|
||||
</TitleContainer>
|
||||
<HeaderActionsContainer>
|
||||
{selectedTab === TabType.Sources && showIngestionTab && (
|
||||
<Button
|
||||
variant="filled"
|
||||
id={INGESTION_CREATE_SOURCE_ID}
|
||||
onClick={handleCreateSource}
|
||||
data-testid="create-ingestion-source-button"
|
||||
>
|
||||
<PlusOutlined style={{ marginRight: '4px' }} /> Create new source
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{selectedTab === TabType.Secrets && showSecretsTab && (
|
||||
<Button variant="filled" onClick={handleCreateSecret} data-testid="create-secret-button">
|
||||
<PlusOutlined style={{ marginRight: '4px' }} /> Create new secret
|
||||
</Button>
|
||||
)}
|
||||
</HeaderActionsContainer>
|
||||
</PageHeaderContainer>
|
||||
<StyledTabs
|
||||
activeKey={selectedTab}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Modal, Pagination, Typography, message } from 'antd';
|
||||
import { debounce } from 'lodash';
|
||||
import { SearchBar } from '@components';
|
||||
import { Empty, Modal, Typography, message } from 'antd';
|
||||
import { PencilSimpleLine, Trash } from 'phosphor-react';
|
||||
import * as QueryString from 'query-string';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { StyledTable } from '@app/entity/shared/components/styled/StyledTable';
|
||||
import TabToolbar from '@app/entity/shared/components/styled/TabToolbar';
|
||||
import { ONE_SECOND_IN_MS } from '@app/entity/shared/tabs/Dataset/Queries/utils/constants';
|
||||
import { SecretBuilderModal } from '@app/ingest/secret/SecretBuilderModal';
|
||||
import {
|
||||
addSecretToListSecretsCache,
|
||||
@ -16,10 +14,9 @@ import {
|
||||
updateSecretInListSecretsCache,
|
||||
} from '@app/ingest/secret/cacheUtils';
|
||||
import { SecretBuilderState } from '@app/ingest/secret/types';
|
||||
import { SearchBar } from '@app/search/SearchBar';
|
||||
import { Message } from '@app/shared/Message';
|
||||
import { scrollToTop } from '@app/shared/searchUtils';
|
||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||
import { Pagination, Table } from '@src/alchemy-components';
|
||||
import { useShowNavBarRedesign } from '@src/app/useShowNavBarRedesign';
|
||||
|
||||
import {
|
||||
@ -32,27 +29,87 @@ import {
|
||||
const DeleteButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
`;
|
||||
gap: 8px;
|
||||
|
||||
const SourcePaginationContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
button {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 20px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 4px;
|
||||
color: #595959;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
:hover {
|
||||
color: #262626;
|
||||
border-color: #262626;
|
||||
}
|
||||
|
||||
const StyledTableWithNavBarRedesign = styled(StyledTable)`
|
||||
overflow: hidden;
|
||||
|
||||
&&& .ant-table-body {
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 450px);
|
||||
&.delete-action {
|
||||
color: #ff4d4f;
|
||||
:hover {
|
||||
color: #cf1322;
|
||||
border-color: #262626;
|
||||
}
|
||||
}
|
||||
}
|
||||
` as typeof StyledTable;
|
||||
`;
|
||||
|
||||
const EmptyState = () => (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<Empty description="No Secrets found!" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</div>
|
||||
);
|
||||
|
||||
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<boolean>(false);
|
||||
const [editSecret, setEditSecret] = useState<SecretBuilderState | undefined>(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) => <Typography.Text strong>{name}</Typography.Text>,
|
||||
render: (record: TableDataType) => <Typography.Text strong>{record.name}</Typography.Text>,
|
||||
sorter: (a: TableDataType, b: TableDataType) => a.name.localeCompare(b.name),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
render: (description: any) => {
|
||||
return <>{description || <Typography.Text type="secondary">No description</Typography.Text>}</>;
|
||||
render: (record: TableDataType) => {
|
||||
return <>{record.description || <Typography.Text type="secondary">No description</Typography.Text>}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: '',
|
||||
key: 'x',
|
||||
render: (_, record: any) => (
|
||||
key: 'actions',
|
||||
render: (record: TableDataType) => (
|
||||
<DeleteButtonContainer>
|
||||
<Button style={{ marginRight: 16 }} onClick={() => onEditSecret(record)}>
|
||||
EDIT
|
||||
</Button>
|
||||
<Button onClick={() => onDeleteSecret(record.urn)} type="text" shape="circle" danger>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
<button type="button" onClick={() => onEditSecret(record)} aria-label="Edit secret">
|
||||
<PencilSimpleLine size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="delete-action"
|
||||
onClick={() => onDeleteSecret(record.urn)}
|
||||
aria-label="Delete secret"
|
||||
data-test-id="delete-secret-action"
|
||||
data-icon="delete"
|
||||
>
|
||||
<Trash size={16} />
|
||||
</button>
|
||||
</DeleteButtonContainer>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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 && <Message type="loading" content="Loading secrets..." />}
|
||||
{error && message.error({ content: `Failed to load secrets! \n ${error.message || ''}`, duration: 3 })}
|
||||
<div>
|
||||
<TabToolbar>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="create-secret-button"
|
||||
type="text"
|
||||
onClick={() => setIsCreatingSecret(true)}
|
||||
>
|
||||
<PlusOutlined /> Create new secret
|
||||
</Button>
|
||||
</div>
|
||||
<SearchBar
|
||||
initialQuery={query || ''}
|
||||
placeholderText="Search secrets..."
|
||||
suggestions={[]}
|
||||
style={{
|
||||
maxWidth: 220,
|
||||
padding: 0,
|
||||
}}
|
||||
inputStyle={{
|
||||
height: 32,
|
||||
fontSize: 12,
|
||||
}}
|
||||
onSearch={() => null}
|
||||
onQueryChange={(q) => {
|
||||
setPage(1);
|
||||
debouncedSetQuery(q);
|
||||
}}
|
||||
entityRegistry={entityRegistry}
|
||||
hideRecommendations
|
||||
/>
|
||||
</TabToolbar>
|
||||
<FinalStyledTable
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
scroll={isShowNavBarRedesign ? { y: 'max-content' } : {}}
|
||||
rowKey="urn"
|
||||
locale={{
|
||||
emptyText: <Empty description="No Secrets found!" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
}}
|
||||
pagination={false}
|
||||
/>
|
||||
<SourcePaginationContainer>
|
||||
<Pagination
|
||||
style={{ margin: 40 }}
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={totalSecrets}
|
||||
showLessItems
|
||||
onChange={onChangePage}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</SourcePaginationContainer>
|
||||
<StyledTabToolbar>
|
||||
<SearchContainer>
|
||||
<StyledSearchBar
|
||||
placeholder="Search..."
|
||||
value={query || ''}
|
||||
onChange={(value) => handleSearch(value)}
|
||||
/>
|
||||
</SearchContainer>
|
||||
</StyledTabToolbar>
|
||||
{tableData.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
<>
|
||||
<TableContainer>
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
data={tableData}
|
||||
rowKey="urn"
|
||||
isScrollable={isShowNavBarRedesign}
|
||||
maxHeight={isShowNavBarRedesign ? 'calc(100vh - 450px)' : undefined}
|
||||
showHeader
|
||||
/>
|
||||
</TableContainer>
|
||||
<PaginationContainer>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
itemsPerPage={pageSize}
|
||||
totalPages={totalSecrets}
|
||||
onPageChange={onChangePage}
|
||||
/>
|
||||
</PaginationContainer>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<SecretBuilderModal
|
||||
open={isCreatingSecret}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PlusOutlined, RedoOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal, Pagination, Select, message } from 'antd';
|
||||
import { debounce } from 'lodash';
|
||||
import { Button, SearchBar, SimpleSelect } from '@components';
|
||||
import { Modal, Pagination, message } from 'antd';
|
||||
import { ArrowClockwise } from 'phosphor-react';
|
||||
import * as QueryString from 'query-string';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
@ -8,7 +8,7 @@ import styled from 'styled-components';
|
||||
|
||||
import analytics, { EventType } from '@app/analytics';
|
||||
import TabToolbar from '@app/entity/shared/components/styled/TabToolbar';
|
||||
import { ONE_SECOND_IN_MS } from '@app/entity/shared/tabs/Dataset/Queries/utils/constants';
|
||||
import { INGESTION_TAB_QUERY_PARAMS } from '@app/ingest/constants';
|
||||
import IngestionSourceTable from '@app/ingest/source/IngestionSourceTable';
|
||||
import RecipeViewerModal from '@app/ingest/source/RecipeViewerModal';
|
||||
import { IngestionSourceBuilderModal } from '@app/ingest/source/builder/IngestionSourceBuilderModal';
|
||||
@ -22,14 +22,10 @@ import {
|
||||
addToListIngestionSourcesCache,
|
||||
removeFromListIngestionSourcesCache,
|
||||
} from '@app/ingest/source/utils';
|
||||
import {
|
||||
INGESTION_CREATE_SOURCE_ID,
|
||||
INGESTION_REFRESH_SOURCES_ID,
|
||||
} from '@app/onboarding/config/IngestionOnboardingConfig';
|
||||
import { SearchBar } from '@app/search/SearchBar';
|
||||
import { INGESTION_REFRESH_SOURCES_ID } from '@app/onboarding/config/IngestionOnboardingConfig';
|
||||
import { Message } from '@app/shared/Message';
|
||||
import { scrollToTop } from '@app/shared/searchUtils';
|
||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||
import { OnboardingTour } from '@src/app/onboarding/OnboardingTour';
|
||||
|
||||
import {
|
||||
useCreateIngestionExecutionRequestMutation,
|
||||
@ -42,28 +38,66 @@ import { IngestionSource, SortCriterion, SortOrder, UpdateIngestionSourceInput }
|
||||
|
||||
const PLACEHOLDER_URN = 'placeholder-urn';
|
||||
|
||||
const SourceContainer = styled.div``;
|
||||
const SourceContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const SourcePaginationContainer = styled.div`
|
||||
const HeaderContainer = styled.div`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const TableContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const PaginationContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
margin-right: 15px;
|
||||
min-width: 75px;
|
||||
const StyledTabToolbar = styled(TabToolbar)`
|
||||
padding: 16px 20px;
|
||||
height: auto;
|
||||
z-index: unset;
|
||||
&&& {
|
||||
padding: 8px 20px;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const FilterWrapper = styled.div`
|
||||
const SearchContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const RefreshButtonContainer = styled.div``;
|
||||
|
||||
const StyledSearchBar = styled(SearchBar)`
|
||||
width: 220px;
|
||||
`;
|
||||
|
||||
const StyledPagination = styled(Pagination)`
|
||||
margin: 15px;
|
||||
`;
|
||||
|
||||
const StyledSimpleSelect = styled(SimpleSelect)`
|
||||
display: flex;
|
||||
align-self: start;
|
||||
`;
|
||||
|
||||
const SYSTEM_INTERNAL_SOURCE_TYPE = 'SYSTEM';
|
||||
|
||||
export enum IngestionSourceType {
|
||||
ALL,
|
||||
UI,
|
||||
CLI,
|
||||
enum IngestionSourceType {
|
||||
ALL = 0,
|
||||
UI = 1,
|
||||
CLI = 2,
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
@ -80,11 +114,16 @@ const removeExecutionsFromIngestionSource = (source) => {
|
||||
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 | string>(undefined);
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||
// highlight search input if user arrives with a query preset for salience
|
||||
@ -111,6 +150,14 @@ export const IngestionSourceList = () => {
|
||||
const [sort, setSort] = useState<SortCriterion>();
|
||||
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 = () => {
|
||||
<Message type="error" content="Failed to load ingestion sources! An unexpected error occurred." />
|
||||
)}
|
||||
<SourceContainer>
|
||||
<TabToolbar>
|
||||
<div>
|
||||
<Button
|
||||
id={INGESTION_CREATE_SOURCE_ID}
|
||||
type="text"
|
||||
onClick={() => setIsBuildingSource(true)}
|
||||
data-testid="create-ingestion-source-button"
|
||||
>
|
||||
<PlusOutlined /> Create new source
|
||||
</Button>
|
||||
<Button id={INGESTION_REFRESH_SOURCES_ID} type="text" onClick={onRefresh}>
|
||||
<RedoOutlined /> Refresh
|
||||
</Button>
|
||||
</div>
|
||||
<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
|
||||
searchInputRef={searchInputRef}
|
||||
initialQuery={query || ''}
|
||||
placeholderText="Search sources..."
|
||||
suggestions={[]}
|
||||
style={{
|
||||
maxWidth: 220,
|
||||
padding: 0,
|
||||
}}
|
||||
inputStyle={{
|
||||
height: 32,
|
||||
fontSize: 12,
|
||||
}}
|
||||
onSearch={() => null}
|
||||
onQueryChange={(q) => {
|
||||
setPage(1);
|
||||
debouncedSetQuery(q);
|
||||
}}
|
||||
entityRegistry={entityRegistry}
|
||||
hideRecommendations
|
||||
/>
|
||||
</FilterWrapper>
|
||||
</TabToolbar>
|
||||
<IngestionSourceTable
|
||||
lastRefresh={lastRefresh}
|
||||
sources={filteredSources || []}
|
||||
setFocusExecutionUrn={setFocusExecutionUrn}
|
||||
onExecute={onExecute}
|
||||
onEdit={onEdit}
|
||||
onView={onView}
|
||||
onDelete={onDelete}
|
||||
onRefresh={onRefresh}
|
||||
onChangeSort={onChangeSort}
|
||||
/>
|
||||
<SourcePaginationContainer>
|
||||
<Pagination
|
||||
style={{ margin: 15 }}
|
||||
<OnboardingTour stepIds={[INGESTION_REFRESH_SOURCES_ID]} />
|
||||
<HeaderContainer>
|
||||
<StyledTabToolbar>
|
||||
<SearchContainer>
|
||||
<StyledSearchBar
|
||||
placeholder="Search..."
|
||||
value={query || ''}
|
||||
onChange={(value) => handleSearch(value)}
|
||||
/>
|
||||
<StyledSimpleSelect
|
||||
options={[
|
||||
{ label: 'All', value: '0' },
|
||||
{ label: 'UI', value: '1' },
|
||||
{ label: 'CLI', value: '2' },
|
||||
]}
|
||||
values={[sourceFilter.toString()]}
|
||||
onUpdate={(values) => setSourceFilter(Number(values[0]))}
|
||||
showClear={false}
|
||||
width={60}
|
||||
/>
|
||||
</SearchContainer>
|
||||
<RefreshButtonContainer>
|
||||
<Button id={INGESTION_REFRESH_SOURCES_ID} variant="text" onClick={onRefresh}>
|
||||
<ArrowClockwise /> Refresh
|
||||
</Button>
|
||||
</RefreshButtonContainer>
|
||||
</StyledTabToolbar>
|
||||
</HeaderContainer>
|
||||
<TableContainer>
|
||||
<IngestionSourceTable
|
||||
lastRefresh={lastRefresh}
|
||||
sources={filteredSources || []}
|
||||
setFocusExecutionUrn={setFocusExecutionUrn}
|
||||
onExecute={onExecute}
|
||||
onEdit={onEdit}
|
||||
onView={onView}
|
||||
onDelete={onDelete}
|
||||
onRefresh={onRefresh}
|
||||
onChangeSort={onChangeSort}
|
||||
/>
|
||||
</TableContainer>
|
||||
<PaginationContainer>
|
||||
<StyledPagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={totalSources}
|
||||
@ -480,7 +511,7 @@ export const IngestionSourceList = () => {
|
||||
onChange={onChangePage}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</SourcePaginationContainer>
|
||||
</PaginationContainer>
|
||||
</SourceContainer>
|
||||
<IngestionSourceBuilderModal
|
||||
initialState={removeExecutionsFromIngestionSource(focusSource)}
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Modal, Pagination, Typography, message } from 'antd';
|
||||
import { debounce } from 'lodash';
|
||||
import { Icon, Pagination, SearchBar, Table, colors } from '@components';
|
||||
import { Empty, Modal, Typography, message } from 'antd';
|
||||
import * as QueryString from 'query-string';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { StyledTable } from '@app/entity/shared/components/styled/StyledTable';
|
||||
import TabToolbar from '@app/entity/shared/components/styled/TabToolbar';
|
||||
import { ONE_SECOND_IN_MS } from '@app/entity/shared/tabs/Dataset/Queries/utils/constants';
|
||||
import { SecretBuilderModal } from '@app/ingestV2/secret/SecretBuilderModal';
|
||||
import {
|
||||
addSecretToListSecretsCache,
|
||||
@ -16,11 +13,8 @@ import {
|
||||
updateSecretInListSecretsCache,
|
||||
} from '@app/ingestV2/secret/cacheUtils';
|
||||
import { SecretBuilderState } from '@app/ingestV2/secret/types';
|
||||
import { SearchBar } from '@app/search/SearchBar';
|
||||
import { Message } from '@app/shared/Message';
|
||||
import { scrollToTop } from '@app/shared/searchUtils';
|
||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||
import { useShowNavBarRedesign } from '@src/app/useShowNavBarRedesign';
|
||||
|
||||
import {
|
||||
useCreateSecretMutation,
|
||||
@ -29,24 +23,73 @@ import {
|
||||
useUpdateSecretMutation,
|
||||
} from '@graphql/ingestion.generated';
|
||||
|
||||
const DeleteButtonContainer = styled.div`
|
||||
const ButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
`;
|
||||
justify-content: end;
|
||||
gap: 8px;
|
||||
|
||||
const SourcePaginationContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
button {
|
||||
border: 1px solid ${colors.gray[100]};
|
||||
border-radius: 20px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
color: ${colors.gray[1800]};
|
||||
|
||||
const StyledTableWithNavBarRedesign = styled(StyledTable)`
|
||||
overflow: hidden;
|
||||
|
||||
&&& .ant-table-body {
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 450px);
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
` as typeof StyledTable;
|
||||
`;
|
||||
|
||||
const SecretsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const StyledTabToolbar = styled(TabToolbar)`
|
||||
padding: 0 20px 16px 0;
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
border-bottom: none;
|
||||
`;
|
||||
|
||||
const SearchContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const StyledSearchBar = styled(SearchBar)`
|
||||
width: 400px;
|
||||
`;
|
||||
|
||||
const TableContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const TextContainer = styled(Typography.Text)`
|
||||
color: ${colors.gray[1700]};
|
||||
`;
|
||||
|
||||
const EmptyState = () => (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
<Empty description="No Secrets found!" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</div>
|
||||
);
|
||||
|
||||
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) => <Typography.Text strong>{name}</Typography.Text>,
|
||||
render: (record: TableDataType) => (
|
||||
<TextContainer
|
||||
ellipsis={{
|
||||
tooltip: {
|
||||
title: record.name,
|
||||
color: 'white',
|
||||
overlayInnerStyle: { color: colors.gray[1700] },
|
||||
showArrow: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{record.name}
|
||||
</TextContainer>
|
||||
),
|
||||
sorter: (a: TableDataType, b: TableDataType) => a.name.localeCompare(b.name),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
render: (description: any) => {
|
||||
return <>{description || <Typography.Text type="secondary">No description</Typography.Text>}</>;
|
||||
render: (record: TableDataType) => {
|
||||
return (
|
||||
<TextContainer
|
||||
ellipsis={{
|
||||
tooltip: {
|
||||
title: record.description,
|
||||
color: 'white',
|
||||
overlayInnerStyle: { color: colors.gray[1700] },
|
||||
showArrow: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{record.description || 'No description'}
|
||||
</TextContainer>
|
||||
);
|
||||
},
|
||||
width: '75%',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: '',
|
||||
key: 'x',
|
||||
render: (_, record: any) => (
|
||||
<DeleteButtonContainer>
|
||||
<Button style={{ marginRight: 16 }} onClick={() => onEditSecret(record)}>
|
||||
EDIT
|
||||
</Button>
|
||||
<Button onClick={() => onDeleteSecret(record.urn)} type="text" shape="circle" danger>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</DeleteButtonContainer>
|
||||
key: 'actions',
|
||||
render: (record: TableDataType) => (
|
||||
<ButtonsContainer>
|
||||
<button type="button" onClick={() => onEditSecret(record)} aria-label="Edit secret">
|
||||
<Icon icon="PencilSimpleLine" source="phosphor" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="delete-action"
|
||||
onClick={() => onDeleteSecret(record.urn)}
|
||||
aria-label="Delete secret"
|
||||
data-test-id="delete-secret-action"
|
||||
data-icon="delete"
|
||||
>
|
||||
<Icon icon="Trash" source="phosphor" color="red" />
|
||||
</button>
|
||||
</ButtonsContainer>
|
||||
),
|
||||
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 && <Message type="loading" content="Loading secrets..." />}
|
||||
{error && message.error({ content: `Failed to load secrets! \n ${error.message || ''}`, duration: 3 })}
|
||||
<div>
|
||||
<TabToolbar>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="create-secret-button"
|
||||
type="text"
|
||||
onClick={() => setIsCreatingSecret(true)}
|
||||
>
|
||||
<PlusOutlined /> Create new secret
|
||||
</Button>
|
||||
</div>
|
||||
<SearchBar
|
||||
initialQuery={query || ''}
|
||||
placeholderText="Search secrets..."
|
||||
suggestions={[]}
|
||||
style={{
|
||||
maxWidth: 220,
|
||||
padding: 0,
|
||||
}}
|
||||
inputStyle={{
|
||||
height: 32,
|
||||
fontSize: 12,
|
||||
}}
|
||||
onSearch={() => null}
|
||||
onQueryChange={(q) => {
|
||||
setPage(1);
|
||||
debouncedSetQuery(q);
|
||||
}}
|
||||
entityRegistry={entityRegistry}
|
||||
hideRecommendations
|
||||
/>
|
||||
</TabToolbar>
|
||||
<FinalStyledTable
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
scroll={isShowNavBarRedesign ? { y: 'max-content' } : {}}
|
||||
rowKey="urn"
|
||||
locale={{
|
||||
emptyText: <Empty description="No Secrets found!" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
}}
|
||||
pagination={false}
|
||||
/>
|
||||
<SourcePaginationContainer>
|
||||
<Pagination
|
||||
style={{ margin: 40 }}
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={totalSecrets}
|
||||
showLessItems
|
||||
onChange={onChangePage}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</SourcePaginationContainer>
|
||||
</div>
|
||||
<SecretsContainer>
|
||||
<StyledTabToolbar>
|
||||
<SearchContainer>
|
||||
<StyledSearchBar
|
||||
placeholder="Search..."
|
||||
value={query || ''}
|
||||
onChange={(value) => handleSearch(value)}
|
||||
/>
|
||||
</SearchContainer>
|
||||
</StyledTabToolbar>
|
||||
{tableData.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
<>
|
||||
<TableContainer>
|
||||
<Table
|
||||
columns={tableColumns}
|
||||
data={tableData}
|
||||
rowKey="urn"
|
||||
isScrollable
|
||||
style={{ tableLayout: 'fixed' }}
|
||||
/>
|
||||
</TableContainer>
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
itemsPerPage={pageSize}
|
||||
totalPages={totalSecrets}
|
||||
showLessItems
|
||||
onChange={onChangePage}
|
||||
showSizeChanger={false}
|
||||
hideOnSinglePage
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SecretsContainer>
|
||||
<SecretBuilderModal
|
||||
open={isCreatingSecret}
|
||||
editSecret={editSecret}
|
||||
|
||||
@ -53,7 +53,7 @@ export const MenuItem = styled.div`
|
||||
export const ActionIcons = styled.div`
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
|
||||
div {
|
||||
border: 1px solid ${colors.gray[100]};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user