mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-09 07:42:41 +00:00
* Feat : #5057 UI : Add support for adding a custom field to an entity * Add suport for deeplink * Add unit test for custom entity page component * Add unit test for custom entity detail * Fix : params code styling for get list method * Fetch type details on type select * Fix failing unit test * Fix code styling * Fix #5042: Add a field to an entity API is failing * Add unit test * Add unit test * Style : change customfields table styling. * Add unit test * Add support for deleting custom field * Add unit test for delete field flow * Fix failing unit test * Add operation state * Add support for edit field * Add unit test * Fix no-data row styling * Complete todo items * Add check for if no entity data available * Add unit test * Fix code smell * Fix feed issue for entity `type` * Addressing review comment * Addressing review comments Co-authored-by: Sriharsha Chintalapani <harsha@getcollate.io>
This commit is contained in:
parent
a5055a5585
commit
db0eaa8cbb
@ -36,6 +36,7 @@ module.exports = {
|
||||
'^.+\\.ts|tsx?$': 'ts-jest',
|
||||
'^.+\\.js|jsx?$': '<rootDir>/node_modules/babel-jest',
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/?!(react-markdown)'],
|
||||
|
||||
// "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
|
||||
// "moduleFileExtensions": ["js", "json","jsx" ],
|
||||
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { Category, CustomField } from '../generated/entity/type';
|
||||
import APIClient from './index';
|
||||
|
||||
export const getTypeListByCategory = (
|
||||
category: Category
|
||||
): Promise<AxiosResponse> => {
|
||||
const path = `/metadata/types`;
|
||||
|
||||
const params = { category, limit: '12' };
|
||||
|
||||
return APIClient.get(path, { params });
|
||||
};
|
||||
|
||||
export const getTypeByFQN = (typeFQN: string): Promise<AxiosResponse> => {
|
||||
const path = `/metadata/types/name/${typeFQN}`;
|
||||
|
||||
const params = { fields: 'customFields' };
|
||||
|
||||
return APIClient.get(path, { params });
|
||||
};
|
||||
|
||||
export const addFieldToEntity = (
|
||||
entityTypeId: string,
|
||||
data: CustomField
|
||||
): Promise<AxiosResponse> => {
|
||||
const path = `/metadata/types/${entityTypeId}`;
|
||||
|
||||
return APIClient.put(path, data);
|
||||
};
|
||||
|
||||
export const updateType = (
|
||||
entityTypeId: string,
|
||||
data: Operation[]
|
||||
): Promise<AxiosResponse> => {
|
||||
const configOptions = {
|
||||
headers: { 'Content-type': 'application/json-patch+json' },
|
||||
};
|
||||
const path = `/metadata/types/${entityTypeId}`;
|
||||
|
||||
return APIClient.patch(path, data, configOptions);
|
||||
};
|
@ -181,6 +181,7 @@ const FeedCardHeader: FC<FeedHeaderProp> = ({
|
||||
EntityType.DASHBOARD_SERVICE,
|
||||
EntityType.MESSAGING_SERVICE,
|
||||
EntityType.PIPELINE_SERVICE,
|
||||
EntityType.TYPE,
|
||||
].includes(entityType as EntityType)
|
||||
) {
|
||||
displayName = getPartialNameFromFQN(entityFQN, ['service']);
|
||||
@ -207,6 +208,7 @@ const FeedCardHeader: FC<FeedHeaderProp> = ({
|
||||
EntityType.WEBHOOK,
|
||||
EntityType.GLOSSARY,
|
||||
EntityType.GLOSSARY_TERM,
|
||||
EntityType.TYPE,
|
||||
];
|
||||
|
||||
const entityLink = getEntityLink(entityType, entityFQN);
|
||||
|
@ -0,0 +1,309 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { findByTestId, fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { addFieldToEntity } from '../../../axiosAPIs/metadataTypeAPI';
|
||||
import AddCustomField from './AddCustomField';
|
||||
|
||||
const mockFieldTypes = [
|
||||
{
|
||||
id: '153a0c07-6480-404e-990b-555a42c8a7b5',
|
||||
name: 'date',
|
||||
fullyQualifiedName: 'date',
|
||||
displayName: 'date',
|
||||
description: '"Date in ISO 8601 format in UTC. Example - \'2018-11-13\'."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/153a0c07-6480-404e-990b-555a42c8a7b5',
|
||||
},
|
||||
{
|
||||
id: '6ce245d8-80c0-4641-9b60-32cf03ca79a2',
|
||||
name: 'dateTime',
|
||||
fullyQualifiedName: 'dateTime',
|
||||
displayName: 'dateTime',
|
||||
description:
|
||||
'"Date and time in ISO 8601 format. Example - \'2018-11-13T20:20:39+00:00\'."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/6ce245d8-80c0-4641-9b60-32cf03ca79a2',
|
||||
},
|
||||
{
|
||||
id: 'f5b7d80a-8429-4918-b092-548714ba5a0d',
|
||||
name: 'duration',
|
||||
fullyQualifiedName: 'duration',
|
||||
displayName: 'duration',
|
||||
description:
|
||||
'"Duration in ISO 8601 format in UTC. Example - \'P23DT23H\'."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/f5b7d80a-8429-4918-b092-548714ba5a0d',
|
||||
},
|
||||
{
|
||||
id: 'cb405660-95ea-4de5-a5a9-d484b612f33d',
|
||||
name: 'email',
|
||||
fullyQualifiedName: 'email',
|
||||
displayName: 'email',
|
||||
description: '"Email address of a user or other entities."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/cb405660-95ea-4de5-a5a9-d484b612f33d',
|
||||
},
|
||||
{
|
||||
id: 'be5f2241-8915-4f93-810a-d3c56fe43f29',
|
||||
name: 'integer',
|
||||
fullyQualifiedName: 'integer',
|
||||
displayName: 'integer',
|
||||
description: '"An integer type."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/be5f2241-8915-4f93-810a-d3c56fe43f29',
|
||||
},
|
||||
{
|
||||
id: '080d393a-7520-44cf-989d-14430668bc97',
|
||||
name: 'markdown',
|
||||
fullyQualifiedName: 'markdown',
|
||||
displayName: 'markdown',
|
||||
description: '"Text in Markdown format"',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/080d393a-7520-44cf-989d-14430668bc97',
|
||||
},
|
||||
{
|
||||
id: '7057cd7c-710b-4a8f-b14a-1950adf87cc0',
|
||||
name: 'number',
|
||||
fullyQualifiedName: 'number',
|
||||
displayName: 'number',
|
||||
description:
|
||||
'"A numeric type that includes integer or floating point numbers."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/7057cd7c-710b-4a8f-b14a-1950adf87cc0',
|
||||
},
|
||||
{
|
||||
id: '149f852f-c8b2-4581-84bd-e1d492836009',
|
||||
name: 'sqlQuery',
|
||||
fullyQualifiedName: 'sqlQuery',
|
||||
displayName: 'sqlQuery',
|
||||
description: '"SQL query statement. Example - \'select * from orders\'."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/149f852f-c8b2-4581-84bd-e1d492836009',
|
||||
},
|
||||
{
|
||||
id: '05e7b2f2-cf1e-4f9f-ae8b-3011372f361e',
|
||||
name: 'string',
|
||||
fullyQualifiedName: 'string',
|
||||
displayName: 'string',
|
||||
description: '"A String type."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/05e7b2f2-cf1e-4f9f-ae8b-3011372f361e',
|
||||
},
|
||||
{
|
||||
id: '5db5e3ef-b4f5-41a7-a512-8d10409d9b63',
|
||||
name: 'timeInterval',
|
||||
fullyQualifiedName: 'timeInterval',
|
||||
displayName: 'timeInterval',
|
||||
description: '"Time interval in unixTimeMillis."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/5db5e3ef-b4f5-41a7-a512-8d10409d9b63',
|
||||
},
|
||||
{
|
||||
id: '4ae124a9-c799-42cc-8bd4-048362b4b4e6',
|
||||
name: 'timestamp',
|
||||
fullyQualifiedName: 'timestamp',
|
||||
displayName: 'timestamp',
|
||||
description: '"Timestamp in Unix epoch time milliseconds."',
|
||||
category: 'field',
|
||||
nameSpace: 'basic',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653976591924,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/4ae124a9-c799-42cc-8bd4-048362b4b4e6',
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: jest.fn(),
|
||||
useParams: jest.fn().mockReturnValue({
|
||||
entityTypeFQN: 'entityTypeFQN',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../../axiosAPIs/metadataTypeAPI', () => ({
|
||||
addFieldToEntity: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ data: mockFieldTypes[0] })),
|
||||
getTypeByFQN: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ data: mockFieldTypes[0] })),
|
||||
getTypeListByCategory: jest
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ data: { data: mockFieldTypes } })
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/CommonUtils', () => ({
|
||||
errorMsg: jest.fn(),
|
||||
requiredField: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/ToastUtils', () => ({
|
||||
showErrorToast: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../common/rich-text-editor/RichTextEditor', () => {
|
||||
return jest
|
||||
.fn()
|
||||
.mockReturnValue(<div data-testid="richtext-editor">RichTextEditor</div>);
|
||||
});
|
||||
|
||||
jest.mock('../../containers/PageContainer', () => {
|
||||
return jest
|
||||
.fn()
|
||||
.mockImplementation(({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
));
|
||||
});
|
||||
|
||||
jest.mock(
|
||||
'../../containers/PageLayout',
|
||||
() =>
|
||||
({
|
||||
children,
|
||||
rightPanel,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
rightPanel: React.ReactNode;
|
||||
}) =>
|
||||
(
|
||||
<div data-testid="PageLayout">
|
||||
<div data-testid="right-panel-content">{rightPanel}</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
describe('Test Add Custom Field Component', () => {
|
||||
it('Should render component', async () => {
|
||||
const { container } = render(<AddCustomField />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
const rightPanel = await findByTestId(container, 'right-panel-content');
|
||||
|
||||
expect(rightPanel).toBeInTheDocument();
|
||||
|
||||
const formContainer = await findByTestId(container, 'form-container');
|
||||
|
||||
expect(formContainer).toBeInTheDocument();
|
||||
|
||||
const nameField = await findByTestId(container, 'name');
|
||||
|
||||
const typeField = await findByTestId(container, 'type');
|
||||
|
||||
const descriptionField = await findByTestId(container, 'richtext-editor');
|
||||
|
||||
const backButton = await findByTestId(container, 'cancel-custom-field');
|
||||
|
||||
const createButton = await findByTestId(container, 'create-custom-field');
|
||||
|
||||
expect(nameField).toBeInTheDocument();
|
||||
expect(typeField).toBeInTheDocument();
|
||||
expect(descriptionField).toBeInTheDocument();
|
||||
|
||||
expect(backButton).toBeInTheDocument();
|
||||
expect(createButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test create field flow', async () => {
|
||||
const { container } = render(<AddCustomField />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
const formContainer = await findByTestId(container, 'form-container');
|
||||
|
||||
expect(formContainer).toBeInTheDocument();
|
||||
|
||||
const nameField = await findByTestId(container, 'name');
|
||||
|
||||
const typeField = await findByTestId(container, 'type');
|
||||
|
||||
const descriptionField = await findByTestId(container, 'richtext-editor');
|
||||
|
||||
const backButton = await findByTestId(container, 'cancel-custom-field');
|
||||
|
||||
const createButton = await findByTestId(container, 'create-custom-field');
|
||||
|
||||
expect(nameField).toBeInTheDocument();
|
||||
expect(typeField).toBeInTheDocument();
|
||||
expect(descriptionField).toBeInTheDocument();
|
||||
|
||||
expect(backButton).toBeInTheDocument();
|
||||
expect(createButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(nameField, { target: { value: 'updatedBy' } });
|
||||
fireEvent.change(typeField, {
|
||||
target: { value: '05e7b2f2-cf1e-4f9f-ae8b-3011372f361e' },
|
||||
});
|
||||
|
||||
fireEvent.click(createButton);
|
||||
|
||||
expect(addFieldToEntity).toBeCalled();
|
||||
});
|
||||
});
|
@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { EditorContentRef, FormErrorData } from 'Models';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import {
|
||||
addFieldToEntity,
|
||||
getTypeByFQN,
|
||||
getTypeListByCategory,
|
||||
} from '../../../axiosAPIs/metadataTypeAPI';
|
||||
import { Category, Type } from '../../../generated/entity/type';
|
||||
import { errorMsg, requiredField } from '../../../utils/CommonUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import { Button } from '../../buttons/Button/Button';
|
||||
import RichTextEditor from '../../common/rich-text-editor/RichTextEditor';
|
||||
import PageContainer from '../../containers/PageContainer';
|
||||
import PageLayout from '../../containers/PageLayout';
|
||||
import { Field } from '../../Field/Field';
|
||||
import { RightPanel } from './RightPanel';
|
||||
|
||||
const InitialFormData = {
|
||||
name: '',
|
||||
type: '',
|
||||
};
|
||||
|
||||
const AddCustomField = () => {
|
||||
const { entityTypeFQN } = useParams<{ [key: string]: string }>();
|
||||
const history = useHistory();
|
||||
const markdownRef = useRef<EditorContentRef>();
|
||||
|
||||
const [typeDetail, setTypeDetail] = useState<Type>({} as Type);
|
||||
|
||||
const [fieldTypes, setFieldTypes] = useState<Array<Type>>([]);
|
||||
|
||||
const [formData, setFormData] =
|
||||
useState<Record<string, string>>(InitialFormData);
|
||||
|
||||
const [formErrorData, setFormErrorData] = useState<FormErrorData>(
|
||||
{} as FormErrorData
|
||||
);
|
||||
|
||||
const getDescription = () => markdownRef.current?.getEditorContent() || '';
|
||||
|
||||
const fetchFieldType = () => {
|
||||
getTypeListByCategory(Category.Field)
|
||||
.then((res: AxiosResponse) => {
|
||||
setFieldTypes(res.data.data);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(err);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchTypeDetail = (typeFQN: string) => {
|
||||
getTypeByFQN(typeFQN)
|
||||
.then((res: AxiosResponse) => {
|
||||
setTypeDetail(res.data);
|
||||
})
|
||||
.catch((err: AxiosError) => showErrorToast(err));
|
||||
};
|
||||
|
||||
const validateName = (name: string) => {
|
||||
const nameRegEx = /^[a-z][a-zA-Z0-9]+$/;
|
||||
|
||||
return nameRegEx.test(name);
|
||||
};
|
||||
|
||||
const validateType = (type: string) => {
|
||||
return Boolean(type);
|
||||
};
|
||||
|
||||
const handleError = (flag: boolean, field: string) => {
|
||||
const message =
|
||||
field === 'name' ? 'Invalid Field Name' : 'Type is required';
|
||||
|
||||
setFormErrorData((preVdata) => ({
|
||||
...preVdata,
|
||||
[field]: !flag ? message : '',
|
||||
}));
|
||||
};
|
||||
|
||||
const onChangeHandler = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
switch (name) {
|
||||
case 'name': {
|
||||
const newData = { ...formData, name: value };
|
||||
const isValidName = validateName(value);
|
||||
handleError(isValidName, 'name');
|
||||
setFormData(newData);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'type': {
|
||||
const newData = { ...formData, type: value };
|
||||
const isValidType = validateType(value);
|
||||
handleError(isValidType, 'type');
|
||||
setFormData(newData);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
const isValidName = validateName(formData.name);
|
||||
const isValidType = validateType(formData.type);
|
||||
if (isValidName && isValidType) {
|
||||
const fieldData = {
|
||||
description: getDescription(),
|
||||
name: formData.name,
|
||||
fieldType: {
|
||||
id: formData.type,
|
||||
type: 'type',
|
||||
},
|
||||
};
|
||||
addFieldToEntity(typeDetail.id as string, fieldData)
|
||||
.then(() => {
|
||||
history.goBack();
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
showErrorToast(err);
|
||||
});
|
||||
} else {
|
||||
handleError(isValidName, 'name');
|
||||
handleError(isValidType, 'type');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTypeDetail(entityTypeFQN);
|
||||
}, [entityTypeFQN]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFieldType();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageLayout
|
||||
classes="tw-max-w-full-hd tw-h-full tw-pt-4"
|
||||
rightPanel={<RightPanel />}>
|
||||
<div
|
||||
className="tw-bg-white tw-p-4 tw-border tw-border-main tw-rounded"
|
||||
data-testid="form-container">
|
||||
<h6 className="tw-heading tw-text-base">Add Custom Field</h6>
|
||||
|
||||
<Field>
|
||||
<label className="tw-block tw-form-label" htmlFor="name">
|
||||
{requiredField('Name:')}
|
||||
</label>
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="tw-form-inputs tw-form-inputs-padding"
|
||||
data-testid="name"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
{formErrorData?.name && errorMsg(formErrorData.name)}
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<label className="tw-block tw-form-label" htmlFor="type">
|
||||
{requiredField('Type:')}
|
||||
</label>
|
||||
<select
|
||||
className="tw-form-inputs tw-form-inputs-padding"
|
||||
data-testid="type"
|
||||
id="type"
|
||||
name="type"
|
||||
placeholder="type"
|
||||
value={formData.type || ''}
|
||||
onChange={onChangeHandler}>
|
||||
<option value="">Select type</option>
|
||||
{fieldTypes.map((fieldType) => (
|
||||
<option key={uniqueId()} value={fieldType.id}>
|
||||
{fieldType.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{formErrorData?.type && errorMsg(formErrorData.type)}
|
||||
</Field>
|
||||
<Field>
|
||||
<label
|
||||
className="tw-block tw-form-label tw-mb-0"
|
||||
htmlFor="description">
|
||||
Description:
|
||||
</label>
|
||||
<RichTextEditor
|
||||
data-testid="description"
|
||||
initialValue=""
|
||||
ref={markdownRef}
|
||||
/>
|
||||
</Field>
|
||||
<Field className="tw-flex tw-justify-end">
|
||||
<Button
|
||||
data-testid="cancel-custom-field"
|
||||
size="regular"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
onClick={onCancel}>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="tw-px-3 tw-rounded"
|
||||
data-testid="create-custom-field"
|
||||
size="custom"
|
||||
theme="primary"
|
||||
type="submit"
|
||||
onClick={onSave}>
|
||||
Create
|
||||
</Button>
|
||||
</Field>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCustomField;
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { RightPanel } from './RightPanel';
|
||||
|
||||
describe('Test Add custom field right panel component', () => {
|
||||
it('Should render right panel component', async () => {
|
||||
const { findByTestId } = render(<RightPanel />);
|
||||
|
||||
const panelHeader = await findByTestId('header');
|
||||
|
||||
const panelBody = await findByTestId('body');
|
||||
|
||||
expect(panelHeader).toBeInTheDocument();
|
||||
|
||||
expect(panelBody).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
export const RightPanel = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<h6 className="tw-heading tw-text-base" data-testid="header">
|
||||
Add a Custom Field
|
||||
</h6>
|
||||
<div className="tw-mb-5" data-testid="body">
|
||||
OpenMetadata supports custom fields in the Table entity. Create a custom
|
||||
field by adding a unique field name. The name must start with a
|
||||
lowercase letter, as preferred in the camelCase format. Uppercase
|
||||
letters and numbers can be included in the field name; but spaces,
|
||||
underscores, and dots are not supported. Select the preferred field Type
|
||||
from among the options provided. Describe your custom field to provide
|
||||
more information to your team.
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Type } from '../../generated/entity/type';
|
||||
import { Tab } from '../common/TabsPane/TabsPane';
|
||||
import CustomEntityDetail from './CustomEntityDetail';
|
||||
|
||||
const mockData = {
|
||||
id: '32f81349-d7d7-4a6a-8fc7-d767f233b674',
|
||||
name: 'table',
|
||||
fullyQualifiedName: 'table',
|
||||
displayName: 'table',
|
||||
description:
|
||||
// eslint-disable-next-line max-len
|
||||
'"This schema defines the Table entity. A Table organizes data in rows and columns and is defined by a Schema. OpenMetadata does not have a separate abstraction for Schema. Both Table and Schema are captured in this entity."',
|
||||
category: 'entity',
|
||||
nameSpace: 'data',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653626359971,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/32f81349-d7d7-4a6a-8fc7-d767f233b674',
|
||||
} as Type;
|
||||
|
||||
const mockPush = jest.fn();
|
||||
|
||||
const MOCK_HISTORY = {
|
||||
push: mockPush,
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: jest.fn().mockImplementation(() => MOCK_HISTORY),
|
||||
}));
|
||||
|
||||
jest.mock('../../axiosAPIs/metadataTypeAPI', () => ({
|
||||
getTypeByFQN: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ data: mockData })),
|
||||
}));
|
||||
|
||||
jest.mock('../containers/PageContainer', () => {
|
||||
return jest
|
||||
.fn()
|
||||
.mockImplementation(({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
));
|
||||
});
|
||||
|
||||
jest.mock('../../constants/constants', () => ({
|
||||
getAddCustomFieldPath: jest.fn().mockReturnValue('/custom-entity/table'),
|
||||
}));
|
||||
|
||||
jest.mock('../common/TabsPane/TabsPane', () =>
|
||||
jest.fn().mockImplementation(({ setActiveTab, tabs }) => {
|
||||
return (
|
||||
<div>
|
||||
<nav
|
||||
className="tw-flex tw-items-center tw-justify-between tw-gh-tabs-container tw-px-7"
|
||||
data-testid="tabs"
|
||||
id="tabs">
|
||||
{tabs.map((tab: Tab) => (
|
||||
<button
|
||||
data-testid={tab.name}
|
||||
key={tab.position}
|
||||
onClick={() => setActiveTab?.(tab.position)}>
|
||||
{tab.name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('../schema-editor/SchemaEditor', () =>
|
||||
jest
|
||||
.fn()
|
||||
.mockReturnValue(<div data-testid="schema-editor">Schema Editor</div>)
|
||||
);
|
||||
|
||||
jest.mock('./CustomFieldTable', () => ({
|
||||
CustomFieldTable: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
<div data-testid="CustomFieldTable">CustomFieldTable</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('./LeftPanel', () => ({
|
||||
LeftPanel: jest
|
||||
.fn()
|
||||
.mockReturnValue(<div data-testid="LeftPanel">LeftPanel</div>),
|
||||
}));
|
||||
|
||||
describe('Test Custom Entity Detail Component', () => {
|
||||
it('Should render custom entity component', async () => {
|
||||
const { findByTestId } = render(
|
||||
<CustomEntityDetail entityTypes={[mockData]} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
|
||||
const leftPanel = await findByTestId('LeftPanel');
|
||||
const tabContainer = await findByTestId('tabs');
|
||||
const schema = await findByTestId('schema-editor');
|
||||
|
||||
const schemTab = await findByTestId('Schema');
|
||||
const customFieldTab = await findByTestId('Custom Fields');
|
||||
|
||||
expect(leftPanel).toBeInTheDocument();
|
||||
expect(tabContainer).toBeInTheDocument();
|
||||
expect(schema).toBeInTheDocument();
|
||||
expect(schemTab).toBeInTheDocument();
|
||||
expect(customFieldTab).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render custom fields table if active tab is Custom Fields', async () => {
|
||||
const { findByTestId } = render(
|
||||
<CustomEntityDetail entityTypes={[mockData]} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
|
||||
const leftPanel = await findByTestId('LeftPanel');
|
||||
const tabContainer = await findByTestId('tabs');
|
||||
|
||||
expect(leftPanel).toBeInTheDocument();
|
||||
expect(tabContainer).toBeInTheDocument();
|
||||
|
||||
const customFieldTab = await findByTestId('Custom Fields');
|
||||
|
||||
expect(customFieldTab).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(customFieldTab);
|
||||
|
||||
expect(await findByTestId('CustomFieldTable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should call history.push method on click of Add field button', async () => {
|
||||
const { findByTestId } = render(
|
||||
<CustomEntityDetail entityTypes={[mockData]} />,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
|
||||
const tabContainer = await findByTestId('tabs');
|
||||
|
||||
expect(tabContainer).toBeInTheDocument();
|
||||
|
||||
const customFieldTab = await findByTestId('Custom Fields');
|
||||
|
||||
expect(customFieldTab).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(customFieldTab);
|
||||
|
||||
expect(await findByTestId('CustomFieldTable')).toBeInTheDocument();
|
||||
|
||||
const addFieldButton = await findByTestId('add-field-button');
|
||||
|
||||
expect(addFieldButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(addFieldButton);
|
||||
|
||||
expect(mockPush).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getTypeByFQN, updateType } from '../../axiosAPIs/metadataTypeAPI';
|
||||
import { getAddCustomFieldPath } from '../../constants/constants';
|
||||
import { Type } from '../../generated/entity/type';
|
||||
import jsonData from '../../jsons/en';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import { Button } from '../buttons/Button/Button';
|
||||
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
|
||||
import TabsPane from '../common/TabsPane/TabsPane';
|
||||
import PageContainer from '../containers/PageContainer';
|
||||
import PageLayout from '../containers/PageLayout';
|
||||
import SchemaEditor from '../schema-editor/SchemaEditor';
|
||||
import { CustomFieldTable } from './CustomFieldTable';
|
||||
import { LeftPanel } from './LeftPanel';
|
||||
|
||||
interface Props {
|
||||
entityTypes: Array<Type>;
|
||||
entityTypeFQN?: string;
|
||||
}
|
||||
|
||||
const CustomEntityDetail: FC<Props> = ({ entityTypes, entityTypeFQN }) => {
|
||||
const history = useHistory();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const [selectedEntityType, setSelectedEntityType] = useState<Type>(
|
||||
{} as Type
|
||||
);
|
||||
const [selectedEntityTypeDetail, setSelectedEntityTypeDetail] =
|
||||
useState<Type>({} as Type);
|
||||
|
||||
const fetchTypeDetail = (typeFQN: string) => {
|
||||
getTypeByFQN(typeFQN)
|
||||
.then((res: AxiosResponse) => {
|
||||
setSelectedEntityTypeDetail(res.data);
|
||||
})
|
||||
.catch((err: AxiosError) => showErrorToast(err));
|
||||
};
|
||||
|
||||
const onTabChange = (tab: number) => {
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
const onEntityTypeSelect = (entityType: Type) => {
|
||||
setSelectedEntityType(entityType);
|
||||
};
|
||||
|
||||
const handleAddField = () => {
|
||||
const path = getAddCustomFieldPath(
|
||||
selectedEntityTypeDetail.fullyQualifiedName as string
|
||||
);
|
||||
history.push(path);
|
||||
};
|
||||
|
||||
const schemaCheck = activeTab === 1 && !isEmpty(selectedEntityTypeDetail);
|
||||
const schemaValue = selectedEntityTypeDetail.schema || '{}';
|
||||
|
||||
const customFieldsCheck =
|
||||
activeTab === 2 && !isEmpty(selectedEntityTypeDetail);
|
||||
const customFields = selectedEntityTypeDetail.customFields || [];
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: 'Schema',
|
||||
isProtected: false,
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
name: 'Custom Fields',
|
||||
isProtected: false,
|
||||
position: 2,
|
||||
count: customFields.length,
|
||||
},
|
||||
];
|
||||
|
||||
const componentCheck = Boolean(entityTypes.length);
|
||||
|
||||
const updateEntityType = (fields: Type['customFields']) => {
|
||||
const patch = compare(selectedEntityTypeDetail, {
|
||||
...selectedEntityTypeDetail,
|
||||
customFields: fields,
|
||||
});
|
||||
|
||||
updateType(selectedEntityTypeDetail.id as string, patch)
|
||||
.then((res: AxiosResponse) => {
|
||||
const { customFields: Fields } = res.data;
|
||||
|
||||
setSelectedEntityTypeDetail((prev) => ({
|
||||
...prev,
|
||||
customFields: Fields,
|
||||
}));
|
||||
})
|
||||
.catch((err: AxiosError) => showErrorToast(err));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (entityTypes.length) {
|
||||
const entityType =
|
||||
entityTypes.find((type) => type.fullyQualifiedName === entityTypeFQN) ||
|
||||
entityTypes[0];
|
||||
onEntityTypeSelect(entityType);
|
||||
}
|
||||
}, [entityTypes, entityTypeFQN]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(selectedEntityType)) {
|
||||
fetchTypeDetail(selectedEntityType.fullyQualifiedName as string);
|
||||
}
|
||||
}, [selectedEntityType]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{componentCheck ? (
|
||||
<PageLayout
|
||||
leftPanel={
|
||||
<LeftPanel
|
||||
selectedType={selectedEntityTypeDetail}
|
||||
typeList={entityTypes}
|
||||
/>
|
||||
}>
|
||||
<TabsPane
|
||||
activeTab={activeTab}
|
||||
setActiveTab={onTabChange}
|
||||
tabs={tabs}
|
||||
/>
|
||||
<div className="tw-mt-6">
|
||||
{schemaCheck && (
|
||||
<div data-testid="entity-schema">
|
||||
<SchemaEditor
|
||||
className="tw-border tw-border-main tw-rounded-md tw-py-4"
|
||||
editorClass="custom-entity-schema"
|
||||
value={JSON.parse(schemaValue)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{customFieldsCheck && (
|
||||
<div data-testid="entity-custom-fields">
|
||||
<div className="tw-flex tw-justify-end">
|
||||
<Button
|
||||
className="tw-mb-4 tw-py-1 tw-px-2 tw-rounded"
|
||||
data-testid="add-field-button"
|
||||
size="custom"
|
||||
theme="primary"
|
||||
onClick={() => handleAddField()}>
|
||||
Add Field
|
||||
</Button>
|
||||
</div>
|
||||
<CustomFieldTable
|
||||
customFields={customFields}
|
||||
updateEntityType={updateEntityType}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageLayout>
|
||||
) : (
|
||||
<ErrorPlaceHolder>
|
||||
{jsonData['message']['no-custom-entity']}
|
||||
</ErrorPlaceHolder>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEntityDetail;
|
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
findAllByTestId,
|
||||
findByTestId,
|
||||
fireEvent,
|
||||
render,
|
||||
} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { CustomFieldTable } from './CustomFieldTable';
|
||||
|
||||
jest.mock('../../utils/CommonUtils', () => ({
|
||||
getEntityName: jest.fn().mockReturnValue('entityName'),
|
||||
isEven: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
|
||||
return jest.fn().mockReturnValue(<p>RichTextEditorPreview</p>);
|
||||
});
|
||||
|
||||
const mockUpdateEntityType = jest.fn();
|
||||
const mockFields = [
|
||||
{
|
||||
name: 'tableCreatedBy',
|
||||
description: 'To track of who created the table.',
|
||||
fieldType: {
|
||||
id: '1815eba0-a7e7-4880-8af5-8eee8710d279',
|
||||
type: 'type',
|
||||
name: 'string',
|
||||
fullyQualifiedName: 'string',
|
||||
description: '"A String type."',
|
||||
displayName: 'string',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/1815eba0-a7e7-4880-8af5-8eee8710d279',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tableUpdatedBy',
|
||||
description: 'To track who updated the table.',
|
||||
fieldType: {
|
||||
id: '1815eba0-a7e7-4880-8af5-8eee8710d279',
|
||||
type: 'type',
|
||||
name: 'string',
|
||||
fullyQualifiedName: 'string',
|
||||
description: '"A String type."',
|
||||
displayName: 'string',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/1815eba0-a7e7-4880-8af5-8eee8710d279',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockProp = {
|
||||
customFields: mockFields,
|
||||
updateEntityType: mockUpdateEntityType,
|
||||
};
|
||||
|
||||
describe('Test CustomField Table Component', () => {
|
||||
it('Should render table component', async () => {
|
||||
const { findByTestId, findAllByTestId } = render(
|
||||
<CustomFieldTable {...mockProp} />
|
||||
);
|
||||
|
||||
const table = await findByTestId('entity-custom-fields-table');
|
||||
|
||||
expect(table).toBeInTheDocument();
|
||||
|
||||
const tableHeader = await findByTestId('table-header');
|
||||
|
||||
const tableBody = await findByTestId('table-body');
|
||||
|
||||
expect(tableHeader).toBeInTheDocument();
|
||||
|
||||
expect(tableBody).toBeInTheDocument();
|
||||
|
||||
const dataRows = await findAllByTestId('data-row');
|
||||
|
||||
expect(dataRows).toHaveLength(mockFields.length);
|
||||
});
|
||||
|
||||
it('Test delete field flow', async () => {
|
||||
const { container } = render(<CustomFieldTable {...mockProp} />);
|
||||
|
||||
const table = await findByTestId(container, 'entity-custom-fields-table');
|
||||
|
||||
expect(table).toBeInTheDocument();
|
||||
|
||||
const tableHeader = await findByTestId(container, 'table-header');
|
||||
|
||||
const tableBody = await findByTestId(container, 'table-body');
|
||||
|
||||
expect(tableHeader).toBeInTheDocument();
|
||||
|
||||
expect(tableBody).toBeInTheDocument();
|
||||
|
||||
const dataRows = await findAllByTestId(container, 'data-row');
|
||||
|
||||
expect(dataRows).toHaveLength(mockFields.length);
|
||||
|
||||
const dataRow = dataRows[0];
|
||||
|
||||
const deleteButton = await findByTestId(dataRow, 'delete-button');
|
||||
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
// confirmation modal should be visible on click of delete button
|
||||
const confirmationModal = await findByTestId(
|
||||
container,
|
||||
'confirmation-modal'
|
||||
);
|
||||
|
||||
expect(confirmationModal).toBeInTheDocument();
|
||||
|
||||
const confirmButton = await findByTestId(confirmationModal, 'save-button');
|
||||
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
// update type callback should get called on click of confirm button
|
||||
expect(mockUpdateEntityType).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should render no data row if there is no custom fields', async () => {
|
||||
const { findByTestId, queryAllByTestId } = render(
|
||||
<CustomFieldTable {...mockProp} customFields={[]} />
|
||||
);
|
||||
|
||||
const table = await findByTestId('entity-custom-fields-table');
|
||||
|
||||
expect(table).toBeInTheDocument();
|
||||
|
||||
const tableHeader = await findByTestId('table-header');
|
||||
|
||||
const tableBody = await findByTestId('table-body');
|
||||
|
||||
expect(tableHeader).toBeInTheDocument();
|
||||
|
||||
expect(tableBody).toBeInTheDocument();
|
||||
|
||||
const dataRows = queryAllByTestId('data-row');
|
||||
|
||||
expect(dataRows).toHaveLength(0);
|
||||
|
||||
const noDataRow = await findByTestId('no-data-row');
|
||||
|
||||
expect(noDataRow).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { isEmpty, uniqueId } from 'lodash';
|
||||
import React, { FC, Fragment, useState } from 'react';
|
||||
import { CustomField, Type } from '../../generated/entity/type';
|
||||
import { getEntityName, isEven } from '../../utils/CommonUtils';
|
||||
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
||||
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
|
||||
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
|
||||
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||
|
||||
interface CustomFieldTableProp {
|
||||
customFields: CustomField[];
|
||||
updateEntityType: (customFields: Type['customFields']) => void;
|
||||
}
|
||||
|
||||
type Operation = 'delete' | 'update' | 'no-operation';
|
||||
|
||||
export const CustomFieldTable: FC<CustomFieldTableProp> = ({
|
||||
customFields,
|
||||
updateEntityType,
|
||||
}) => {
|
||||
const [selectedField, setSelectedField] = useState<CustomField>(
|
||||
{} as CustomField
|
||||
);
|
||||
|
||||
const [operation, setOperation] = useState<Operation>('no-operation');
|
||||
|
||||
const resetSelectedField = () => {
|
||||
setSelectedField({} as CustomField);
|
||||
setOperation('no-operation' as Operation);
|
||||
};
|
||||
|
||||
const handleFieldDelete = () => {
|
||||
const updatedFields = customFields.filter(
|
||||
(field) => field.name !== selectedField.name
|
||||
);
|
||||
updateEntityType(updatedFields);
|
||||
resetSelectedField();
|
||||
};
|
||||
|
||||
const handleFieldUpdate = (updatedDescription: string) => {
|
||||
const updatedFields = customFields.map((field) => {
|
||||
if (field.name === selectedField.name) {
|
||||
return { ...field, description: updatedDescription };
|
||||
} else {
|
||||
return field;
|
||||
}
|
||||
});
|
||||
updateEntityType(updatedFields);
|
||||
resetSelectedField();
|
||||
};
|
||||
|
||||
const deleteCheck = !isEmpty(selectedField) && operation === 'delete';
|
||||
const updateCheck = !isEmpty(selectedField) && operation === 'update';
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="tw-bg-white tw-border tw-border-main tw-rounded tw-shadow">
|
||||
<table className="tw-w-full" data-testid="entity-custom-fields-table">
|
||||
<thead data-testid="table-header">
|
||||
<tr className="tableHead-row tw-border-t-0 tw-border-l-0 tw-border-r-0">
|
||||
<th className="tableHead-cell" data-testid="field-name">
|
||||
Name
|
||||
</th>
|
||||
<th className="tableHead-cell" data-testid="field-type">
|
||||
Type
|
||||
</th>
|
||||
<th className="tableHead-cell" data-testid="field-description">
|
||||
Description
|
||||
</th>
|
||||
<th className="tableHead-cell" data-testid="field-actions">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-testid="table-body">
|
||||
{customFields.length ? (
|
||||
customFields.map((field, index) => (
|
||||
<tr
|
||||
className={classNames(
|
||||
`tableBody-row ${!isEven(index + 1) && 'odd-row'}`,
|
||||
'tw-border-l-0 tw-border-r-0',
|
||||
{
|
||||
'tw-border-b-0': index === customFields.length - 1,
|
||||
}
|
||||
)}
|
||||
data-testid="data-row"
|
||||
key={uniqueId()}>
|
||||
<td className="tableBody-cell">{field.name}</td>
|
||||
<td className="tableBody-cell">
|
||||
{getEntityName(field.fieldType)}
|
||||
</td>
|
||||
<td className="tableBody-cell">
|
||||
{field.description ? (
|
||||
<RichTextEditorPreviewer
|
||||
markdown={field.description || ''}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="tw-no-description tw-p-2 tw--ml-1.5"
|
||||
data-testid="no-description">
|
||||
No description{' '}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="tableBody-cell">
|
||||
<div className="tw-flex">
|
||||
<button
|
||||
className="tw-cursor-pointer"
|
||||
data-testid="edit-button"
|
||||
onClick={() => {
|
||||
setSelectedField(field);
|
||||
setOperation('update');
|
||||
}}>
|
||||
<SVGIcons
|
||||
alt="edit"
|
||||
icon={Icons.EDIT}
|
||||
title="Edit"
|
||||
width="12px"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className="tw-cursor-pointer tw-ml-4"
|
||||
data-testid="delete-button"
|
||||
onClick={() => {
|
||||
setSelectedField(field);
|
||||
setOperation('delete');
|
||||
}}>
|
||||
<SVGIcons
|
||||
alt="delete"
|
||||
icon={Icons.DELETE}
|
||||
title="Delete"
|
||||
width="12px"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr
|
||||
className="tableBody-row tw-border-l-0 tw-border-r-0 tw-border-b-0"
|
||||
data-testid="no-data-row">
|
||||
<td
|
||||
className="tableBody-cell tw-text-grey-muted tw-text-center"
|
||||
colSpan={4}>
|
||||
No data
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{deleteCheck && (
|
||||
<ConfirmationModal
|
||||
bodyText={`Are you sure you want to delete the field ${selectedField.name}`}
|
||||
cancelText="Cancel"
|
||||
confirmText="Confirm"
|
||||
header={`Delete field ${selectedField.name}`}
|
||||
onCancel={resetSelectedField}
|
||||
onConfirm={handleFieldDelete}
|
||||
/>
|
||||
)}
|
||||
{updateCheck && (
|
||||
<ModalWithMarkdownEditor
|
||||
header={`Edit Field: "${selectedField.name}"`}
|
||||
placeholder="Enter Field Description"
|
||||
value={selectedField.description || ''}
|
||||
onCancel={resetSelectedField}
|
||||
onSave={handleFieldUpdate}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Type } from '../../generated/entity/type';
|
||||
import { LeftPanel } from './LeftPanel';
|
||||
|
||||
const typeList = [
|
||||
{
|
||||
id: '311ff2b1-472d-4307-878a-6e41b36a852d',
|
||||
name: 'table',
|
||||
fullyQualifiedName: 'table',
|
||||
displayName: 'table',
|
||||
description: '""',
|
||||
category: 'entity',
|
||||
nameSpace: 'data',
|
||||
schema: '',
|
||||
version: 0.2,
|
||||
updatedAt: 1653903654718,
|
||||
updatedBy: 'anonymous',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/311ff2b1-472d-4307-878a-6e41b36a852d',
|
||||
},
|
||||
] as Array<Type>;
|
||||
|
||||
const selectedType = typeList[0];
|
||||
|
||||
const mockPush = jest.fn();
|
||||
|
||||
const MOCK_HISTORY = {
|
||||
push: mockPush,
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: jest.fn().mockImplementation(() => MOCK_HISTORY),
|
||||
}));
|
||||
|
||||
jest.mock('../../constants/constants', () => {
|
||||
return {
|
||||
getCustomEntityPath: jest.fn().mockReturnValue('entityPath'),
|
||||
};
|
||||
});
|
||||
|
||||
const mockProp = {
|
||||
selectedType,
|
||||
typeList,
|
||||
};
|
||||
|
||||
describe('Test CustomEntity Detail Left Panel Component', () => {
|
||||
it('Should render Left Panel Component', async () => {
|
||||
const { findByTestId } = render(<LeftPanel {...mockProp} />);
|
||||
|
||||
const panelHeading = await findByTestId('panel-heading');
|
||||
|
||||
expect(panelHeading).toBeInTheDocument();
|
||||
|
||||
typeList.forEach(async (type) => {
|
||||
expect(
|
||||
await findByTestId(`entity-${type.displayName}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should call history.push on click of entity name', async () => {
|
||||
const { findByTestId } = render(<LeftPanel {...mockProp} />);
|
||||
|
||||
const entity = await findByTestId(`entity-${typeList[0].displayName}`);
|
||||
|
||||
expect(entity).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(entity);
|
||||
|
||||
expect(mockPush).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { startCase, uniqueId } from 'lodash';
|
||||
import React, { FC } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getCustomEntityPath } from '../../constants/constants';
|
||||
import { Type } from '../../generated/entity/type';
|
||||
|
||||
interface LeftPanelProp {
|
||||
typeList: Array<Type>;
|
||||
selectedType: Type;
|
||||
}
|
||||
|
||||
export const LeftPanel: FC<LeftPanelProp> = ({ typeList, selectedType }) => {
|
||||
const history = useHistory();
|
||||
|
||||
const getActiveClass = (typeName: string) => {
|
||||
return typeName === selectedType.name
|
||||
? 'tw-bg-primary-lite tw-text-primary tw-font-bold tw-border-l-2 tw-border-primary'
|
||||
: 'tw-bg-body-main';
|
||||
};
|
||||
|
||||
const handleLabelClick = (typeFQN: string) => {
|
||||
const path = getCustomEntityPath(typeFQN);
|
||||
history.push(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tw-flex tw-flex-col tw-bg-white tw-h-screen tw-p-3 tw-border tw-border-main tw-rounded-md">
|
||||
<h6 className="tw-heading tw-text-sm" data-testid="panel-heading">
|
||||
Schema & Custom Fields
|
||||
</h6>
|
||||
{typeList.map((type) => (
|
||||
<div
|
||||
className="tw-mb-3 tw-cursor-pointer"
|
||||
data-testid={`entity-${type.displayName}`}
|
||||
key={uniqueId()}
|
||||
onClick={() => handleLabelClick(type.fullyQualifiedName || '')}>
|
||||
<p
|
||||
className={classNames(
|
||||
'tw-px-3 tw-py-2 tw--mx-3',
|
||||
getActiveClass(type.name)
|
||||
)}
|
||||
data-testid="entity-displayName">{`${startCase(
|
||||
type.displayName
|
||||
)}s`}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -17,7 +17,8 @@ import React from 'react';
|
||||
import { TITLE_FOR_NON_OWNER_ACTION } from '../../../constants/constants';
|
||||
import { getCountBadge } from '../../../utils/CommonUtils';
|
||||
import NonAdminAction from '../non-admin-action/NonAdminAction';
|
||||
type Tab = {
|
||||
|
||||
export type Tab = {
|
||||
name: string;
|
||||
icon?: {
|
||||
alt: string;
|
||||
|
@ -70,6 +70,7 @@ const PLACEHOLDER_ROUTE_ENTITY_FQN = ':entityFQN';
|
||||
const PLACEHOLDER_WEBHOOK_NAME = ':webhookName';
|
||||
const PLACEHOLDER_USER_NAME = ':username';
|
||||
const PLACEHOLDER_BOTS_NAME = ':botsName';
|
||||
const PLACEHOLDER_ENTITY_TYPE_FQN = ':entityTypeFQN';
|
||||
|
||||
export const pagingObject = { after: '', before: '', total: 0 };
|
||||
|
||||
@ -212,6 +213,9 @@ export const ROUTES = {
|
||||
ADD_GLOSSARY_TERMS_CHILD: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/term/${PLACEHOLDER_GLOSSARY_TERMS_FQN}/add-term`,
|
||||
BOTS: `/bots`,
|
||||
BOTS_PROFILE: `/bots/${PLACEHOLDER_BOTS_NAME}`,
|
||||
CUSTOM_ENTITY: `/custom-entity`,
|
||||
CUSTOM_ENTITY_DETAIL: `/custom-entity/${PLACEHOLDER_ENTITY_TYPE_FQN}`,
|
||||
ADD_CUSTOM_FIELD: `/custom-entity/${PLACEHOLDER_ENTITY_TYPE_FQN}/add-field`,
|
||||
};
|
||||
|
||||
export const IN_PAGE_SEARCH_ROUTES: Record<string, Array<string>> = {
|
||||
@ -363,6 +367,20 @@ export const getBotsPath = (botsName: string) => {
|
||||
return path;
|
||||
};
|
||||
|
||||
export const getAddCustomFieldPath = (entityTypeFQN: string) => {
|
||||
let path = ROUTES.ADD_CUSTOM_FIELD;
|
||||
path = path.replace(PLACEHOLDER_ENTITY_TYPE_FQN, entityTypeFQN);
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const getCustomEntityPath = (entityTypeFQN: string) => {
|
||||
let path = ROUTES.CUSTOM_ENTITY_DETAIL;
|
||||
path = path.replace(PLACEHOLDER_ENTITY_TYPE_FQN, entityTypeFQN);
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const TIMEOUT = {
|
||||
USER_LIST: 60000, // 60 seconds for user retrieval
|
||||
TOAST_DELAY: 5000, // 5 seconds timeout for toaster autohide delay
|
||||
@ -376,6 +394,12 @@ export const navLinkDevelop = [
|
||||
|
||||
export const navLinkSettings = [
|
||||
{ name: 'Bots', to: '/bots', disabled: false, isAdminOnly: true },
|
||||
{
|
||||
name: 'Custom Entity',
|
||||
to: '/custom-entity',
|
||||
disabled: false,
|
||||
isAdminOnly: true,
|
||||
},
|
||||
{ name: 'Glossaries', to: '/glossary', disabled: false },
|
||||
{ name: 'Roles', to: '/roles', disabled: false, isAdminOnly: true },
|
||||
{ name: 'Services', to: '/services', disabled: false },
|
||||
|
@ -26,6 +26,7 @@ export enum EntityType {
|
||||
DASHBOARD_SERVICE = 'dashboardService',
|
||||
PIPELINE_SERVICE = 'pipelineService',
|
||||
WEBHOOK = 'webhook',
|
||||
TYPE = 'type',
|
||||
}
|
||||
|
||||
export enum AssetsType {
|
||||
|
@ -163,6 +163,7 @@ const jsonData = {
|
||||
message: {
|
||||
'no-services': 'No services',
|
||||
'fail-to-deploy-pipeline': 'Failed to deploy Ingestion Pipeline!',
|
||||
'no-custom-entity': 'No custom entity data available',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { getTypeListByCategory } from '../../axiosAPIs/metadataTypeAPI';
|
||||
import CustomEntityPage from './CustomEntityPage';
|
||||
|
||||
const mockData = {
|
||||
id: '32f81349-d7d7-4a6a-8fc7-d767f233b674',
|
||||
name: 'table',
|
||||
fullyQualifiedName: 'table',
|
||||
displayName: 'table',
|
||||
description:
|
||||
// eslint-disable-next-line max-len
|
||||
'"This schema defines the Table entity. A Table organizes data in rows and columns and is defined by a Schema. OpenMetadata does not have a separate abstraction for Schema. Both Table and Schema are captured in this entity."',
|
||||
category: 'entity',
|
||||
nameSpace: 'data',
|
||||
schema: '',
|
||||
version: 0.1,
|
||||
updatedAt: 1653626359971,
|
||||
updatedBy: 'admin',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/32f81349-d7d7-4a6a-8fc7-d767f233b674',
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useParams: jest.fn().mockReturnValue({
|
||||
entityTypeFQN: 'table',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../axiosAPIs/metadataTypeAPI', () => ({
|
||||
getTypeListByCategory: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ data: { data: [mockData] } })),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/CustomEntityDetail/CustomEntityDetail', () => {
|
||||
return jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
<div data-testid="CustomEntityDetail">CustomEntityDetail</div>
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('../../components/Loader/Loader', () =>
|
||||
jest.fn().mockReturnValue(<div data-testid="loader">Loader</div>)
|
||||
);
|
||||
|
||||
const mockGetTypeListByCategory = getTypeListByCategory as jest.Mock;
|
||||
|
||||
describe('Test CustomEntity Page Component', () => {
|
||||
it('Should render Custom Entity Detail Component', async () => {
|
||||
const { findByTestId } = render(<CustomEntityPage />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
const detailComponent = await findByTestId('CustomEntityDetail');
|
||||
|
||||
expect(detailComponent).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render error Component if API fails', async () => {
|
||||
mockGetTypeListByCategory.mockImplementationOnce(() => Promise.reject());
|
||||
|
||||
const { findByTestId } = render(<CustomEntityPage />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
|
||||
const errorComponent = await findByTestId('error');
|
||||
|
||||
expect(errorComponent).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2021 Collate
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { getTypeListByCategory } from '../../axiosAPIs/metadataTypeAPI';
|
||||
import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder';
|
||||
import CustomEntityDetail from '../../components/CustomEntityDetail/CustomEntityDetail';
|
||||
import Loader from '../../components/Loader/Loader';
|
||||
import { Category, Type } from '../../generated/entity/type';
|
||||
import jsonData from '../../jsons/en';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
|
||||
const CustomEntityPage = () => {
|
||||
const { entityTypeFQN } = useParams<{ [key: string]: string }>();
|
||||
const [entityTypes, setEntityTypes] = useState<Array<Type>>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isError, setIsError] = useState<boolean>(false);
|
||||
|
||||
const fetchEntityType = () => {
|
||||
setIsLoading(true);
|
||||
getTypeListByCategory(Category.Entity)
|
||||
.then((res: AxiosResponse) => {
|
||||
setEntityTypes(res.data.data || []);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
setIsError(true);
|
||||
showErrorToast(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchEntityType();
|
||||
}, []);
|
||||
|
||||
const Component = () =>
|
||||
isError ? (
|
||||
<ErrorPlaceHolder>
|
||||
{jsonData['api-error-messages']['unexpected-server-response']}
|
||||
</ErrorPlaceHolder>
|
||||
) : (
|
||||
<CustomEntityDetail
|
||||
entityTypeFQN={entityTypeFQN}
|
||||
entityTypes={entityTypes}
|
||||
/>
|
||||
);
|
||||
|
||||
return <Fragment>{isLoading ? <Loader /> : <Component />}</Fragment>;
|
||||
};
|
||||
|
||||
export default CustomEntityPage;
|
@ -15,6 +15,7 @@ import { isEmpty } from 'lodash';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import AppState from '../AppState';
|
||||
import AddCustomField from '../components/CustomEntityDetail/AddCustomField/AddCustomField';
|
||||
import { ROUTES } from '../constants/constants';
|
||||
import AddGlossaryPage from '../pages/AddGlossary/AddGlossaryPage.component';
|
||||
import AddGlossaryTermPage from '../pages/AddGlossaryTermPage/AddGlossaryTermPage.component';
|
||||
@ -24,6 +25,7 @@ import AddWebhookPage from '../pages/AddWebhookPage/AddWebhookPage.component';
|
||||
import BotsListPage from '../pages/BotsListpage/BotsListpage.component';
|
||||
import BotsPage from '../pages/BotsPage/BotsPage.component';
|
||||
import CreateUserPage from '../pages/CreateUserPage/CreateUserPage.component';
|
||||
import CustomEntityPage from '../pages/CustomEntityPage/CustomEntityPage';
|
||||
import DashboardDetailsPage from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
|
||||
import DatabaseDetails from '../pages/database-details/index';
|
||||
import DatabaseSchemaPageComponent from '../pages/DatabaseSchemaPage/DatabaseSchemaPage.component';
|
||||
@ -172,6 +174,21 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
component={BotsPage}
|
||||
path={ROUTES.BOTS_PROFILE}
|
||||
/>
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={CustomEntityPage}
|
||||
path={ROUTES.CUSTOM_ENTITY}
|
||||
/>
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={CustomEntityPage}
|
||||
path={ROUTES.CUSTOM_ENTITY_DETAIL}
|
||||
/>
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={AddCustomField}
|
||||
path={ROUTES.ADD_CUSTOM_FIELD}
|
||||
/>
|
||||
|
||||
<Redirect to={ROUTES.NOT_FOUND} />
|
||||
</Switch>
|
||||
|
@ -965,3 +965,7 @@ code {
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.custom-entity-schema > .CodeMirror {
|
||||
height: 70vh;
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import React, { Fragment } from 'react';
|
||||
import PopOver from '../components/common/popover/PopOver';
|
||||
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
|
||||
import {
|
||||
getCustomEntityPath,
|
||||
getDashboardDetailsPath,
|
||||
getDatabaseDetailsPath,
|
||||
getDatabaseSchemaDetailsPath,
|
||||
@ -200,6 +201,9 @@ export const getEntityLink = (
|
||||
case EntityType.WEBHOOK:
|
||||
return getEditWebhookPath(fullyQualifiedName);
|
||||
|
||||
case EntityType.TYPE:
|
||||
return getCustomEntityPath(fullyQualifiedName);
|
||||
|
||||
case SearchIndex.TABLE:
|
||||
case EntityType.TABLE:
|
||||
default:
|
||||
|
@ -49,6 +49,7 @@ const tagBG = '#EEEAF8';
|
||||
const badgeBG = '#E3E5E8';
|
||||
const primaryBG = '#7147E840'; // 'rgba(113, 71, 232, 0.25)';
|
||||
const backdropBG = '#302E36';
|
||||
const lightBG = '#F4F0FD';
|
||||
|
||||
// Borders and Separators
|
||||
const mainBorder = '#DCE3EC';
|
||||
@ -101,6 +102,7 @@ module.exports = {
|
||||
'primary-hover': primaryHover,
|
||||
'primary-active': primaryActive,
|
||||
'primary-hover-lite': primaryHoverLite,
|
||||
'primary-lite': lightBG,
|
||||
secondary: secondary,
|
||||
'secondary-lite': secondaryBG,
|
||||
'body-main': bodyBG,
|
||||
|
Loading…
x
Reference in New Issue
Block a user