mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 18:36:08 +00:00
UI : Added mutual exclusive fields in glosaary and classification (#9741)
* added mutual exclusive fields in glosaary and classification * added cypress and unit test and fix spacing issue * fix cypress issue * change translation key
This commit is contained in:
parent
86a57293ef
commit
94d3ffc2fd
@ -46,8 +46,13 @@ const createGlossaryTerm = (term) => {
|
|||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.type(term.synonyms);
|
.type(term.synonyms);
|
||||||
|
cy.get('[data-testid="mutually-exclusive-button"]')
|
||||||
|
.scrollIntoView()
|
||||||
|
.should('exist')
|
||||||
|
.should('be.visible')
|
||||||
|
.click();
|
||||||
|
|
||||||
cy.get('[data-testid="references"] > .tw-flex > .button-comp')
|
cy.get('[data-testid="references"] > .ant-space-item > .button-comp')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.click();
|
.click();
|
||||||
@ -175,6 +180,12 @@ describe('Glossary page should work properly', () => {
|
|||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.type(NEW_GLOSSARY.description);
|
.type(NEW_GLOSSARY.description);
|
||||||
|
|
||||||
|
cy.get('[data-testid="mutually-exclusive-button"]')
|
||||||
|
.scrollIntoView()
|
||||||
|
.should('exist')
|
||||||
|
.should('be.visible')
|
||||||
|
.click();
|
||||||
|
|
||||||
cy.get('[data-testid="add-reviewers"]')
|
cy.get('[data-testid="add-reviewers"]')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
|
@ -86,6 +86,10 @@ describe('Tags page should work', () => {
|
|||||||
cy.get(descriptionBox)
|
cy.get(descriptionBox)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
.type(NEW_TAG_CATEGORY.description);
|
.type(NEW_TAG_CATEGORY.description);
|
||||||
|
cy.get('[data-testid="mutually-exclusive-button"]')
|
||||||
|
.scrollIntoView()
|
||||||
|
.should('be.visible')
|
||||||
|
.click();
|
||||||
|
|
||||||
cy.get('[data-testid="saveButton"]')
|
cy.get('[data-testid="saveButton"]')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Space, Typography } from 'antd';
|
import { Space, Switch, Typography } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Tags from 'components/Tag/Tags/tags';
|
import Tags from 'components/Tag/Tags/tags';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
@ -60,6 +60,7 @@ const AddGlossary = ({
|
|||||||
const [description] = useState<string>('');
|
const [description] = useState<string>('');
|
||||||
const [showReviewerModal, setShowReviewerModal] = useState(false);
|
const [showReviewerModal, setShowReviewerModal] = useState(false);
|
||||||
const [tags, setTags] = useState<EntityTags[]>([]);
|
const [tags, setTags] = useState<EntityTags[]>([]);
|
||||||
|
const [mutuallyExclusive, setMutuallyExclusive] = useState(false);
|
||||||
const [reviewer, setReviewer] = useState<Array<EntityReference>>([]);
|
const [reviewer, setReviewer] = useState<Array<EntityReference>>([]);
|
||||||
|
|
||||||
const getDescription = () => {
|
const getDescription = () => {
|
||||||
@ -129,6 +130,7 @@ const AddGlossary = ({
|
|||||||
type: 'user',
|
type: 'user',
|
||||||
},
|
},
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
mutuallyExclusive,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSave(data);
|
onSave(data);
|
||||||
@ -245,6 +247,23 @@ const AddGlossary = ({
|
|||||||
</Space>
|
</Space>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Space align="end">
|
||||||
|
<label
|
||||||
|
className="tw-form-label m-b-0 tw-mb-1"
|
||||||
|
data-testid="mutually-exclusive-label"
|
||||||
|
htmlFor="mutuallyExclusive">
|
||||||
|
{t('label.mutually-exclusive')}
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
checked={mutuallyExclusive}
|
||||||
|
data-testid="mutually-exclusive-button"
|
||||||
|
id="mutuallyExclusive"
|
||||||
|
onChange={(value) => setMutuallyExclusive(value)}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="tw-flex tw-items-center tw-mt-4">
|
<div className="tw-flex tw-items-center tw-mt-4">
|
||||||
<span className="w-form-label tw-mr-3">
|
<span className="w-form-label tw-mr-3">
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Space } from 'antd';
|
import { Space, Switch } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Tags from 'components/Tag/Tags/tags';
|
import Tags from 'components/Tag/Tags/tags';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
@ -76,6 +76,7 @@ const AddGlossaryTerm = ({
|
|||||||
const [tags, setTags] = useState<EntityTags[]>([]);
|
const [tags, setTags] = useState<EntityTags[]>([]);
|
||||||
const [relatedTerms, setRelatedTerms] = useState<GlossaryTerm[]>([]);
|
const [relatedTerms, setRelatedTerms] = useState<GlossaryTerm[]>([]);
|
||||||
const [synonyms, setSynonyms] = useState('');
|
const [synonyms, setSynonyms] = useState('');
|
||||||
|
const [mutuallyExclusive, setMutuallyExclusive] = useState(false);
|
||||||
const [references, setReferences] = useState<TermReference[]>([]);
|
const [references, setReferences] = useState<TermReference[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -232,6 +233,7 @@ const AddGlossaryTerm = ({
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
synonyms: synonyms ? synonyms.split(',') : undefined,
|
synonyms: synonyms ? synonyms.split(',') : undefined,
|
||||||
|
mutuallyExclusive,
|
||||||
glossary: {
|
glossary: {
|
||||||
id: glossaryData.id,
|
id: glossaryData.id,
|
||||||
type: 'glossary',
|
type: 'glossary',
|
||||||
@ -274,7 +276,7 @@ const AddGlossaryTerm = ({
|
|||||||
theme="primary"
|
theme="primary"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleSave}>
|
onClick={handleSave}>
|
||||||
Save
|
{t('label.save')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -284,14 +286,13 @@ const AddGlossaryTerm = ({
|
|||||||
const fetchRightPanel = () => {
|
const fetchRightPanel = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h6 className="tw-heading tw-text-base">Configure Glossary Term</h6>
|
<h6 className="tw-heading tw-text-base">
|
||||||
|
{t('label.configure-entity', {
|
||||||
|
entity: t('label.glossary-term'),
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<div className="tw-mb-5">
|
<div className="tw-mb-5">
|
||||||
Every term in the glossary has a unique definition. Along with
|
{t('message.configure-glossary-term-description')}
|
||||||
defining the standard term for a concept, the synonyms as well as
|
|
||||||
related terms (for e.g., parent and child terms) can be specified.
|
|
||||||
References can be added to the assets related to the terms. New terms
|
|
||||||
can be added or updated to the Glossary. The glossary terms can be
|
|
||||||
reviewed by certain users, who can accept or reject the terms.
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -304,11 +305,15 @@ const AddGlossaryTerm = ({
|
|||||||
layout={PageLayoutType['2ColRTL']}
|
layout={PageLayoutType['2ColRTL']}
|
||||||
rightPanel={fetchRightPanel()}>
|
rightPanel={fetchRightPanel()}>
|
||||||
<div className="tw-form-container">
|
<div className="tw-form-container">
|
||||||
<h6 className="tw-heading tw-text-base">Add Glossary Term</h6>
|
<h6 className="tw-heading tw-text-base">
|
||||||
|
{t('label.add-entity', {
|
||||||
|
entity: t('label.glossary-term'),
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<div className="tw-pb-3" data-testid="add-glossary-term">
|
<div className="tw-pb-3" data-testid="add-glossary-term">
|
||||||
<Field>
|
<Field>
|
||||||
<label className="tw-block tw-form-label" htmlFor="name">
|
<label className="tw-block tw-form-label" htmlFor="name">
|
||||||
{requiredField('Name:')}
|
{requiredField(`${t('label.name')}:`)}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -316,16 +321,24 @@ const AddGlossaryTerm = ({
|
|||||||
data-testid="name"
|
data-testid="name"
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
placeholder="Name"
|
placeholder={t('label.name')}
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={handleValidation}
|
onChange={handleValidation}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showErrorMsg.name
|
{showErrorMsg.name
|
||||||
? errorMsg('Glossary term name is required.')
|
? errorMsg(
|
||||||
|
t('message.field-text-is-required', {
|
||||||
|
fieldText: `${t('label.glossary-term')} ${t('label.name')}`,
|
||||||
|
})
|
||||||
|
)
|
||||||
: showErrorMsg.invalidName
|
: showErrorMsg.invalidName
|
||||||
? errorMsg('Glossary term name is invalid.')
|
? errorMsg(
|
||||||
|
t('message.field-text-is-invalid', {
|
||||||
|
fieldText: `${t('label.glossary-term')} ${t('label.name')}`,
|
||||||
|
})
|
||||||
|
)
|
||||||
: null}
|
: null}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@ -333,7 +346,7 @@ const AddGlossaryTerm = ({
|
|||||||
<label
|
<label
|
||||||
className="tw-block tw-form-label tw-mb-0"
|
className="tw-block tw-form-label tw-mb-0"
|
||||||
htmlFor="description">
|
htmlFor="description">
|
||||||
{requiredField('Description:')}
|
{requiredField(`${t('label.description')}:`)}
|
||||||
</label>
|
</label>
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
data-testid="description"
|
data-testid="description"
|
||||||
@ -341,12 +354,17 @@ const AddGlossaryTerm = ({
|
|||||||
readonly={!allowAccess}
|
readonly={!allowAccess}
|
||||||
ref={markdownRef}
|
ref={markdownRef}
|
||||||
/>
|
/>
|
||||||
{showErrorMsg.description && errorMsg('Description is required.')}
|
{showErrorMsg.description &&
|
||||||
|
errorMsg(
|
||||||
|
t('label.field-required', {
|
||||||
|
field: t('label.description'),
|
||||||
|
})
|
||||||
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<Space className="w-full" direction="vertical">
|
<Space className="w-full" direction="vertical">
|
||||||
<label htmlFor="tags">Tags:</label>
|
<label htmlFor="tags">{t('label.tag-plural')}:</label>
|
||||||
<AddTags
|
<AddTags
|
||||||
data-testid="tags"
|
data-testid="tags"
|
||||||
setTags={(tag: EntityTags[]) => setTags(tag)}
|
setTags={(tag: EntityTags[]) => setTags(tag)}
|
||||||
@ -356,7 +374,7 @@ const AddGlossaryTerm = ({
|
|||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<label className="tw-block tw-form-label" htmlFor="synonyms">
|
<label className="tw-block tw-form-label" htmlFor="synonyms">
|
||||||
Synonyms:
|
{t('label.synonym-plural')}:
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -371,9 +389,29 @@ const AddGlossaryTerm = ({
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<div data-testid="references">
|
<div className="m-t-lg">
|
||||||
<div className="tw-flex tw-items-center tw-mt-6">
|
<Field>
|
||||||
<p className="w-form-label tw-mr-3">References</p>
|
<Space align="end">
|
||||||
|
<label
|
||||||
|
className="tw-form-label m-b-0"
|
||||||
|
data-testid="mutually-exclusive-label"
|
||||||
|
htmlFor="mutuallyExclusive">
|
||||||
|
{t('label.mutually-exclusive')}
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
checked={mutuallyExclusive}
|
||||||
|
data-testid="mutually-exclusive-button"
|
||||||
|
id="mutuallyExclusive"
|
||||||
|
onChange={(value) => setMutuallyExclusive(value)}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<Space align="end" data-testid="references">
|
||||||
|
<label className="tw-form-label m-b-0">
|
||||||
|
{t('label.reference-plural')}
|
||||||
|
</label>
|
||||||
<Button
|
<Button
|
||||||
className="tw-h-5 tw-px-2"
|
className="tw-h-5 tw-px-2"
|
||||||
size="x-small"
|
size="x-small"
|
||||||
@ -382,7 +420,8 @@ const AddGlossaryTerm = ({
|
|||||||
onClick={addReferenceFields}>
|
onClick={addReferenceFields}>
|
||||||
<FontAwesomeIcon icon="plus" />
|
<FontAwesomeIcon icon="plus" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Space>
|
||||||
|
</Field>
|
||||||
|
|
||||||
{references.map((value, i) => (
|
{references.map((value, i) => (
|
||||||
<div className="tw-flex tw-items-center" key={i}>
|
<div className="tw-flex tw-items-center" key={i}>
|
||||||
@ -392,7 +431,7 @@ const AddGlossaryTerm = ({
|
|||||||
className="tw-form-inputs tw-form-inputs-padding"
|
className="tw-form-inputs tw-form-inputs-padding"
|
||||||
id={`name-${i}`}
|
id={`name-${i}`}
|
||||||
name="key"
|
name="key"
|
||||||
placeholder="Name"
|
placeholder={t('label.name')}
|
||||||
type="text"
|
type="text"
|
||||||
value={value.name}
|
value={value.name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@ -405,7 +444,7 @@ const AddGlossaryTerm = ({
|
|||||||
className="tw-form-inputs tw-form-inputs-padding"
|
className="tw-form-inputs tw-form-inputs-padding"
|
||||||
id={`url-${i}`}
|
id={`url-${i}`}
|
||||||
name="endpoint"
|
name="endpoint"
|
||||||
placeholder="url"
|
placeholder={t('label.url-lowercase')}
|
||||||
type="text"
|
type="text"
|
||||||
value={value.endpoint}
|
value={value.endpoint}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@ -425,7 +464,7 @@ const AddGlossaryTerm = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}>
|
}}>
|
||||||
<SVGIcons
|
<SVGIcons
|
||||||
alt="delete"
|
alt={t('label.valid-url-endpoint')}
|
||||||
icon="icon-delete"
|
icon="icon-delete"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
width="16px"
|
width="16px"
|
||||||
@ -434,13 +473,15 @@ const AddGlossaryTerm = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{showErrorMsg.invalidReferences
|
{showErrorMsg.invalidReferences
|
||||||
? errorMsg('Endpoints should be valid URL.')
|
? errorMsg(t('message.valid-url-endpoint'))
|
||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field>
|
<Field>
|
||||||
<div className="tw-flex tw-items-center tw-mt-4">
|
<div className="tw-flex tw-items-center tw-mt-4">
|
||||||
<p className="w-form-label tw-mr-3">Related terms </p>
|
<p className="w-form-label tw-mr-3">
|
||||||
|
{t('label.related-term-plural')}
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
className="tw-h-5 tw-px-2"
|
className="tw-h-5 tw-px-2"
|
||||||
size="x-small"
|
size="x-small"
|
||||||
@ -469,7 +510,9 @@ const AddGlossaryTerm = ({
|
|||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<div className="tw-flex tw-items-center tw-mt-4">
|
<div className="tw-flex tw-items-center tw-mt-4">
|
||||||
<p className="w-form-label tw-mr-3">Reviewers </p>
|
<p className="w-form-label tw-mr-3">
|
||||||
|
{t('label.reviewer-plural')}
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
className="tw-h-5 tw-px-2"
|
className="tw-h-5 tw-px-2"
|
||||||
data-testid="add-reviewers"
|
data-testid="add-reviewers"
|
||||||
@ -505,7 +548,7 @@ const AddGlossaryTerm = ({
|
|||||||
theme="primary"
|
theme="primary"
|
||||||
variant="text"
|
variant="text"
|
||||||
onClick={onCancel}>
|
onClick={onCancel}>
|
||||||
Cancel
|
{t('label.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
{getSaveButton()}
|
{getSaveButton()}
|
||||||
</Field>
|
</Field>
|
||||||
|
@ -99,7 +99,7 @@ const ImportResult: FC<Props> = ({ csvImportResult }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('label.synonyms'),
|
title: t('label.synonym-plural'),
|
||||||
dataIndex: 'synonyms',
|
dataIndex: 'synonyms',
|
||||||
key: 'synonyms',
|
key: 'synonyms',
|
||||||
render: (synonyms: GlossaryCSVRecord['synonyms']) => {
|
render: (synonyms: GlossaryCSVRecord['synonyms']) => {
|
||||||
|
@ -26,6 +26,7 @@ export type FormModalProp = {
|
|||||||
errorData?: FormErrorData;
|
errorData?: FormErrorData;
|
||||||
isSaveButtonDisabled?: boolean;
|
isSaveButtonDisabled?: boolean;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
showHiddenFields?: boolean;
|
||||||
};
|
};
|
||||||
export type FormRef = {
|
export type FormRef = {
|
||||||
fetchMarkDownData: () => string;
|
fetchMarkDownData: () => string;
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Button, Modal, Typography } from 'antd';
|
import { Button, Modal, Space, Typography } from 'antd';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Classification } from '../../../generated/entity/classification/classification';
|
import { Classification } from '../../../generated/entity/classification/classification';
|
||||||
@ -28,6 +28,7 @@ const FormModal = ({
|
|||||||
errorData,
|
errorData,
|
||||||
isSaveButtonDisabled,
|
isSaveButtonDisabled,
|
||||||
visible,
|
visible,
|
||||||
|
showHiddenFields = false,
|
||||||
}: FormModalProp) => {
|
}: FormModalProp) => {
|
||||||
const formRef = useRef<FormRef>();
|
const formRef = useRef<FormRef>();
|
||||||
const [data, setData] = useState<FormData>(initialData);
|
const [data, setData] = useState<FormData>(initialData);
|
||||||
@ -47,7 +48,10 @@ const FormModal = ({
|
|||||||
closable={false}
|
closable={false}
|
||||||
data-testid="modal-container"
|
data-testid="modal-container"
|
||||||
footer={
|
footer={
|
||||||
<div className="tw-modal-footer" data-testid="cta-container">
|
<Space
|
||||||
|
align="end"
|
||||||
|
className="justify-end p-r-lg p-t-sm"
|
||||||
|
data-testid="cta-container">
|
||||||
<Button type="link" onClick={onCancel}>
|
<Button type="link" onClick={onCancel}>
|
||||||
{t('label.cancel')}
|
{t('label.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -59,7 +63,7 @@ const FormModal = ({
|
|||||||
type="primary">
|
type="primary">
|
||||||
{t('label.save')}
|
{t('label.save')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Space>
|
||||||
}
|
}
|
||||||
open={visible}
|
open={visible}
|
||||||
title={
|
title={
|
||||||
@ -78,6 +82,7 @@ const FormModal = ({
|
|||||||
setData(data);
|
setData(data);
|
||||||
onChange && onChange(data);
|
onChange && onChange(data);
|
||||||
}}
|
}}
|
||||||
|
showHiddenFields={showHiddenFields}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -324,6 +324,7 @@
|
|||||||
"most-recent-session": "Most Recent Session",
|
"most-recent-session": "Most Recent Session",
|
||||||
"move-the-team": "Move the Team",
|
"move-the-team": "Move the Team",
|
||||||
"ms-team-plural": "MS Teams",
|
"ms-team-plural": "MS Teams",
|
||||||
|
"mutually-exclusive": "Mutually Exclusive",
|
||||||
"my-data": "My Data",
|
"my-data": "My Data",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name-lowercase": "name",
|
"name-lowercase": "name",
|
||||||
@ -412,6 +413,7 @@
|
|||||||
"recent-run-plural": "Recent Runs",
|
"recent-run-plural": "Recent Runs",
|
||||||
"recent-search-term-plural": "Recent Search Terms",
|
"recent-search-term-plural": "Recent Search Terms",
|
||||||
"recreate-index-plural": "Recreate Indexes",
|
"recreate-index-plural": "Recreate Indexes",
|
||||||
|
"reference-plural": "References",
|
||||||
"region-name": "Region Name",
|
"region-name": "Region Name",
|
||||||
"registry": "Registry",
|
"registry": "Registry",
|
||||||
"related-term-plural": "Related Terms",
|
"related-term-plural": "Related Terms",
|
||||||
@ -507,7 +509,7 @@
|
|||||||
"success": "Success",
|
"success": "Success",
|
||||||
"suite": "Suite",
|
"suite": "Suite",
|
||||||
"summary": "Summary",
|
"summary": "Summary",
|
||||||
"synonyms": "Synonyms",
|
"synonym-plural": "synonyms",
|
||||||
"table": "Table",
|
"table": "Table",
|
||||||
"table-entity-text": "Table {{entityText}}",
|
"table-entity-text": "Table {{entityText}}",
|
||||||
"table-lowercase": "table",
|
"table-lowercase": "table",
|
||||||
@ -627,6 +629,7 @@
|
|||||||
"collaborate-with-other-user": "to collaborate with other users.",
|
"collaborate-with-other-user": "to collaborate with other users.",
|
||||||
"configure-a-service-description": "Enter a unique service name. The name must be unique across the category of services. For e.g., among database services, both MySQL and Snowflake cannot have the same service name (E.g. customer_data). However, different service categories (dashboard, pipeline) can have the same service name. Spaces are not supported in the service name. Characters like - _ are supported. Also, add a description.",
|
"configure-a-service-description": "Enter a unique service name. The name must be unique across the category of services. For e.g., among database services, both MySQL and Snowflake cannot have the same service name (E.g. customer_data). However, different service categories (dashboard, pipeline) can have the same service name. Spaces are not supported in the service name. Characters like - _ are supported. Also, add a description.",
|
||||||
"configure-dbt-model-description": "A dbt model provides transformation logic that creates a table from raw data. Lineage traces the path of data across tables, but a dbt model provides specifics. Select the required dbt source provider and fill in the mandatory fields. Integrate with dbt from OpenMetadata to view the models used to generate tables.",
|
"configure-dbt-model-description": "A dbt model provides transformation logic that creates a table from raw data. Lineage traces the path of data across tables, but a dbt model provides specifics. Select the required dbt source provider and fill in the mandatory fields. Integrate with dbt from OpenMetadata to view the models used to generate tables.",
|
||||||
|
"configure-glossary-term-description": "Every term in the glossary has a unique definition. Along with defining the standard term for a concept, the synonyms as well as related terms (for e.g., parent and child terms) can be specified. References can be added to the assets related to the terms. New terms can be added or updated to the Glossary. The glossary terms can be reviewed by certain users, who can accept or reject the terms.",
|
||||||
"configure-webhook-message": "OpenMetadata can be configured to automatically send out event notifications to registered webhooks. Enter the Webhook Name, and an Endpoint URL to receive the HTTP callback on. Use Event Filters to only receive notifications based on events of interest, like when an entity is created, updated, or deleted; and for the entities your application is interested in. Add a description to help people understand the purpose of the webhook and to keep track of the use case. Use advanced configuration to set up a shared secret key to verify the webhook events using HMAC signature.",
|
"configure-webhook-message": "OpenMetadata can be configured to automatically send out event notifications to registered webhooks. Enter the Webhook Name, and an Endpoint URL to receive the HTTP callback on. Use Event Filters to only receive notifications based on events of interest, like when an entity is created, updated, or deleted; and for the entities your application is interested in. Add a description to help people understand the purpose of the webhook and to keep track of the use case. Use advanced configuration to set up a shared secret key to verify the webhook events using HMAC signature.",
|
||||||
"configure-webhook-name-message": "OpenMetadata can be configured to automatically send out event notifications to registered {{webhookType}} webhooks through OpenMetadata. Enter the {{webhookType}} webhook name, and an Endpoint URL to receive the HTTP callback on. Use Event Filters to only receive notifications for the required entities. Filter events based on when an entity is created, updated, or deleted. Add a description to note the use case of the webhook. You can use advanced configuration to set up a shared secret key to verify the {{webhookType}} webhook events using HMAC signature.",
|
"configure-webhook-name-message": "OpenMetadata can be configured to automatically send out event notifications to registered {{webhookType}} webhooks through OpenMetadata. Enter the {{webhookType}} webhook name, and an Endpoint URL to receive the HTTP callback on. Use Event Filters to only receive notifications for the required entities. Filter events based on when an entity is created, updated, or deleted. Add a description to note the use case of the webhook. You can use advanced configuration to set up a shared secret key to verify the {{webhookType}} webhook events using HMAC signature.",
|
||||||
"confirm-delete-message": "Are you sure you want to permanently delete this message?",
|
"confirm-delete-message": "Are you sure you want to permanently delete this message?",
|
||||||
@ -802,6 +805,7 @@
|
|||||||
"tour-step-type-search-term": "In the search box, type <0>\"{{text}}\"</0>. Hit <0>{{enterText}}.</0>",
|
"tour-step-type-search-term": "In the search box, type <0>\"{{text}}\"</0>. Hit <0>{{enterText}}.</0>",
|
||||||
"usage-ingestion-description": "Usage ingestion can be configured and deployed after a metadata ingestion has been set up. The usage ingestion workflow obtains the query log and table creation details from the underlying database and feeds it to OpenMetadata. Metadata and usage can have only one pipeline for a database service. Define the Query Log Duration (in days), Stage File Location, and Result Limit to start.",
|
"usage-ingestion-description": "Usage ingestion can be configured and deployed after a metadata ingestion has been set up. The usage ingestion workflow obtains the query log and table creation details from the underlying database and feeds it to OpenMetadata. Metadata and usage can have only one pipeline for a database service. Define the Query Log Duration (in days), Stage File Location, and Result Limit to start.",
|
||||||
"use-fqn-for-filtering-message": "Regex will be applied on fully qualified name (e.g service_name.db_name.schema_name.table_name) instead of raw name (e.g. table_name).",
|
"use-fqn-for-filtering-message": "Regex will be applied on fully qualified name (e.g service_name.db_name.schema_name.table_name) instead of raw name (e.g. table_name).",
|
||||||
|
"valid-url-endpoint": "Endpoints should be valid URL",
|
||||||
"view-deleted-teams": "View All the Deleted Teams, which come under this Team.",
|
"view-deleted-teams": "View All the Deleted Teams, which come under this Team.",
|
||||||
"view-sample-data-message": "To view Sample Data, run the MetaData Ingestion. Please refer to this doc to schedule the",
|
"view-sample-data-message": "To view Sample Data, run the MetaData Ingestion. Please refer to this doc to schedule the",
|
||||||
"webhook-listing-message": "The webhook allows external services to be notified of the metadata change events happening in your organization through APIs. Register callback URLs with webhook integration to receive metadata event notifications. You can add, list, update, and delete webhooks.",
|
"webhook-listing-message": "The webhook allows external services to be notified of the metadata change events happening in your organization through APIs. Register callback URLs with webhook integration to receive metadata event notifications. You can add, list, update, and delete webhooks.",
|
||||||
|
@ -20,6 +20,7 @@ const mockFunction = jest.fn();
|
|||||||
const mockInitialData = {
|
const mockInitialData = {
|
||||||
description: '',
|
description: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
mutuallyExclusive: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('components/common/rich-text-editor/RichTextEditor', () => {
|
jest.mock('components/common/rich-text-editor/RichTextEditor', () => {
|
||||||
@ -33,7 +34,11 @@ jest.mock('components/common/rich-text-editor/RichTextEditor', () => {
|
|||||||
describe('Test TagsPage form component', () => {
|
describe('Test TagsPage form component', () => {
|
||||||
it('Form component should render properly', async () => {
|
it('Form component should render properly', async () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<Form initialData={mockInitialData} saveData={mockFunction} />,
|
<Form
|
||||||
|
initialData={mockInitialData}
|
||||||
|
saveData={mockFunction}
|
||||||
|
showHiddenFields={false}
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
wrapper: MemoryRouter,
|
wrapper: MemoryRouter,
|
||||||
}
|
}
|
||||||
@ -46,4 +51,30 @@ describe('Test TagsPage form component', () => {
|
|||||||
await findByText(container, /MarkdownWithPreview component/i)
|
await findByText(container, /MarkdownWithPreview component/i)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Form component should render Mutually Exclusive field', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Form
|
||||||
|
showHiddenFields
|
||||||
|
initialData={mockInitialData}
|
||||||
|
saveData={mockFunction}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutuallyExclusiveLabel = await findByTestId(
|
||||||
|
container,
|
||||||
|
'mutually-exclusive-label'
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutuallyExclusiveButton = await findByTestId(
|
||||||
|
container,
|
||||||
|
'mutually-exclusive-button'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mutuallyExclusiveLabel).toBeInTheDocument();
|
||||||
|
expect(mutuallyExclusiveButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Space, Switch } from 'antd';
|
||||||
import RichTextEditor from 'components/common/rich-text-editor/RichTextEditor';
|
import RichTextEditor from 'components/common/rich-text-editor/RichTextEditor';
|
||||||
import { FormErrorData } from 'Models';
|
import { FormErrorData } from 'Models';
|
||||||
import React, {
|
import React, {
|
||||||
@ -20,33 +21,41 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CreateClassification } from '../../generated/api/classification/createClassification';
|
import { CreateClassification } from '../../generated/api/classification/createClassification';
|
||||||
import { errorMsg } from '../../utils/CommonUtils';
|
import { errorMsg } from '../../utils/CommonUtils';
|
||||||
|
|
||||||
type CustomClassification = {
|
type CustomClassification = {
|
||||||
description: CreateClassification['description'];
|
description: CreateClassification['description'];
|
||||||
name: CreateClassification['name'];
|
name: CreateClassification['name'];
|
||||||
|
mutuallyExclusive: CreateClassification['mutuallyExclusive'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormProp = {
|
type FormProp = {
|
||||||
saveData: (value: CreateClassification) => void;
|
saveData: (value: CreateClassification) => void;
|
||||||
initialData: CustomClassification;
|
initialData: CustomClassification;
|
||||||
errorData?: FormErrorData;
|
errorData?: FormErrorData;
|
||||||
|
showHiddenFields: boolean;
|
||||||
};
|
};
|
||||||
type EditorContentRef = {
|
type EditorContentRef = {
|
||||||
getEditorContent: () => string;
|
getEditorContent: () => string;
|
||||||
};
|
};
|
||||||
const Form: React.FC<FormProp> = forwardRef(
|
const Form: React.FC<FormProp> = forwardRef(
|
||||||
({ saveData, initialData, errorData }: FormProp, ref): JSX.Element => {
|
(
|
||||||
|
{ saveData, initialData, errorData, showHiddenFields }: FormProp,
|
||||||
|
ref
|
||||||
|
): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [data, setData] = useState<CustomClassification>({
|
const [data, setData] = useState<CustomClassification>({
|
||||||
name: initialData.name,
|
name: initialData.name,
|
||||||
description: initialData.description,
|
description: initialData.description,
|
||||||
|
mutuallyExclusive: initialData.mutuallyExclusive,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isMounting = useRef<boolean>(true);
|
const isMounting = useRef<boolean>(true);
|
||||||
const markdownRef = useRef<EditorContentRef>();
|
const markdownRef = useRef<EditorContentRef>();
|
||||||
|
|
||||||
const onChangeHadler = (
|
const onChangeHandler = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
) => {
|
) => {
|
||||||
e.persist();
|
e.persist();
|
||||||
@ -82,7 +91,9 @@ const Form: React.FC<FormProp> = forwardRef(
|
|||||||
<div className="tw-flex tw-w-full">
|
<div className="tw-flex tw-w-full">
|
||||||
<div className="tw-w-full">
|
<div className="tw-w-full">
|
||||||
<div className="tw-mb-4">
|
<div className="tw-mb-4">
|
||||||
<label className="tw-form-label required-field">Name</label>
|
<label className="tw-form-label required-field">
|
||||||
|
{t('label.name')}
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="tw-text-sm tw-appearance-none tw-border tw-border-main
|
className="tw-text-sm tw-appearance-none tw-border tw-border-main
|
||||||
@ -93,17 +104,41 @@ const Form: React.FC<FormProp> = forwardRef(
|
|||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
type="text"
|
type="text"
|
||||||
value={data.name}
|
value={data.name}
|
||||||
onChange={onChangeHadler}
|
onChange={onChangeHandler}
|
||||||
/>
|
/>
|
||||||
{errorData?.name && errorMsg(errorData.name)}
|
{errorData?.name && errorMsg(errorData.name)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="tw-form-label">Description</label>
|
<label className="tw-form-label">{t('label.description')}</label>
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
initialValue={data.description}
|
initialValue={data.description}
|
||||||
ref={markdownRef}
|
ref={markdownRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showHiddenFields && (
|
||||||
|
<div>
|
||||||
|
<Space align="end" className="m-y-md">
|
||||||
|
<label
|
||||||
|
className="tw-form-label m-b-0 tw-mb-1"
|
||||||
|
data-testid="mutually-exclusive-label"
|
||||||
|
htmlFor="mutuallyExclusive">
|
||||||
|
{t('label.mutually-exclusive')}
|
||||||
|
</label>
|
||||||
|
<Switch
|
||||||
|
checked={data.mutuallyExclusive}
|
||||||
|
data-testid="mutually-exclusive-button"
|
||||||
|
id="mutuallyExclusive"
|
||||||
|
onChange={(value) =>
|
||||||
|
setData((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
mutuallyExclusive: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -944,6 +944,7 @@ const TagsPage = () => {
|
|||||||
onSave={updatePrimaryTag}
|
onSave={updatePrimaryTag}
|
||||||
/>
|
/>
|
||||||
<FormModal
|
<FormModal
|
||||||
|
showHiddenFields
|
||||||
errorData={errorDataClassification}
|
errorData={errorDataClassification}
|
||||||
form={Form}
|
form={Form}
|
||||||
header={t('label.adding-new-classification')}
|
header={t('label.adding-new-classification')}
|
||||||
|
@ -24,7 +24,7 @@ ${TabSpecificField.USAGE_SUMMARY}, ${TabSpecificField.CHARTS},${TabSpecificField
|
|||||||
|
|
||||||
export const dashboardDetailsTabs = [
|
export const dashboardDetailsTabs = [
|
||||||
{
|
{
|
||||||
name: i18next.t('label.details'),
|
name: i18next.t('label.detail-plural'),
|
||||||
path: 'details',
|
path: 'details',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user