feat(ingestion): update the layout of secrets tab (#13627)

This commit is contained in:
purnimagarg1 2025-05-31 02:12:07 +05:30 committed by GitHub
parent de20d4ec20
commit d2f9db2d9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 524 additions and 325 deletions

View File

@ -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}

View File

@ -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}

View File

@ -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)}

View File

@ -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}

View File

@ -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]};