mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-10-31 02:29:03 +00:00 
			
		
		
		
	fix(ui): show user info with displayName and profile on version timeline (#15233)
This commit is contained in:
		
							parent
							
								
									ff2ecc56f2
								
							
						
					
					
						commit
						ad9a6df282
					
				| @ -57,6 +57,7 @@ import { | ||||
| } from '../Utils/Owner'; | ||||
| import { assignTags, removeTags, udpateTags } from '../Utils/Tags'; | ||||
| import { addTier, removeTier, updateTier } from '../Utils/Tier'; | ||||
| import { validateDomain } from '../Utils/Versions'; | ||||
| import { downVoteEntity, upVoteEntity } from '../Utils/Voting'; | ||||
| 
 | ||||
| const description = | ||||
| @ -366,6 +367,7 @@ class EntityClass { | ||||
| 
 | ||||
|   assignDomain() { | ||||
|     addDomainToEntity(domainDetails1.displayName); | ||||
|     validateDomain(domainDetails1.displayName, this.endPoint); | ||||
|   } | ||||
| 
 | ||||
|   updateDomain() { | ||||
|  | ||||
| @ -0,0 +1,20 @@ | ||||
| /* | ||||
|  *  Copyright 2024 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 { EntityType } from '../../constants/Entity.interface'; | ||||
| import { interceptURL, verifyResponseStatusCode } from '../common'; | ||||
| 
 | ||||
| export const validateDomain = (domain: string, entityType: EntityType) => { | ||||
|   interceptURL('GET', `/api/v1/${entityType}/*/versions/0.2`, 'getVersion'); | ||||
|   cy.get('[data-testid="version-button"]').should('contain', '0.2').click(); | ||||
|   verifyResponseStatusCode('@getVersion', 200); | ||||
| }; | ||||
| @ -108,6 +108,10 @@ jest.mock('../../../components/PageLayoutV1/PageLayoutV1', () => { | ||||
|   return jest.fn().mockImplementation(({ children }) => <div>{children}</div>); | ||||
| }); | ||||
| 
 | ||||
| jest.mock('../../Entity/EntityVersionTimeLine/EntityVersionTimeLine', () => { | ||||
|   return jest.fn().mockReturnValue(<>Version timeline</>); | ||||
| }); | ||||
| 
 | ||||
| describe('DomainVersion', () => { | ||||
|   it('renders domain version', async () => { | ||||
|     await act(async () => { | ||||
| @ -118,11 +122,6 @@ describe('DomainVersion', () => { | ||||
|     expect(await screen.findByText('Domain component')).toBeInTheDocument(); | ||||
| 
 | ||||
|     // Check that the version timeline is displayed
 | ||||
|     expect( | ||||
|       screen.getByText('label.version-plural-history') | ||||
|     ).toBeInTheDocument(); | ||||
| 
 | ||||
|     // check active version visible
 | ||||
|     expect(screen.getByText('v0.4')).toBeInTheDocument(); | ||||
|     expect(screen.getByText('Version timeline')).toBeInTheDocument(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -13,101 +13,127 @@ | ||||
| 
 | ||||
| import { Col, Divider, Drawer, Row, Typography } from 'antd'; | ||||
| import classNames from 'classnames'; | ||||
| import { capitalize, isEmpty, toString } from 'lodash'; | ||||
| import React, { Fragment, useMemo, useState } from 'react'; | ||||
| import { isEmpty, toString } from 'lodash'; | ||||
| import React, { Fragment, useMemo } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| import { getUserPath } from '../../../constants/constants'; | ||||
| import { EntityHistory } from '../../../generated/type/entityHistory'; | ||||
| import { getUserByName } from '../../../rest/userAPI'; | ||||
| import { useUserProfile } from '../../../hooks/user-profile/useUserProfile'; | ||||
| import { formatDateTime } from '../../../utils/date-time/DateTimeUtils'; | ||||
| import { getEntityName } from '../../../utils/EntityUtils'; | ||||
| import { getSummary, isMajorVersion } from '../../../utils/EntityVersionUtils'; | ||||
| import UserPopOverCard from '../../common/PopOverCard/UserPopOverCard'; | ||||
| import CloseIcon from '../../Modals/CloseIcon.component'; | ||||
| import './entity-version-timeline.less'; | ||||
| import { | ||||
|   EntityVersionButtonProps, | ||||
|   EntityVersionTimelineProps, | ||||
| } from './EntityVersionTimeline.interface'; | ||||
| 
 | ||||
| type Props = { | ||||
|   versionList: EntityHistory; | ||||
|   currentVersion: string; | ||||
|   versionHandler: (v: string) => void; | ||||
|   onBack: () => void; | ||||
| export const VersionButton = ({ | ||||
|   version, | ||||
|   onVersionSelect, | ||||
|   selected, | ||||
|   isMajorVersion, | ||||
| }: EntityVersionButtonProps) => { | ||||
|   const { t } = useTranslation(); | ||||
| 
 | ||||
|   const { | ||||
|     updatedBy, | ||||
|     version: versionNumber, | ||||
|     changeDescription, | ||||
|     updatedAt, | ||||
|     glossary, | ||||
|   } = version; | ||||
|   const [, , user] = useUserProfile({ | ||||
|     permission: true, | ||||
|     name: updatedBy, | ||||
|   }); | ||||
| 
 | ||||
|   const versionText = `v${parseFloat(versionNumber).toFixed(1)}`; | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       className="timeline-content p-b-md cursor-pointer" | ||||
|       onClick={() => onVersionSelect(toString(versionNumber))}> | ||||
|       <div className="timeline-wrapper"> | ||||
|         <span | ||||
|           className={classNames( | ||||
|             'timeline-rounder', | ||||
|             { | ||||
|               selected, | ||||
|             }, | ||||
|             { | ||||
|               major: isMajorVersion, | ||||
|             } | ||||
|           )} | ||||
|           data-testid={`version-selector-${versionText}`} | ||||
|         /> | ||||
|         <span className={classNames('timeline-line')} /> | ||||
|       </div> | ||||
|       <div> | ||||
|         <Typography.Text | ||||
|           className={classNames('d-flex font-medium', { | ||||
|             'text-primary': selected, | ||||
|           })}> | ||||
|           <span>{versionText}</span> | ||||
|           {isMajorVersion ? ( | ||||
|             <span | ||||
|               className="m-l-xs text-xs font-medium text-grey-body tw-bg-tag p-x-xs p-y-xss bg-grey rounded-4" | ||||
|               style={{ backgroundColor: '#EEEAF8' }}> | ||||
|               {t('label.major')} | ||||
|             </span> | ||||
|           ) : null} | ||||
|         </Typography.Text> | ||||
|         <div | ||||
|           className={classNames('text-xs font-normal break-all', { | ||||
|             'diff-description': selected, | ||||
|           })}> | ||||
|           {getSummary({ | ||||
|             changeDescription: changeDescription, | ||||
|             isGlossaryTerm: !isEmpty(glossary), | ||||
|           })} | ||||
|         </div> | ||||
|         <div className="text-xs d-flex gap-1 items-center flex-wrap"> | ||||
|           <UserPopOverCard | ||||
|             className="font-italic" | ||||
|             profileWidth={16} | ||||
|             userName={updatedBy}> | ||||
|             <Link className="thread-author m-r-xss" to={getUserPath(updatedBy)}> | ||||
|               {getEntityName(user)} | ||||
|             </Link> | ||||
|           </UserPopOverCard> | ||||
|           <span className="font-medium font-italic version-timestamp"> | ||||
|             {formatDateTime(updatedAt)} | ||||
|           </span> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| type VersionType = 'all' | 'major' | 'minor'; | ||||
| 
 | ||||
| const EntityVersionTimeLine: React.FC<Props> = ({ | ||||
| const EntityVersionTimeLine: React.FC<EntityVersionTimelineProps> = ({ | ||||
|   versionList = {} as EntityHistory, | ||||
|   currentVersion, | ||||
|   versionHandler, | ||||
|   onBack, | ||||
| }: Props) => { | ||||
| }) => { | ||||
|   const { t } = useTranslation(); | ||||
|   const [versionType] = useState<VersionType>('all'); | ||||
|   const [uname, setUname] = useState<string>(''); | ||||
| 
 | ||||
|   const fetchUserName = async (userName: string) => { | ||||
|     try { | ||||
|       const userData = await getUserByName(userName); | ||||
| 
 | ||||
|       const name: string = getEntityName(userData); | ||||
|       setUname(name); | ||||
|     } catch (err) { | ||||
|       setUname(userName); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const versions = useMemo(() => { | ||||
|     let versionTypeList = []; | ||||
|     const list = versionList.versions ?? []; | ||||
| 
 | ||||
|     switch (versionType) { | ||||
|       case 'major': | ||||
|         versionTypeList = list.filter((v) => { | ||||
|   const versions = useMemo( | ||||
|     () => | ||||
|       versionList.versions.map((v, i) => { | ||||
|         const currV = JSON.parse(v); | ||||
| 
 | ||||
|         const majorVersionChecks = () => { | ||||
|           return isMajorVersion( | ||||
|             parseFloat(currV?.changeDescription?.previousVersion) | ||||
|               .toFixed(1) | ||||
|               .toString(), | ||||
|             parseFloat(currV?.version).toFixed(1).toString() | ||||
|           ); | ||||
|         }); | ||||
| 
 | ||||
|         break; | ||||
|       case 'minor': | ||||
|         versionTypeList = list.filter((v) => { | ||||
|           const currV = JSON.parse(v); | ||||
| 
 | ||||
|           return !isMajorVersion( | ||||
|             parseFloat(currV?.changeDescription?.previousVersion) | ||||
|               .toFixed(1) | ||||
|               .toString(), | ||||
|             parseFloat(currV?.version).toFixed(1).toString() | ||||
|           ); | ||||
|         }); | ||||
| 
 | ||||
|         break; | ||||
|       case 'all': | ||||
|       default: | ||||
|         versionTypeList = list; | ||||
| 
 | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     return versionTypeList.length ? ( | ||||
|       versionTypeList.map((v, i) => { | ||||
|         const currV = JSON.parse(v); | ||||
|         const userId: string = currV?.updatedBy; | ||||
|         fetchUserName(userId); | ||||
|         { | ||||
|           userId === 'admin' ? setUname('admin') : ' '; | ||||
|         } | ||||
|         const majorVersionChecks = () => { | ||||
|           return ( | ||||
|             isMajorVersion( | ||||
|               parseFloat(currV?.changeDescription?.previousVersion) | ||||
|                 .toFixed(1) | ||||
|                 .toString(), | ||||
|               parseFloat(currV?.version).toFixed(1).toString() | ||||
|             ) && versionType === 'all' | ||||
|           ); | ||||
|         }; | ||||
|         const versionText = `v${parseFloat(currV?.version).toFixed(1)}`; | ||||
| 
 | ||||
|         return ( | ||||
|           <Fragment key={currV.version}> | ||||
| @ -118,74 +144,17 @@ const EntityVersionTimeLine: React.FC<Props> = ({ | ||||
|                 </div> | ||||
|               </div> | ||||
|             ) : null} | ||||
|             <div | ||||
|               className="timeline-content p-b-md cursor-pointer" | ||||
|               onClick={() => versionHandler(toString(currV?.version))}> | ||||
|               <div className="timeline-wrapper"> | ||||
|                 <span | ||||
|                   className={classNames( | ||||
|                     'timeline-rounder', | ||||
|                     { | ||||
|                       selected: toString(currV?.version) === currentVersion, | ||||
|                     }, | ||||
|                     { | ||||
|                       major: majorVersionChecks(), | ||||
|                     } | ||||
|                   )} | ||||
|                   data-testid={`version-selector-${versionText}`} | ||||
|             <VersionButton | ||||
|               isMajorVersion={majorVersionChecks()} | ||||
|               selected={toString(currV.version) === currentVersion} | ||||
|               version={currV} | ||||
|               onVersionSelect={versionHandler} | ||||
|             /> | ||||
|                 <span className={classNames('timeline-line')} /> | ||||
|               </div> | ||||
|               <div> | ||||
|                 <Typography.Text | ||||
|                   className={classNames('d-flex font-medium', { | ||||
|                     'text-primary': toString(currV?.version) === currentVersion, | ||||
|                   })}> | ||||
|                   <span>{versionText}</span> | ||||
|                   {majorVersionChecks() ? ( | ||||
|                     <span | ||||
|                       className="m-l-xs text-xs font-medium text-grey-body tw-bg-tag p-x-xs p-y-xss bg-grey rounded-4" | ||||
|                       style={{ backgroundColor: '#EEEAF8' }}> | ||||
|                       {t('label.major')} | ||||
|                     </span> | ||||
|                   ) : null} | ||||
|                 </Typography.Text> | ||||
|                 <div | ||||
|                   className={classNames('text-xs font-normal break-all', { | ||||
|                     'diff-description': | ||||
|                       toString(currV?.version) === currentVersion, | ||||
|                   })}> | ||||
|                   {getSummary({ | ||||
|                     changeDescription: currV?.changeDescription, | ||||
|                     isGlossaryTerm: !isEmpty(currV?.glossary), | ||||
|                   })} | ||||
|                 </div> | ||||
|                 <p className="text-xs font-italic"> | ||||
|                   <span className="font-medium">{uname}</span> | ||||
|                   <span className="text-grey-muted"> | ||||
|                     {' '} | ||||
|                     {t('label.updated-on')}{' '} | ||||
|                   </span> | ||||
|                   <span className="font-medium"> | ||||
|                     {new Date(currV?.updatedAt).toLocaleDateString('en-CA', { | ||||
|                       hour: 'numeric', | ||||
|                       minute: 'numeric', | ||||
|                     })} | ||||
|                   </span> | ||||
|                 </p> | ||||
|               </div> | ||||
|             </div> | ||||
|           </Fragment> | ||||
|         ); | ||||
|       }) | ||||
|     ) : ( | ||||
|       <p className="text-grey-muted d-flex justify-center items-center"> | ||||
|         {t('message.no-version-type-available', { | ||||
|           type: capitalize(versionType), | ||||
|         })} | ||||
|       </p> | ||||
|       }), | ||||
|     [versionList, currentVersion, versionHandler] | ||||
|   ); | ||||
|   }, [versionList, currentVersion, versionHandler, versionType]); | ||||
| 
 | ||||
|   return ( | ||||
|     <Drawer | ||||
|  | ||||
| @ -0,0 +1,34 @@ | ||||
| /* | ||||
|  *  Copyright 2024 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 { ChangeDescription } from '../../../generated/entity/type'; | ||||
| import { EntityHistory } from '../../../generated/type/entityHistory'; | ||||
| 
 | ||||
| export type EntityVersionTimelineProps = { | ||||
|   versionList: EntityHistory; | ||||
|   currentVersion: string; | ||||
|   versionHandler: (v: string) => void; | ||||
|   onBack: () => void; | ||||
| }; | ||||
| 
 | ||||
| export type EntityVersionButtonProps = { | ||||
|   version: { | ||||
|     updatedBy: string; | ||||
|     version: string; | ||||
|     changeDescription: ChangeDescription; | ||||
|     updatedAt: number; | ||||
|     glossary: string; | ||||
|   }; | ||||
|   onVersionSelect: (v: string) => void; | ||||
|   selected: boolean; | ||||
|   isMajorVersion: boolean; | ||||
| }; | ||||
| @ -0,0 +1,108 @@ | ||||
| /* | ||||
|  *  Copyright 2024 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, screen } from '@testing-library/react'; | ||||
| import React from 'react'; | ||||
| import { ChangeDescription } from '../../../generated/entity/type'; | ||||
| import { VersionButton } from './EntityVersionTimeLine'; | ||||
| 
 | ||||
| jest.mock('../../common/PopOverCard/UserPopOverCard', () => ({ | ||||
|   __esModule: true, | ||||
|   default: ({ userName }: { userName: string }) => <p>{userName}</p>, | ||||
| })); | ||||
| 
 | ||||
| const user = { | ||||
|   name: 'John Doe displayName', | ||||
|   displayName: 'John Doe displayName', | ||||
| }; | ||||
| 
 | ||||
| jest.mock('../../../hooks/user-profile/useUserProfile', () => ({ | ||||
|   useUserProfile: jest.fn().mockImplementation(() => [false, false, user]), | ||||
| })); | ||||
| 
 | ||||
| jest.mock('../../../utils/EntityVersionUtils', () => ({ | ||||
|   __esModule: true, | ||||
|   getSummary: jest.fn().mockReturnValue('Some change description'), | ||||
|   isMajorVersion: jest.fn().mockReturnValue(false), | ||||
| })); | ||||
| 
 | ||||
| describe('VersionButton', () => { | ||||
|   const version = { | ||||
|     updatedBy: 'John Doe', | ||||
|     version: '1.0', | ||||
|     changeDescription: {} as ChangeDescription, | ||||
|     updatedAt: 123, | ||||
|     glossary: '', | ||||
|   }; | ||||
| 
 | ||||
|   const onVersionSelect = jest.fn(); | ||||
|   const selected = false; | ||||
|   const isMajorVersion = false; | ||||
| 
 | ||||
|   it('renders version number', () => { | ||||
|     render( | ||||
|       <VersionButton | ||||
|         isMajorVersion={isMajorVersion} | ||||
|         selected={selected} | ||||
|         version={version} | ||||
|         onVersionSelect={onVersionSelect} | ||||
|       /> | ||||
|     ); | ||||
|     const versionNumber = screen.getByText('v1.0'); | ||||
| 
 | ||||
|     expect(versionNumber).toBeInTheDocument(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders change description', () => { | ||||
|     render( | ||||
|       <VersionButton | ||||
|         isMajorVersion={isMajorVersion} | ||||
|         selected={selected} | ||||
|         version={version} | ||||
|         onVersionSelect={onVersionSelect} | ||||
|       /> | ||||
|     ); | ||||
|     const changeDescription = screen.getByText('Some change description'); | ||||
| 
 | ||||
|     expect(changeDescription).toBeInTheDocument(); | ||||
|   }); | ||||
| 
 | ||||
|   it('should render updatedBy with UserPopoverCard', async () => { | ||||
|     render( | ||||
|       <VersionButton | ||||
|         isMajorVersion={isMajorVersion} | ||||
|         selected={selected} | ||||
|         version={version} | ||||
|         onVersionSelect={onVersionSelect} | ||||
|       /> | ||||
|     ); | ||||
| 
 | ||||
|     const ownerDisplayName = await screen.findByText('John Doe'); | ||||
| 
 | ||||
|     expect(ownerDisplayName).toBeInTheDocument(); | ||||
|   }); | ||||
| 
 | ||||
|   it('calls onVersionSelect when clicked', () => { | ||||
|     render( | ||||
|       <VersionButton | ||||
|         isMajorVersion={isMajorVersion} | ||||
|         selected={selected} | ||||
|         version={version} | ||||
|         onVersionSelect={onVersionSelect} | ||||
|       /> | ||||
|     ); | ||||
|     const versionButton = screen.getByTestId('version-selector-v1.0'); | ||||
|     fireEvent.click(versionButton); | ||||
| 
 | ||||
|     expect(onVersionSelect).toHaveBeenCalledWith('1.0'); | ||||
|   }); | ||||
| }); | ||||
| @ -0,0 +1,17 @@ | ||||
| /* | ||||
|  *  Copyright 2024 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. | ||||
|  */ | ||||
| .version-timestamp::before { | ||||
|   content: '\2022'; | ||||
|   margin-right: 6px; | ||||
|   margin-left: 4px; | ||||
| } | ||||
| @ -64,6 +64,10 @@ jest.mock('../../PageLayoutV1/PageLayoutV1', () => { | ||||
|   return jest.fn().mockImplementation(({ children }) => <div>{children}</div>); | ||||
| }); | ||||
| 
 | ||||
| jest.mock('../../Entity/EntityVersionTimeLine/EntityVersionTimeLine', () => { | ||||
|   return jest.fn().mockReturnValue(<>Version timeline</>); | ||||
| }); | ||||
| 
 | ||||
| describe('GlossaryVersion', () => { | ||||
|   it('renders glossary version', async () => { | ||||
|     const glossaryName = '305b0130-b9c1-4441-a0fc-6463fd019540'; | ||||
| @ -82,11 +86,6 @@ describe('GlossaryVersion', () => { | ||||
|     expect(await screen.findByText('Glossary component')).toBeInTheDocument(); | ||||
| 
 | ||||
|     // Check that the version timeline is displayed
 | ||||
|     expect( | ||||
|       screen.getByText('label.version-plural-history') | ||||
|     ).toBeInTheDocument(); | ||||
| 
 | ||||
|     // check active version visible
 | ||||
|     expect(screen.getByText('v0.1')).toBeInTheDocument(); | ||||
|     expect(screen.getByText('Version timeline')).toBeInTheDocument(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -269,9 +269,7 @@ const UserPopOverCard: FC<Props> = ({ | ||||
|               : getUserPath(userName ?? '') | ||||
|           }> | ||||
|           {showUserProfile ? profilePicture : null} | ||||
|           {showUserName ? ( | ||||
|             <span className="">{displayName ?? userName ?? ''}</span> | ||||
|           ) : null} | ||||
|           {showUserName ? <span>{displayName ?? userName}</span> : null} | ||||
|         </Link> | ||||
|       )} | ||||
|     </Popover> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Chirag Madlani
						Chirag Madlani