feat(ui): sortable domain list (#6736)

This commit is contained in:
Teppo Naakka 2022-12-22 17:22:22 +02:00 committed by GitHub
parent 86ee0e58de
commit b0a82ca7f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 185 additions and 158 deletions

View File

@ -0,0 +1,65 @@
import React from 'react';
import { DeleteOutlined } from '@ant-design/icons';
import { Dropdown, Menu, message, Modal } from 'antd';
import { EntityType } from '../../types.generated';
import { useEntityRegistry } from '../useEntityRegistry';
import { useDeleteDomainMutation } from '../../graphql/domain.generated';
import { MenuIcon } from '../entity/shared/EntityDropdown/EntityDropdown';
type Props = {
urn: string;
name: string;
onDelete?: () => void;
};
export default function DomainItemMenu({ name, urn, onDelete }: Props) {
const entityRegistry = useEntityRegistry();
const [deleteDomainMutation] = useDeleteDomainMutation();
const deleteDomain = () => {
deleteDomainMutation({
variables: {
urn,
},
})
.then(({ errors }) => {
if (!errors) {
message.success('Deleted Domain!');
onDelete?.();
}
})
.catch(() => {
message.destroy();
message.error({ content: `Failed to delete Domain!: An unknown error occurred.`, duration: 3 });
});
};
const onConfirmDelete = () => {
Modal.confirm({
title: `Delete Domain '${name}'`,
content: `Are you sure you want to remove this ${entityRegistry.getEntityName(EntityType.Domain)}?`,
onOk() {
deleteDomain();
},
onCancel() {},
okText: 'Yes',
maskClosable: true,
closable: true,
});
};
return (
<Dropdown
trigger={['click']}
overlay={
<Menu>
<Menu.Item onClick={onConfirmDelete} key="delete">
<DeleteOutlined /> &nbsp;Delete
</Menu.Item>
</Menu>
}
>
<MenuIcon fontSize={20} />
</Dropdown>
);
}

View File

@ -0,0 +1,65 @@
import React from 'react';
import { Tag, Tooltip, Typography } from 'antd';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Maybe, Ownership } from '../../types.generated';
import { useEntityRegistry } from '../useEntityRegistry';
import AvatarsGroup from '../shared/avatar/AvatarsGroup';
import DomainItemMenu from './DomainItemMenu';
interface DomainEntry {
name: string;
entities: string;
urn: string;
ownership?: Maybe<Ownership>;
url: string;
}
const AvatarGroupWrapper = styled.div`
margin-right: 10px;
display: inline-block;
`;
const DomainNameContainer = styled.div`
margin-left: 16px;
margin-right: 16px;
display: inline;
`;
export function DomainListMenuColumn(handleDelete: (urn: string) => void) {
return (record: DomainEntry) => (
<DomainItemMenu name={record.name} urn={record.urn} onDelete={() => handleDelete(record.urn)} />
);
}
export function DomainNameColumn(logoIcon: JSX.Element) {
return (record: DomainEntry) => (
<Link to={record.url}>
{logoIcon}
<DomainNameContainer>
<Typography.Text>{record.name}</Typography.Text>
</DomainNameContainer>
<Tooltip title={`There are ${record.entities} entities in this domain.`}>
<Tag>{record.entities} entities</Tag>
</Tooltip>
</Link>
);
}
export function DomainOwnersColumn(ownership: Maybe<Ownership>) {
const entityRegistry = useEntityRegistry();
if (!ownership) {
return null;
}
const { owners } = ownership;
if (!owners || owners.length === 0) {
return null;
}
return (
<AvatarGroupWrapper>
<AvatarsGroup size={24} owners={owners} entityRegistry={entityRegistry} maxCount={4} />
</AvatarGroupWrapper>
);
}

View File

