mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-27 09:58:14 +00:00
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:
parent
313997cc76
commit
2620af8e2a
@ -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>
|
||||
|
||||
44
datahub-web-react/src/app/ingestV2/EmptySources.tsx
Normal file
44
datahub-web-react/src/app/ingestV2/EmptySources.tsx
Normal 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;
|
||||
@ -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
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user