mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-03 06:03:26 +00:00
refactor(ui): Adding apollo caching to manage domains page. (#6494)
This commit is contained in:
parent
ebd685d40d
commit
a400eb0d52
@ -1,5 +1,7 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.domain;
|
||||
|
||||
import com.linkedin.common.AuditStamp;
|
||||
import com.linkedin.common.urn.UrnUtils;
|
||||
import com.linkedin.data.template.SetMode;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
|
||||
@ -65,7 +67,7 @@ public class CreateDomainResolver implements DataFetcher<CompletableFuture<Strin
|
||||
proposal.setEntityKeyAspect(GenericRecordUtils.serializeAspect(key));
|
||||
proposal.setEntityType(Constants.DOMAIN_ENTITY_NAME);
|
||||
proposal.setAspectName(Constants.DOMAIN_PROPERTIES_ASPECT_NAME);
|
||||
proposal.setAspect(GenericRecordUtils.serializeAspect(mapDomainProperties(input)));
|
||||
proposal.setAspect(GenericRecordUtils.serializeAspect(mapDomainProperties(input, context)));
|
||||
proposal.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
String domainUrn = _entityClient.ingestProposal(proposal, context.getAuthentication());
|
||||
@ -78,10 +80,11 @@ public class CreateDomainResolver implements DataFetcher<CompletableFuture<Strin
|
||||
});
|
||||
}
|
||||
|
||||
private DomainProperties mapDomainProperties(final CreateDomainInput input) {
|
||||
private DomainProperties mapDomainProperties(final CreateDomainInput input, final QueryContext context) {
|
||||
final DomainProperties result = new DomainProperties();
|
||||
result.setName(input.getName());
|
||||
result.setDescription(input.getDescription(), SetMode.IGNORE_NULL);
|
||||
result.setCreated(new AuditStamp().setActor(UrnUtils.getUrn(context.getActorUrn())).setTime(System.currentTimeMillis()));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -10,17 +10,19 @@ import com.linkedin.datahub.graphql.generated.ListDomainsInput;
|
||||
import com.linkedin.datahub.graphql.generated.ListDomainsResult;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.query.filter.SortCriterion;
|
||||
import com.linkedin.metadata.query.filter.SortOrder;
|
||||
import com.linkedin.metadata.search.SearchEntity;
|
||||
import com.linkedin.metadata.search.SearchResult;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
|
||||
import static com.linkedin.metadata.Constants.*;
|
||||
|
||||
|
||||
/**
|
||||
@ -56,7 +58,8 @@ public class ListDomainsResolver implements DataFetcher<CompletableFuture<ListDo
|
||||
final SearchResult gmsResult = _entityClient.search(
|
||||
Constants.DOMAIN_ENTITY_NAME,
|
||||
query,
|
||||
Collections.emptyMap(),
|
||||
null,
|
||||
new SortCriterion().setField(DOMAIN_CREATED_TIME_INDEX_FIELD_NAME).setOrder(SortOrder.DESCENDING),
|
||||
start,
|
||||
count,
|
||||
context.getAuthentication());
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.domain;
|
||||
|
||||
import com.linkedin.domain.DomainProperties;
|
||||
import com.linkedin.metadata.utils.GenericRecordUtils;
|
||||
import com.linkedin.mxe.GenericAspect;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
|
||||
|
||||
public class CreateDomainProposalMatcher implements ArgumentMatcher<MetadataChangeProposal> {
|
||||
|
||||
private MetadataChangeProposal left;
|
||||
|
||||
public CreateDomainProposalMatcher(MetadataChangeProposal left) {
|
||||
this.left = left;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(MetadataChangeProposal right) {
|
||||
return left.getEntityType().equals(right.getEntityType())
|
||||
&& left.getAspectName().equals(right.getAspectName())
|
||||
&& left.getChangeType().equals(right.getChangeType())
|
||||
&& domainPropertiesMatch(left.getAspect(), right.getAspect());
|
||||
}
|
||||
|
||||
private boolean domainPropertiesMatch(GenericAspect left, GenericAspect right) {
|
||||
DomainProperties leftProps = GenericRecordUtils.deserializeAspect(
|
||||
left.getValue(),
|
||||
"application/json",
|
||||
DomainProperties.class
|
||||
);
|
||||
|
||||
DomainProperties rightProps = GenericRecordUtils.deserializeAspect(
|
||||
right.getValue(),
|
||||
"application/json",
|
||||
DomainProperties.class
|
||||
);
|
||||
|
||||
// Omit timestamp comparison.
|
||||
return leftProps.getName().equals(rightProps.getName())
|
||||
&& leftProps.getDescription().equals(rightProps.getDescription())
|
||||
&& leftProps.getCreated().getActor().equals(rightProps.getCreated().getActor());
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.domain;
|
||||
|
||||
import com.datahub.authentication.Authentication;
|
||||
import com.linkedin.common.AuditStamp;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.common.urn.UrnUtils;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.CreateDomainInput;
|
||||
import com.linkedin.domain.DomainProperties;
|
||||
@ -28,6 +31,7 @@ public class CreateDomainResolverTest {
|
||||
"test-name",
|
||||
"test-description"
|
||||
);
|
||||
private static final Urn TEST_ACTOR_URN = UrnUtils.getUrn("urn:li:corpuser:test");
|
||||
private static final String TEST_ENTITY_URN = "urn:li:dataset:(urn:li:dataPlatform:mysql,my-test,PROD)";
|
||||
private static final String TEST_TAG_1_URN = "urn:li:tag:test-id-1";
|
||||
private static final String TEST_TAG_2_URN = "urn:li:tag:test-id-2";
|
||||
@ -55,13 +59,14 @@ public class CreateDomainResolverTest {
|
||||
DomainProperties props = new DomainProperties();
|
||||
props.setDescription("test-description");
|
||||
props.setName("test-name");
|
||||
props.setCreated(new AuditStamp().setActor(TEST_ACTOR_URN).setTime(0L));
|
||||
proposal.setAspectName(Constants.DOMAIN_PROPERTIES_ASPECT_NAME);
|
||||
proposal.setAspect(GenericRecordUtils.serializeAspect(props));
|
||||
proposal.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
// Not ideal to match against "any", but we don't know the auto-generated execution request id
|
||||
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
|
||||
Mockito.eq(proposal),
|
||||
Mockito.argThat(new CreateDomainProposalMatcher(proposal)),
|
||||
Mockito.any(Authentication.class)
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,17 +7,19 @@ import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.ListDomainsInput;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.query.filter.SortCriterion;
|
||||
import com.linkedin.metadata.query.filter.SortOrder;
|
||||
import com.linkedin.metadata.search.SearchEntity;
|
||||
import com.linkedin.metadata.search.SearchEntityArray;
|
||||
import com.linkedin.metadata.search.SearchResult;
|
||||
import com.linkedin.r2.RemoteInvocationException;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.*;
|
||||
import static com.linkedin.metadata.Constants.*;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
|
||||
@ -37,7 +39,8 @@ public class ListDomainsResolverTest {
|
||||
Mockito.when(mockClient.search(
|
||||
Mockito.eq(Constants.DOMAIN_ENTITY_NAME),
|
||||
Mockito.eq(""),
|
||||
Mockito.eq(Collections.emptyMap()),
|
||||
Mockito.eq(null),
|
||||
Mockito.eq(new SortCriterion().setField(DOMAIN_CREATED_TIME_INDEX_FIELD_NAME).setOrder(SortOrder.DESCENDING)),
|
||||
Mockito.eq(0),
|
||||
Mockito.eq(20),
|
||||
Mockito.any(Authentication.class)
|
||||
|
||||
@ -18,49 +18,57 @@ const ClickableTag = styled(Tag)`
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onCreate: (id: string | undefined, name: string, description: string) => void;
|
||||
onCreate: (urn: string, id: string | undefined, name: string, description: string) => void;
|
||||
};
|
||||
|
||||
const SUGGESTED_DOMAIN_NAMES = ['Engineering', 'Marketing', 'Sales', 'Product'];
|
||||
|
||||
const ID_FIELD_NAME = 'id';
|
||||
const NAME_FIELD_NAME = 'name';
|
||||
const DESCRIPTION_FIELD_NAME = 'description';
|
||||
|
||||
export default function CreateDomainModal({ onClose, onCreate }: Props) {
|
||||
const [stagedName, setStagedName] = useState('');
|
||||
const [stagedDescription, setStagedDescription] = useState('');
|
||||
const [stagedId, setStagedId] = useState<string | undefined>(undefined);
|
||||
const [createDomainMutation] = useCreateDomainMutation();
|
||||
const [createButtonEnabled, setCreateButtonEnabled] = useState(true);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const setStagedName = (name) => {
|
||||
form.setFieldsValue({
|
||||
name,
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateDomain = () => {
|
||||
createDomainMutation({
|
||||
variables: {
|
||||
input: {
|
||||
id: stagedId,
|
||||
name: stagedName,
|
||||
description: stagedDescription,
|
||||
id: form.getFieldValue(ID_FIELD_NAME),
|
||||
name: form.getFieldValue(NAME_FIELD_NAME),
|
||||
description: form.getFieldValue(DESCRIPTION_FIELD_NAME),
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ errors }) => {
|
||||
.then(({ data, errors }) => {
|
||||
if (!errors) {
|
||||
analytics.event({
|
||||
type: EventType.CreateDomainEvent,
|
||||
});
|
||||
message.success({
|
||||
content: `Created domain!`,
|
||||
duration: 3,
|
||||
});
|
||||
onCreate(
|
||||
data?.createDomain || '',
|
||||
form.getFieldValue(ID_FIELD_NAME),
|
||||
form.getFieldValue(NAME_FIELD_NAME),
|
||||
form.getFieldValue(DESCRIPTION_FIELD_NAME),
|
||||
);
|
||||
form.resetFields();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to create Domain!: \n ${e.message || ''}`, duration: 3 });
|
||||
})
|
||||
.finally(() => {
|
||||
message.success({
|
||||
content: `Created domain!`,
|
||||
duration: 3,
|
||||
});
|
||||
onCreate(stagedId, stagedName, stagedDescription);
|
||||
setStagedName('');
|
||||
setStagedDescription('');
|
||||
setStagedId(undefined);
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
@ -97,7 +105,7 @@ export default function CreateDomainModal({ onClose, onCreate }: Props) {
|
||||
<Form.Item label={<Typography.Text strong>Name</Typography.Text>}>
|
||||
<Typography.Paragraph>Give your new Domain a name. </Typography.Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
name={NAME_FIELD_NAME}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@ -108,15 +116,15 @@ export default function CreateDomainModal({ onClose, onCreate }: Props) {
|
||||
]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
placeholder="A name for your domain"
|
||||
value={stagedName}
|
||||
onChange={(event) => setStagedName(event.target.value)}
|
||||
/>
|
||||
<Input placeholder="A name for your domain" />
|
||||
</Form.Item>
|
||||
<SuggestedNamesGroup>
|
||||
{SUGGESTED_DOMAIN_NAMES.map((name) => {
|
||||
return <ClickableTag onClick={() => setStagedName(name)}>{name}</ClickableTag>;
|
||||
return (
|
||||
<ClickableTag key={name} onClick={() => setStagedName(name)}>
|
||||
{name}
|
||||
</ClickableTag>
|
||||
);
|
||||
})}
|
||||
</SuggestedNamesGroup>
|
||||
</Form.Item>
|
||||
@ -124,12 +132,12 @@ export default function CreateDomainModal({ onClose, onCreate }: Props) {
|
||||
<Typography.Paragraph>
|
||||
An optional description for your new domain. You can change this later.
|
||||
</Typography.Paragraph>
|
||||
<Form.Item name="description" rules={[{ whitespace: true }, { min: 1, max: 500 }]} hasFeedback>
|
||||
<Input
|
||||
placeholder="A description for your domain"
|
||||
value={stagedDescription}
|
||||
onChange={(event) => setStagedDescription(event.target.value)}
|
||||
/>
|
||||
<Form.Item
|
||||
name={DESCRIPTION_FIELD_NAME}
|
||||
rules={[{ whitespace: true }, { min: 1, max: 500 }]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input placeholder="A description for your domain" />
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
<Collapse ghost>
|
||||
@ -142,23 +150,19 @@ export default function CreateDomainModal({ onClose, onCreate }: Props) {
|
||||
creation.
|
||||
</Typography.Paragraph>
|
||||
<Form.Item
|
||||
name="domainId"
|
||||
name={ID_FIELD_NAME}
|
||||
rules={[
|
||||
() => ({
|
||||
validator(_, value) {
|
||||
if (value && groupIdTextValidation(value)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('Please enter correct Domain name'));
|
||||
return Promise.reject(new Error('Please enter a valid Domain id'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="engineering"
|
||||
value={stagedId || ''}
|
||||
onChange={(event) => setStagedId(event.target.value)}
|
||||
/>
|
||||
<Input placeholder="engineering" />
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
</Collapse.Panel>
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Col, List, Row, Tag, Tooltip, Typography } from 'antd';
|
||||
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 EntityDropdown from '../entity/shared/EntityDropdown';
|
||||
import { EntityMenuItems } from '../entity/shared/EntityDropdown/EntityDropdown';
|
||||
import { getElasticCappedTotalValueText } from '../entity/shared/constants';
|
||||
import { useDeleteDomainMutation } from '../../graphql/domain.generated';
|
||||
|
||||
const DomainItemContainer = styled(Row)`
|
||||
display: flex;
|
||||
@ -45,6 +45,10 @@ 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;
|
||||
@ -56,6 +60,39 @@ export default function DomainListItem({ domain, onDelete }: Props) {
|
||||
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}>
|
||||
@ -79,14 +116,18 @@ export default function DomainListItem({ domain, onDelete }: Props) {
|
||||
<AvatarsGroup size={24} owners={owners} entityRegistry={entityRegistry} maxCount={4} />
|
||||
</AvatarGroupWrapper>
|
||||
)}
|
||||
<EntityDropdown
|
||||
urn={domain.urn}
|
||||
entityType={EntityType.Domain}
|
||||
entityData={domain}
|
||||
menuItems={new Set([EntityMenuItems.DELETE])}
|
||||
size={20}
|
||||
onDeleteEntity={onDelete}
|
||||
/>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item onClick={onConfirmDelete}>
|
||||
<DeleteOutlined /> Delete
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<MenuIcon fontSize={20} />
|
||||
</Dropdown>
|
||||
</DomainEndContainer>
|
||||
</DomainItemContainer>
|
||||
</List.Item>
|
||||
|
||||
@ -13,6 +13,7 @@ import DomainListItem from './DomainListItem';
|
||||
import { SearchBar } from '../search/SearchBar';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import { scrollToTop } from '../shared/searchUtils';
|
||||
import { addToListDomainsCache, removeFromListDomainsCache } from './utils';
|
||||
|
||||
const DomainsContainer = styled.div``;
|
||||
|
||||
@ -51,12 +52,11 @@ export const DomainsList = () => {
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [isCreatingDomain, setIsCreatingDomain] = useState(false);
|
||||
const [removedUrns, setRemovedUrns] = useState<string[]>([]);
|
||||
|
||||
const pageSize = DEFAULT_PAGE_SIZE;
|
||||
const start = (page - 1) * pageSize;
|
||||
|
||||
const { loading, error, data, refetch } = useListDomainsQuery({
|
||||
const { loading, error, data, client, refetch } = useListDomainsQuery({
|
||||
variables: {
|
||||
input: {
|
||||
start,
|
||||
@ -64,15 +64,12 @@ export const DomainsList = () => {
|
||||
query,
|
||||
},
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const totalDomains = data?.listDomains?.total || 0;
|
||||
const lastResultIndex = start + pageSize > totalDomains ? totalDomains : start + pageSize;
|
||||
const domains = (data?.listDomains?.domains || []).sort(
|
||||
(a, b) => (b.entities?.total || 0) - (a.entities?.total || 0),
|
||||
);
|
||||
const filteredDomains = domains.filter((domain) => !removedUrns.includes(domain.urn));
|
||||
const domains = data?.listDomains?.domains || [];
|
||||
|
||||
const onChangePage = (newPage: number) => {
|
||||
scrollToTop();
|
||||
@ -80,24 +77,21 @@ export const DomainsList = () => {
|
||||
};
|
||||
|
||||
const handleDelete = (urn: string) => {
|
||||
// Hack to deal with eventual consistency.
|
||||
const newRemovedUrns = [...removedUrns, urn];
|
||||
setRemovedUrns(newRemovedUrns);
|
||||
removeFromListDomainsCache(client, urn, page, pageSize, query);
|
||||
setTimeout(function () {
|
||||
refetch?.();
|
||||
}, 3000);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!data && loading && <Message type="loading" content="Loading domains..." />}
|
||||
{error && <Message type="error" content="Failed to load domains! An unexpected error occurred." />}
|
||||
<DomainsContainer>
|
||||
<TabToolbar>
|
||||
<div>
|
||||
<Button type="text" onClick={() => setIsCreatingDomain(true)}>
|
||||
<PlusOutlined /> New Domain
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="text" onClick={() => setIsCreatingDomain(true)}>
|
||||
<PlusOutlined /> New Domain
|
||||
</Button>
|
||||
<SearchBar
|
||||
initialQuery={query || ''}
|
||||
placeholderText="Search domains..."
|
||||
@ -121,16 +115,20 @@ export const DomainsList = () => {
|
||||
locale={{
|
||||
emptyText: <Empty description="No Domains!" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
}}
|
||||
dataSource={filteredDomains}
|
||||
dataSource={domains}
|
||||
renderItem={(item: any) => (
|
||||
<DomainListItem domain={item as Domain} onDelete={() => handleDelete(item.urn)} />
|
||||
<DomainListItem
|
||||
key={item.urn}
|
||||
domain={item as Domain}
|
||||
onDelete={() => handleDelete(item.urn)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<DomainsPaginationContainer>
|
||||
<PaginationInfo>
|
||||
<b>
|
||||
{lastResultIndex > 0 ? (page - 1) * pageSize + 1 : 0} - {lastResultIndex}
|
||||
</b>{' '}
|
||||
</b>
|
||||
of <b>{totalDomains}</b>
|
||||
</PaginationInfo>
|
||||
<Pagination
|
||||
@ -146,11 +144,22 @@ export const DomainsList = () => {
|
||||
{isCreatingDomain && (
|
||||
<CreateDomainModal
|
||||
onClose={() => setIsCreatingDomain(false)}
|
||||
onCreate={() => {
|
||||
// Hack to deal with eventual consistency.
|
||||
setTimeout(function () {
|
||||
refetch?.();
|
||||
}, 2000);
|
||||
onCreate={(urn, _, name, description) => {
|
||||
addToListDomainsCache(
|
||||
client,
|
||||
{
|
||||
urn,
|
||||
properties: {
|
||||
name,
|
||||
description,
|
||||
},
|
||||
ownership: null,
|
||||
entities: null,
|
||||
},
|
||||
pageSize,
|
||||
query,
|
||||
);
|
||||
setTimeout(() => refetch(), 2000);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
81
datahub-web-react/src/app/domain/utils.ts
Normal file
81
datahub-web-react/src/app/domain/utils.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { ListDomainsDocument, ListDomainsQuery } from '../../graphql/domain.generated';
|
||||
|
||||
/**
|
||||
* Add an entry to the list domains cache.
|
||||
*/
|
||||
export const addToListDomainsCache = (client, newDomain, pageSize, query) => {
|
||||
// Read the data from our cache for this query.
|
||||
const currData: ListDomainsQuery | null = client.readQuery({
|
||||
query: ListDomainsDocument,
|
||||
variables: {
|
||||
input: {
|
||||
start: 0,
|
||||
count: pageSize,
|
||||
query,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Add our new domain into the existing list.
|
||||
const newDomains = [newDomain, ...(currData?.listDomains?.domains || [])];
|
||||
|
||||
// Write our data back to the cache.
|
||||
client.writeQuery({
|
||||
query: ListDomainsDocument,
|
||||
variables: {
|
||||
input: {
|
||||
start: 0,
|
||||
count: pageSize,
|
||||
query,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
listDomains: {
|
||||
start: 0,
|
||||
count: (currData?.listDomains?.count || 0) + 1,
|
||||
total: (currData?.listDomains?.total || 0) + 1,
|
||||
domains: newDomains,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an entry from the list domains cache.
|
||||
*/
|
||||
export const removeFromListDomainsCache = (client, urn, page, pageSize, query) => {
|
||||
// Read the data from our cache for this query.
|
||||
const currData: ListDomainsQuery | null = client.readQuery({
|
||||
query: ListDomainsDocument,
|
||||
variables: {
|
||||
input: {
|
||||
start: (page - 1) * pageSize,
|
||||
count: pageSize,
|
||||
query,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Remove the domain from the existing domain set.
|
||||
const newDomains = [...(currData?.listDomains?.domains || []).filter((domain) => domain.urn !== urn)];
|
||||
|
||||
// Write our data back to the cache.
|
||||
client.writeQuery({
|
||||
query: ListDomainsDocument,
|
||||
variables: {
|
||||
input: {
|
||||
start: (page - 1) * pageSize,
|
||||
count: pageSize,
|
||||
query,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
listDomains: {
|
||||
start: currData?.listDomains?.start || 0,
|
||||
count: (currData?.listDomains?.count || 1) - 1,
|
||||
total: (currData?.listDomains?.total || 1) - 1,
|
||||
domains: newDomains,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -33,7 +33,6 @@ query listDomains($input: ListDomainsInput!) {
|
||||
total
|
||||
domains {
|
||||
urn
|
||||
id
|
||||
properties {
|
||||
name
|
||||
description
|
||||
|
||||
@ -199,6 +199,7 @@ public class Constants {
|
||||
public static final String DOMAIN_KEY_ASPECT_NAME = "domainKey";
|
||||
public static final String DOMAIN_PROPERTIES_ASPECT_NAME = "domainProperties";
|
||||
public static final String DOMAINS_ASPECT_NAME = "domains";
|
||||
public static final String DOMAIN_CREATED_TIME_INDEX_FIELD_NAME = "createdTime";
|
||||
|
||||
// Assertion
|
||||
public static final String ASSERTION_KEY_ASPECT_NAME = "assertionKey";
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
namespace com.linkedin.domain
|
||||
|
||||
import com.linkedin.common.AuditStamp
|
||||
|
||||
/**
|
||||
* Information about a Domain
|
||||
*/
|
||||
@ -23,4 +25,14 @@ record DomainProperties {
|
||||
*/
|
||||
description: optional string
|
||||
|
||||
/**
|
||||
* Created Audit stamp
|
||||
*/
|
||||
@Searchable = {
|
||||
"/time": {
|
||||
"fieldName": "createdTime",
|
||||
"fieldType": "DATETIME"
|
||||
}
|
||||
}
|
||||
created: optional AuditStamp
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user