feat(ui/ingestion): add empty and loading states for sources and secrets tables (#13646)

Co-authored-by: Chris Collins <chriscollins3456@gmail.com>
This commit is contained in:
purnimagarg1 2025-05-31 06:14:02 +05:30 committed by GitHub
parent 313997cc76
commit 2620af8e2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 121 additions and 82 deletions

View File

@ -178,14 +178,14 @@ export const ExecutionDetailsModal = ({ urn, open, onClose }: Props) => {
bodyStyle={modalBodyStyle}
title={
<HeaderSection>
<StyledTitle level={4}>Sync Details</StyledTitle>
<StyledTitle level={4}>Execution Run Details</StyledTitle>
</HeaderSection>
}
open={open}
onCancel={onClose}
>
{!data && loading && <Message type="loading" content="Loading sync details..." />}
{error && message.error('Failed to load sync details :(')}
{!data && loading && <Message type="loading" content="Loading execution run details..." />}
{error && message.error('Failed to load execution run details :(')}
<Section>
<StatusSection>
<Typography.Title level={5}>Status</Typography.Title>

View File

@ -0,0 +1,44 @@
import { Text } from '@components';
import React from 'react';
import styled from 'styled-components';
import { EmptyContainer } from '@app/govern/structuredProperties/styledComponents';
import EmptyFormsImage from '@src/images/empty-forms.svg?react';
export const TextContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`;
interface Props {
sourceType?: string;
isEmptySearchResult?: boolean;
}
const EmptySources = ({ sourceType, isEmptySearchResult }: Props) => {
return (
<EmptyContainer>
{isEmptySearchResult ? (
<TextContainer>
<Text size="lg" color="gray" weight="bold">
No search results!
</Text>
<Text size="sm" color="gray" weight="normal">
Try another search query with at least 3 characters...
</Text>
</TextContainer>
) : (
<>
<EmptyFormsImage />
<Text size="md" color="gray" weight="bold">
{`No ${sourceType || 'sources'} yet!`}
</Text>
</>
)}
</EmptyContainer>
);
};
export default EmptySources;

View File

@ -1,11 +1,12 @@
import { Icon, Pagination, SearchBar, Table, colors } from '@components';
import { Empty, Modal, Typography, message } from 'antd';
import { 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 TabToolbar from '@app/entity/shared/components/styled/TabToolbar';
import EmptySources from '@app/ingestV2/EmptySources';
import { SecretBuilderModal } from '@app/ingestV2/secret/SecretBuilderModal';
import {
addSecretToListSecretsCache,
@ -13,8 +14,8 @@ import {
updateSecretInListSecretsCache,
} from '@app/ingestV2/secret/cacheUtils';
import { SecretBuilderState } from '@app/ingestV2/secret/types';
import { Message } from '@app/shared/Message';
import { scrollToTop } from '@app/shared/searchUtils';
import { ConfirmationModal } from '@app/sharedV2/modals/ConfirmationModal';
import {
useCreateSecretMutation,
@ -79,12 +80,6 @@ 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;
@ -111,6 +106,7 @@ export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateMo
const start = (page - 1) * pageSize;
const [editSecret, setEditSecret] = useState<SecretBuilderState | undefined>(undefined);
const [showConfirmDelete, setShowConfirmDelete] = useState<boolean>(false);
const [deleteSecretMutation] = useDeleteSecretMutation();
const [createSecretMutation] = useCreateSecretMutation();
@ -143,6 +139,8 @@ export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateMo
message.error({ content: `Failed to remove secret: \n ${e.message || ''}`, duration: 3 });
}
});
setShowConfirmDelete(false);
refetch();
};
const onChangePage = (newPage: number) => {
@ -232,18 +230,8 @@ export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateMo
});
};
const onDeleteSecret = (urn: string) => {
Modal.confirm({
title: `Confirm Secret Removal`,
content: `Are you sure you want to remove this secret? Sources that use it may no longer work as expected.`,
onOk() {
deleteSecret(urn);
},
onCancel() {},
okText: 'Yes',
maskClosable: true,
closable: true,
});
const handleDeleteClose = () => {
setShowConfirmDelete(false);
};
const onEditSecret = (urnData: any) => {
@ -301,21 +289,30 @@ export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateMo
title: '',
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>
<>
<ButtonsContainer>
<button type="button" onClick={() => onEditSecret(record)} aria-label="Edit secret">
<Icon icon="PencilSimpleLine" source="phosphor" />
</button>
<button
type="button"
className="delete-action"
onClick={() => setShowConfirmDelete(true)}
aria-label="Delete secret"
data-test-id="delete-secret-action"
data-icon="delete"
>
<Icon icon="Trash" source="phosphor" color="red" />
</button>
</ButtonsContainer>
<ConfirmationModal
isOpen={showConfirmDelete}
modalTitle="Confirm Secret Removal"
modalText="Are you sure you want to remove this secret? Sources that use it may no longer work as expected."
handleConfirm={() => deleteSecret(record.urn)}
handleClose={handleDeleteClose}
/>
</>
),
width: '100px',
},
@ -330,7 +327,6 @@ export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateMo
return (
<>
{!data && loading && <Message type="loading" content="Loading secrets..." />}
{error && message.error({ content: `Failed to load secrets! \n ${error.message || ''}`, duration: 3 })}
<SecretsContainer>
<StyledTabToolbar>
@ -342,8 +338,8 @@ export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateMo
/>
</SearchContainer>
</StyledTabToolbar>
{tableData.length === 0 ? (
<EmptyState />
{!loading && totalSecrets === 0 ? (
<EmptySources sourceType="secrets" isEmptySearchResult={!!query} />
) : (
<>
<TableContainer>
@ -353,6 +349,7 @@ export const SecretsList = ({ showCreateModal: isCreatingSecret, setShowCreateMo
rowKey="urn"
isScrollable
style={{ tableLayout: 'fixed' }}
isLoading={loading}
/>
</TableContainer>
<Pagination

View File

@ -6,6 +6,7 @@ import { useLocation } from 'react-router';
import styled from 'styled-components';
import analytics, { EventType } from '@app/analytics';
import EmptySources from '@app/ingestV2/EmptySources';
import IngestionSourceTable from '@app/ingestV2/source/IngestionSourceTable';
import RecipeViewerModal from '@app/ingestV2/source/RecipeViewerModal';
import { IngestionSourceBuilderModal } from '@app/ingestV2/source/builder/IngestionSourceBuilderModal';
@ -446,7 +447,6 @@ export const IngestionSourceList = ({ showCreateModal, setShowCreateModal }: Pro
return (
<>
{!data && loading && <Message type="loading" content="Loading ingestion sources..." />}
{error && (
<Message type="error" content="Failed to load ingestion sources! An unexpected error occurred." />
)}
@ -484,29 +484,35 @@ export const IngestionSourceList = ({ showCreateModal, setShowCreateModal }: Pro
</FilterButtonsContainer>
</StyledTabToolbar>
</HeaderContainer>
<TableContainer>
<IngestionSourceTable
sources={filteredSources || []}
setFocusExecutionUrn={setFocusExecutionUrn}
onExecute={onExecute}
onEdit={onEdit}
onView={onView}
onDelete={onDelete}
onChangeSort={onChangeSort}
/>
</TableContainer>
<PaginationContainer>
<Pagination
currentPage={page}
itemsPerPage={pageSize}
totalPages={totalSources}
showLessItems
onPageChange={onChangePage}
showSizeChanger={false}
hideOnSinglePage
/>
</PaginationContainer>
{!loading && totalSources === 0 ? (
<EmptySources sourceType="sources" isEmptySearchResult={!!query} />
) : (
<>
<TableContainer>
<IngestionSourceTable
sources={filteredSources || []}
setFocusExecutionUrn={setFocusExecutionUrn}
onExecute={onExecute}
onEdit={onEdit}
onView={onView}
onDelete={onDelete}
onChangeSort={onChangeSort}
isLoading={loading}
/>
</TableContainer>
<PaginationContainer>
<Pagination
currentPage={page}
itemsPerPage={pageSize}
totalPages={totalSources}
showLessItems
onPageChange={onChangePage}
showSizeChanger={false}
hideOnSinglePage
/>
</PaginationContainer>
</>
)}
</SourceContainer>
<IngestionSourceBuilderModal
initialState={removeExecutionsFromIngestionSource(focusSource)}

View File

@ -26,6 +26,7 @@ interface Props {
onView: (urn: string) => void;
onDelete: (urn: string) => void;
onChangeSort: (field: string, order: SorterResult<any>['order']) => void;
isLoading?: boolean;
}
function IngestionSourceTable({
@ -36,6 +37,7 @@ function IngestionSourceTable({
onView,
onDelete,
onChangeSort,
isLoading,
}: Props) {
const tableData = sources.map((source) => ({
urn: source.urn,
@ -121,6 +123,7 @@ function IngestionSourceTable({
data={tableData}
isScrollable
handleSortColumnChange={handleSortColumnChange}
isLoading={isLoading}
/>
);
}

View File

@ -1,6 +1,6 @@
import { DownloadOutlined } from '@ant-design/icons';
import { Icon, Pill } from '@components';
import { Button, Modal, Typography, message } from 'antd';
import { Icon, Modal, Pill } from '@components';
import { Button, Typography, message } from 'antd';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import YAML from 'yamljs';
@ -25,11 +25,6 @@ import { Message } from '@app/shared/Message';
import { useGetIngestionExecutionRequestQuery } from '@graphql/ingestion.generated';
import { ExecutionRequestResult } from '@types';
const StyledTitle = styled(Typography.Title)`
padding: 0px;
margin: 0px;
`;
const Section = styled.div`
display: flex;
flex-direction: column;
@ -54,8 +49,6 @@ const SubHeaderParagraph = styled(Typography.Paragraph)`
margin-bottom: 0px;
`;
const HeaderSection = styled.div``;
const StatusSection = styled.div`
border-bottom: 1px solid ${ANTD_GRAY[4]};
padding: 16px;
@ -180,19 +173,15 @@ export const ExecutionDetailsModal = ({ urn, open, onClose }: Props) => {
return (
<Modal
width={800}
footer={<Button onClick={onClose}>Close</Button>}
style={modalStyle}
bodyStyle={modalBodyStyle}
title={
<HeaderSection>
<StyledTitle level={4}>Sync Details</StyledTitle>
</HeaderSection>
}
title="Execution Run Details"
open={open}
onCancel={onClose}
buttons={[{ text: 'Close', variant: 'outline', onClick: onClose }]}
>
{!data && loading && <Message type="loading" content="Loading sync details..." />}
{error && message.error('Failed to load sync details :(')}
{!data && loading && <Message type="loading" content="Loading execution run details..." />}
{error && message.error('Failed to load execution run details :(')}
<Section>
<StatusSection>
<Typography.Title level={5}>Status</Typography.Title>