@ -1,135 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { DeleteOutlined, MoreOutlined } from '@ant-design/icons';
import { Col, Dropdown, List, Menu, message, Modal, Row, Tag, Tooltip, Typography } from 'antd';
import { Link } from 'react-router-dom';
import { IconStyleType } from '../entity/Entity';
import { Domain, EntityType } from '../../types.generated';
import { useEntityRegistry } from '../useEntityRegistry';
import AvatarsGroup from '../shared/avatar/AvatarsGroup';
import { getElasticCappedTotalValueText } from '../entity/shared/constants';
import { useDeleteDomainMutation } from '../../graphql/domain.generated';
const DomainItemContainer = styled(Row)`
display: flex;
justify-content: space-between;
padding-left: 8px;
padding-right: 8px;
width: 100%;
`;
const DomainStartContainer = styled(Col)`
display: flex;
justify-content: start;
align-items: center;
`;
const DomainEndContainer = styled(Col)`
display: flex;
justify-content: end;
align-items: center;
`;
const DomainHeaderContainer = styled.div`
display: flex;
justify-content: left;
align-items: center;
`;
const DomainNameContainer = styled.div`
margin-left: 16px;
margin-right: 16px;
`;
const AvatarGroupWrapper = styled.div`
margin-right: 10px;
`;
const MenuIcon = styled(MoreOutlined)<{ fontSize?: number }>`
font-size: ${(props) => props.fontSize || '24'}px;
`;
type Props = {
domain: Domain;
onDelete?: () => void;
};
export default function DomainListItem({ domain, onDelete }: Props) {
const entityRegistry = useEntityRegistry();
const displayName = entityRegistry.getDisplayName(EntityType.Domain, domain);
const logoIcon = entityRegistry.getIcon(EntityType.Domain, 12, IconStyleType.ACCENT);
const owners = domain.ownership?.owners;
const totalEntitiesText = getElasticCappedTotalValueText(domain.entities?.total || 0);
const [deleteDomainMutation] = useDeleteDomainMutation();
const deleteDomain = () => {
deleteDomainMutation({
variables: {
urn: domain.urn,
},
})
.then(({ errors }) => {
if (!errors) {
message.success('Deleted Domain!');
onDelete?.();
}
})
.catch(() => {
message.destroy();
message.error({ content: `Failed to delee Domain!: An unknown error occurred.`, duration: 3 });
});
};
const onConfirmDelete = () => {
Modal.confirm({
title: `Delete Domain '${displayName}'`,
content: `Are you sure you want to remove this ${entityRegistry.getEntityName(EntityType.Domain)}?`,
onOk() {
deleteDomain();
},
onCancel() {},
okText: 'Yes',
maskClosable: true,
closable: true,
});
};
return (
<List.Item data-testid={domain.urn}>
<DomainItemContainer>
<DomainStartContainer>
<Link to={entityRegistry.getEntityUrl(EntityType.Domain, domain.urn)}>
<DomainHeaderContainer>
{logoIcon}
<DomainNameContainer>
<Typography.Text>{displayName}</Typography.Text>
</DomainNameContainer>
<Tooltip title={`There are ${totalEntitiesText} entities in this domain.`}>
<Tag>{totalEntitiesText} entities</Tag>
</Tooltip>
</DomainHeaderContainer>
</Link>
</DomainStartContainer>
<DomainEndContainer>
{owners && owners.length > 0 && (
<AvatarGroupWrapper>
<AvatarsGroup size={24} owners={owners} entityRegistry={entityRegistry} maxCount={4} />
</AvatarGroupWrapper>
)}
<Dropdown
trigger={['click']}
overlay={
<Menu>
<Menu.Item onClick={onConfirmDelete}>
<DeleteOutlined /> &nbsp;Delete
</Menu.Item>
</Menu>
}
>
<MenuIcon fontSize={20} />
</Dropdown>
</DomainEndContainer>
</DomainItemContainer>
</List.Item>
);
}

View File

@ -1,31 +1,28 @@
import React, { useEffect, useState } from 'react';
import { Button, Empty, List, Pagination, Typography } from 'antd';
import { Button, Empty, Pagination, Typography } from 'antd';
import { useLocation } from 'react-router';
import styled from 'styled-components';
import * as QueryString from 'query-string';
import { PlusOutlined } from '@ant-design/icons';
import { Domain } from '../../types.generated';
import { AlignType } from 'rc-table/lib/interface';
import { EntityType } from '../../types.generated';
import { useListDomainsQuery } from '../../graphql/domain.generated';
import CreateDomainModal from './CreateDomainModal';
import { Message } from '../shared/Message';
import TabToolbar from '../entity/shared/components/styled/TabToolbar';
import DomainListItem from './DomainListItem';
import { SearchBar } from '../search/SearchBar';
import { useEntityRegistry } from '../useEntityRegistry';
import { scrollToTop } from '../shared/searchUtils';
import { addToListDomainsCache, removeFromListDomainsCache } from './utils';
import { OnboardingTour } from '../onboarding/OnboardingTour';
import { DOMAINS_INTRO_ID, DOMAINS_CREATE_DOMAIN_ID } from '../onboarding/config/DomainsOnboardingConfig';
import { getElasticCappedTotalValueText } from '../entity/shared/constants';
import { StyledTable } from '../entity/shared/components/styled/StyledTable';
import { IconStyleType } from '../entity/Entity';
import { DomainOwnersColumn, DomainListMenuColumn, DomainNameColumn } from './DomainListColumns';
const DomainsContainer = styled.div``;
const DomainsStyledList = styled(List)`
&&& {
width: 100%;
border-color: ${(props) => props.theme.styles['border-color-base']};
}
`;
const DomainsPaginationContainer = styled.div`
display: flex;
justify-content: center;
@ -85,6 +82,48 @@ export const DomainsList = () => {
}, 2000);
};
const logoIcon = entityRegistry.getIcon(EntityType.Domain, 12, IconStyleType.ACCENT);
const allColumns = [
{
title: 'Name',
dataIndex: '',
key: 'name',
sorter: (sourceA, sourceB) => {
return sourceA.name.localeCompare(sourceB.name);
},
render: DomainNameColumn(logoIcon),
},
{
title: 'Owners',
dataIndex: 'ownership',
width: '10%',
key: 'ownership',
render: DomainOwnersColumn,
},
{
title: '',
dataIndex: '',
width: '5%',
align: 'right' as AlignType,
key: 'menu',
render: DomainListMenuColumn(handleDelete),
},
];
const tableData = domains.map((domain) => {
const displayName = entityRegistry.getDisplayName(EntityType.Domain, domain);
const totalEntitiesText = getElasticCappedTotalValueText(domain.entities?.total || 0);
const url = entityRegistry.getEntityUrl(EntityType.Domain, domain.urn);
return {
name: displayName,
entities: totalEntitiesText,
urn: domain.urn,
ownership: domain.ownership,
url,
};
});
return (
<>
{!data && loading && <Message type="loading" content="Loading domains..." />}
@ -113,19 +152,12 @@ export const DomainsList = () => {
hideRecommendations
/>
</TabToolbar>
<DomainsStyledList
bordered
locale={{
emptyText: <Empty description="No Domains!" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
}}
dataSource={domains}
renderItem={(item: any) => (
<DomainListItem
key={item.urn}
domain={item as Domain}
onDelete={() => handleDelete(item.urn)}
/>
)}
<StyledTable
columns={allColumns}
dataSource={tableData}
rowKey="urn"
pagination={false}
locale={{ emptyText: <Empty description="No Domains!" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
/>
<DomainsPaginationContainer>
<PaginationInfo>