diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Login.ts b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Login.ts new file mode 100644 index 00000000000..44112e5ef53 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Login.ts @@ -0,0 +1,48 @@ +/* + * 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 { interceptURL } from '../common'; + +/** + * Try Performing login with the given username and password. + * Particularly used for testing login. + * + * @param {string} username - The username for login + * @param {string} password - The password for login + * @return {void} + */ +export const performLogin = (username, password) => { + cy.visit('/'); + interceptURL('POST', '/api/v1/users/login', 'loginUser'); + cy.get('[id="email"]').should('be.visible').clear().type(username); + cy.get('[id="password"]').should('be.visible').clear().type(password); + + // Don't want to show any popup in the tests + cy.setCookie(`STAR_OMD_USER_${username.split('@')[0]}`, 'true'); + + // Get version and set cookie to hide version banner + cy.request({ + method: 'GET', + url: `api/v1/system/version`, + }).then((res) => { + const version = res.body.version; + const versionCookie = `VERSION_${version + .split('-')[0] + .replaceAll('.', '_')}`; + + cy.setCookie(versionCookie, 'true'); + window.localStorage.setItem('loggedInUsers', username.split('@')[0]); + }); + + cy.get('.ant-btn').contains('Login').should('be.visible').click(); + cy.wait('@loginUser'); +}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddRoleAndAssignToUser.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddRoleAndAssignToUser.spec.ts index 88a88468f6f..ca15d9760c7 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddRoleAndAssignToUser.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddRoleAndAssignToUser.spec.ts @@ -62,7 +62,7 @@ describe( // Verify added description cy.get( - '[data-testid="description"] > [data-testid="viewer-container"]' + '[data-testid="asset-description-container"] [data-testid="viewer-container"]' ).should('contain', 'description'); }); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Explore.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Explore.spec.ts index 474a568038d..20e5ed6f6f0 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Explore.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Explore.spec.ts @@ -13,7 +13,7 @@ import { interceptURL, verifyResponseStatusCode } from '../../common/common'; import { SidebarItem } from '../../constants/Entity.interface'; -describe('Explore Page', { tags: 'DataAssets' }, () => { +describe.skip('Explore Page', { tags: 'DataAssets' }, () => { before(() => { cy.login(); }); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Login.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Login.spec.ts index 5b710cdf3c7..f92acd31a99 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Login.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Login.spec.ts @@ -12,6 +12,7 @@ */ import { interceptURL, verifyResponseStatusCode } from '../../common/common'; +import { performLogin } from '../../common/Utils/Login'; import { BASE_URL, LOGIN_ERROR_MESSAGE } from '../../constants/constants'; const CREDENTIALS = { @@ -27,12 +28,14 @@ const invalidPassword = 'testUsers@123'; describe('Login flow should work properly', { tags: 'Settings' }, () => { after(() => { cy.login(); - const token = localStorage.getItem('oidcIdToken'); - cy.request({ - method: 'DELETE', - url: `/api/v1/users/${CREDENTIALS.id}?hardDelete=true&recursive=false`, - headers: { Authorization: `Bearer ${token}` }, + cy.getAllLocalStorage().then((data) => { + const token = Object.values(data)[0].oidcIdToken; + cy.request({ + method: 'DELETE', + url: `/api/v1/users/${CREDENTIALS.id}?hardDelete=true&recursive=false`, + headers: { Authorization: `Bearer ${token}` }, + }); }); }); @@ -72,7 +75,7 @@ describe('Login flow should work properly', { tags: 'Settings' }, () => { // Login with the created user - cy.login(CREDENTIALS.email, CREDENTIALS.password); + performLogin(CREDENTIALS.email, CREDENTIALS.password); cy.url().should('eq', `${BASE_URL}/my-data`); // Verify user profile @@ -100,14 +103,14 @@ describe('Login flow should work properly', { tags: 'Settings' }, () => { it('Signin using invalid credentials', () => { // Login with invalid email - cy.login(invalidEmail, CREDENTIALS.password); + performLogin(invalidEmail, CREDENTIALS.password); cy.get('[data-testid="login-error-container"]') .should('be.visible') .invoke('text') .should('eq', LOGIN_ERROR_MESSAGE); // Login with invalid password - cy.login(CREDENTIALS.email, invalidPassword); + performLogin(CREDENTIALS.email, invalidPassword); cy.get('[data-testid="login-error-container"]') .should('be.visible') .invoke('text') diff --git a/openmetadata-ui/src/main/resources/ui/cypress/tsconfig.json b/openmetadata-ui/src/main/resources/ui/cypress/tsconfig.json index bc1710a0aac..396fd27f32d 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/tsconfig.json +++ b/openmetadata-ui/src/main/resources/ui/cypress/tsconfig.json @@ -4,7 +4,7 @@ "downlevelIteration": true, "target": "ES5", "lib": ["dom", "dom.iterable", "ES2020.Promise", "es2021"], - "types": ["cypress", "node"] + "types": ["cypress", "node", "@cypress/grep"] }, "include": ["./**/*.ts"] } diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json index c0be71736dc..5c6aedc9fbc 100644 --- a/openmetadata-ui/src/main/resources/ui/package.json +++ b/openmetadata-ui/src/main/resources/ui/package.json @@ -84,6 +84,7 @@ "crypto-random-string-with-promisify-polyfill": "^5.0.0", "dagre": "^0.8.5", "diff": "^5.0.0", + "eventemitter3": "^5.0.1", "fast-json-patch": "^3.1.1", "history": "4.5.1", "html-react-parser": "^1.4.14", diff --git a/openmetadata-ui/src/main/resources/ui/src/App.tsx b/openmetadata-ui/src/main/resources/ui/src/App.tsx index d7abea1deac..4fb86f83643 100644 --- a/openmetadata-ui/src/main/resources/ui/src/App.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/App.tsx @@ -22,6 +22,7 @@ import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider'; import ErrorBoundary from './components/common/ErrorBoundary/ErrorBoundary'; import DomainProvider from './components/Domain/DomainProvider/DomainProvider'; import { EntityExportModalProvider } from './components/Entity/EntityExportModalProvider/EntityExportModalProvider.component'; +import ApplicationsProvider from './components/Settings/Applications/ApplicationsProvider/ApplicationsProvider'; import WebAnalyticsProvider from './components/WebAnalytics/WebAnalyticsProvider'; import { TOAST_OPTIONS } from './constants/Toasts.constants'; import ApplicationConfigProvider from './context/ApplicationConfigProvider/ApplicationConfigProvider'; @@ -49,11 +50,13 @@ const App: FC = () => { - - - - - + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/MetaPilotApplication.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/MetaPilotApplication.svg deleted file mode 100644 index ad5a5f7d519..00000000000 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/MetaPilotApplication.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/withSuggestions.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/withSuggestions.tsx new file mode 100644 index 00000000000..e0bb19062ab --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/withSuggestions.tsx @@ -0,0 +1,24 @@ +/* + * 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 React, { FC } from 'react'; +import SuggestionsProvider from '../Suggestions/SuggestionsProvider/SuggestionsProvider'; + +export const withSuggestions = + (Component: FC) => + (props: JSX.IntrinsicAttributes & { children?: React.ReactNode }) => { + return ( + + + + ); + }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/withSuspenseFallback.js b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/withSuspenseFallback.tsx similarity index 77% rename from openmetadata-ui/src/main/resources/ui/src/components/AppRouter/withSuspenseFallback.js rename to openmetadata-ui/src/main/resources/ui/src/components/AppRouter/withSuspenseFallback.tsx index 5c69f2be408..4fdd5d3b1e5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/withSuspenseFallback.js +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/withSuspenseFallback.tsx @@ -11,11 +11,15 @@ * limitations under the License. */ -import React, { Suspense } from 'react'; +import React, { FC, Suspense } from 'react'; import Loader from '../common/Loader/Loader'; -export default function withSuspenseFallback(Component) { - return function DefaultFallback(props) { +export default function withSuspenseFallback( + Component: FC +) { + return function DefaultFallback( + props: JSX.IntrinsicAttributes & { children?: React.ReactNode } & T + ) { return ( = ({ tableConstraints, }: Props) => { const [searchText, setSearchText] = useState(''); + const { selectedUserSuggestions } = useSuggestionsContext(); const handleSearchAction = (searchValue: string) => { setSearchText(searchValue); }; + const columnSuggestions = useMemo( + () => + selectedUserSuggestions?.filter( + (item) => EntityLink.getTableColumnName(item.entityLink) !== undefined + ) ?? [], + [selectedUserSuggestions] + ); + return ( -
- +
+
+ +
+ {columnSuggestions.length > 0 && ( + + )}
- { const { t } = useTranslation(); + const { selectedUserSuggestions = [] } = useSuggestionsContext(); - return ( - - {columnData.field ? ( - - ) : ( + const entityLink = useMemo( + () => + entityType === EntityType.TABLE + ? EntityLink.getTableEntityLink( + entityFqn, + columnData.record?.name ?? '' + ) + : getEntityFeedLink(entityType, columnData.fqn), + [entityType, entityFqn] + ); + + const suggestionData = useMemo(() => { + const activeSuggestion = selectedUserSuggestions.find( + (suggestion) => suggestion.entityLink === entityLink + ); + + if (activeSuggestion?.entityLink === entityLink) { + return ( + + ); + } + + return null; + }, [hasEditPermission, entityLink, selectedUserSuggestions]); + + const descriptionContent = useMemo(() => { + if (suggestionData) { + return suggestionData; + } else if (columnData.field) { + return ; + } else { + return ( {t('label.no-entity', { entity: t('label.description'), })} - )} - {!isReadOnly ? ( + ); + } + }, [columnData, suggestionData]); + + return ( + + {descriptionContent} + + {!suggestionData && !isReadOnly ? ( {hasEditPermission && ( { isRunLoading: false, isSaveLoading: false, }); + const UiSchema = applicationSchemaClassBase.getJSONUISchema(); const fetchAppDetails = useCallback(async () => { setLoadingState((prev) => ({ ...prev, isFetchLoading: true })); @@ -345,7 +346,7 @@ const AppDetails = () => { okText={t('label.submit')} schema={jsonSchema} serviceCategory={ServiceCategory.DASHBOARD_SERVICES} - uiSchema={APP_UI_SCHEMA} + uiSchema={UiSchema} validator={validator} onCancel={noop} onSubmit={onConfigSave} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.test.tsx index e6bb8397fbc..6095faf1c70 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/AppDetails.test.tsx @@ -140,6 +140,7 @@ jest.mock('../AppSchedule/AppSchedule.component', () => jest.mock('./ApplicationSchemaClassBase', () => ({ importSchema: jest.fn().mockReturnValue({ default: ['table'] }), + getJSONUISchema: jest.fn().mockReturnValue({}), })); jest.mock('react-router-dom', () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationSchemaClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationSchemaClassBase.ts index fbd8e469682..efba29edf5b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationSchemaClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppDetails/ApplicationSchemaClassBase.ts @@ -15,6 +15,9 @@ class ApplicationSchemaClassBase { public importSchema(fqn: string) { return import(`../../../../utils/ApplicationSchemas/${fqn}.json`); } + public getJSONUISchema() { + return {}; + } } const applicationSchemaClassBase = new ApplicationSchemaClassBase(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.interface.ts new file mode 100644 index 00000000000..2e21220e2b2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.interface.ts @@ -0,0 +1,18 @@ +/* + * 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 { App } from '../../../../generated/entity/applications/app'; + +export type ApplicationsContextType = { + applications: App[]; + loading: boolean; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.tsx new file mode 100644 index 00000000000..16ca60af4cc --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/ApplicationsProvider/ApplicationsProvider.tsx @@ -0,0 +1,69 @@ +/* + * 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 { isEmpty } from 'lodash'; +import React, { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { usePermissionProvider } from '../../../../context/PermissionProvider/PermissionProvider'; +import { App } from '../../../../generated/entity/applications/app'; +import { getApplicationList } from '../../../../rest/applicationAPI'; +import { ApplicationsContextType } from './ApplicationsProvider.interface'; + +export const ApplicationsContext = createContext({} as ApplicationsContextType); + +export const ApplicationsProvider = ({ children }: { children: ReactNode }) => { + const [applications, setApplications] = useState([]); + const [loading, setLoading] = useState(false); + const { permissions } = usePermissionProvider(); + + const fetchApplicationList = useCallback(async () => { + try { + setLoading(true); + const { data } = await getApplicationList({ + limit: 100, + }); + + setApplications(data); + } catch (err) { + // do not handle error + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + if (!isEmpty(permissions)) { + fetchApplicationList(); + } + }, [permissions]); + + const appContext = useMemo(() => { + return { applications, loading }; + }, [applications, loading]); + + return ( + + {children} + + ); +}; + +export const useApplicationsProvider = () => useContext(ApplicationsContext); + +export default ApplicationsProvider; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/SuggestionsAlert.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/SuggestionsAlert.interface.ts new file mode 100644 index 00000000000..189c4a40a86 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/SuggestionsAlert.interface.ts @@ -0,0 +1,19 @@ +/* + * 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 { Suggestion } from '../../../generated/entity/feed/suggestion'; + +export interface SuggestionsAlertProps { + suggestion: Suggestion; + hasEditAccess?: boolean; + maxLength?: number; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/SuggestionsAlert.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/SuggestionsAlert.test.tsx new file mode 100644 index 00000000000..dec5df75847 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/SuggestionsAlert.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Suggestion } from '../../../generated/entity/feed/suggestion'; +import SuggestionsProvider from '../SuggestionsProvider/SuggestionsProvider'; +import SuggestionsAlert from './SuggestionsAlert'; + +jest.mock('../../../hooks/useFqn', () => ({ + useFqn: jest.fn().mockImplementation(() => ({ fqn: 'testFqn' })), +})); + +jest.mock('../../common/ProfilePicture/ProfilePicture', () => { + return jest.fn().mockImplementation(() =>

ProfilePicture

); +}); + +jest.mock('../SuggestionsProvider/SuggestionsProvider', () => ({ + useSuggestionsContext: jest.fn().mockImplementation(() => ({ + suggestions: [ + { + id: '1', + description: 'Test suggestion', + createdBy: { id: '1', name: 'Test User', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, + ], + acceptRejectSuggestion: jest.fn(), + })), + __esModule: true, + default: 'SuggestionsProvider', +})); + +describe('SuggestionsAlert', () => { + const mockSuggestion: Suggestion = { + id: '1', + description: 'Test suggestion', + createdBy: { id: '1', name: 'Test User', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }; + + it('renders alert without access', () => { + render( + + + + ); + + expect(screen.getByText(/Test suggestion/i)).toBeInTheDocument(); + expect(screen.getByText(/Test User/i)).toBeInTheDocument(); + }); + + it('renders alert with access', () => { + render( + + + + ); + + expect(screen.getByText(/Test suggestion/i)).toBeInTheDocument(); + expect(screen.getByText(/Test User/i)).toBeInTheDocument(); + expect(screen.getByTestId('reject-suggestion')).toBeInTheDocument(); + expect(screen.getByTestId('accept-suggestion')).toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/SuggestionsAlert.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/SuggestionsAlert.tsx new file mode 100644 index 00000000000..407573c35da --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/SuggestionsAlert.tsx @@ -0,0 +1,90 @@ +/* + * 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 { CheckOutlined, CloseOutlined } from '@ant-design/icons'; +import { Button, Card, Space, Typography } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import UserPopOverCard from '../../common/PopOverCard/UserPopOverCard'; +import ProfilePicture from '../../common/ProfilePicture/ProfilePicture'; +import RichTextEditorPreviewer from '../../common/RichTextEditor/RichTextEditorPreviewer'; +import { useSuggestionsContext } from '../SuggestionsProvider/SuggestionsProvider'; +import { SuggestionAction } from '../SuggestionsProvider/SuggestionsProvider.interface'; +import './suggestions-alert.less'; +import { SuggestionsAlertProps } from './SuggestionsAlert.interface'; + +const SuggestionsAlert = ({ + suggestion, + hasEditAccess = false, + maxLength, +}: SuggestionsAlertProps) => { + const { t } = useTranslation(); + const { acceptRejectSuggestion } = useSuggestionsContext(); + const userName = suggestion?.createdBy?.name ?? ''; + + if (!suggestion) { + return null; + } + + return ( + + +
+
+ + + + + + + + {`${userName} ${t('label.suggested-description')}`} + +
+
+ + {hasEditAccess && ( +
+
+ )} +
+
+ ); +}; + +export default SuggestionsAlert; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/suggestions-alert.less b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/suggestions-alert.less new file mode 100644 index 00000000000..f8703de801c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsAlert/suggestions-alert.less @@ -0,0 +1,30 @@ +/* + * 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 url('../../../styles/variables.less'); + +.suggested-description-card { + border-color: @primary-color !important; + border-radius: 10px !important; + .markdown-parser .toastui-editor-contents { + p { + color: @text-grey-muted; + } + } +} + +.suggestion-pending-btn { + background: #ffbe0e0d !important; + border: 1px solid @yellow-2 !important; + color: @yellow-2 !important; + border-radius: 8px !important; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts new file mode 100644 index 00000000000..dcf6d4fd56a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface.ts @@ -0,0 +1,33 @@ +/* + * 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 { Suggestion } from '../../../generated/entity/feed/suggestion'; +import { EntityReference } from '../../../generated/entity/type'; + +export interface SuggestionsContextType { + selectedUserSuggestions: Suggestion[]; + suggestions: Suggestion[]; + suggestionsByUser: Map; + loading: boolean; + allSuggestionsUsers: EntityReference[]; + onUpdateActiveUser: (user: EntityReference) => void; + fetchSuggestions: (entityFqn: string) => void; + acceptRejectSuggestion: ( + suggestion: Suggestion, + action: SuggestionAction + ) => void; +} + +export enum SuggestionAction { + Accept = 'accept', + Reject = 'reject', +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx new file mode 100644 index 00000000000..6357f429bdc --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsProvider/SuggestionsProvider.tsx @@ -0,0 +1,164 @@ +/* + * 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 { AxiosError } from 'axios'; +import { isEmpty, isEqual, uniqWith } from 'lodash'; + +import React, { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useTranslation } from 'react-i18next'; +import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; +import { Suggestion } from '../../../generated/entity/feed/suggestion'; +import { EntityReference } from '../../../generated/entity/type'; +import { useFqn } from '../../../hooks/useFqn'; +import { usePub } from '../../../hooks/usePubSub'; +import { + getSuggestionsList, + updateSuggestionStatus, +} from '../../../rest/suggestionsAPI'; +import { showErrorToast } from '../../../utils/ToastUtils'; +import { + SuggestionAction, + SuggestionsContextType, +} from './SuggestionsProvider.interface'; + +export const SuggestionsContext = createContext({} as SuggestionsContextType); + +const SuggestionsProvider = ({ children }: { children?: ReactNode }) => { + const { t } = useTranslation(); + const { fqn: entityFqn } = useFqn(); + const [activeUser, setActiveUser] = useState(); + const [allSuggestionsUsers, setAllSuggestionsUsers] = useState< + EntityReference[] + >([]); + const [suggestions, setSuggestions] = useState([]); + const [suggestionsByUser, setSuggestionsByUser] = useState< + Map + >(new Map()); + const publish = usePub(); + + const [loading, setLoading] = useState(false); + const refreshEntity = useRef<(suggestion: Suggestion) => void>(); + const { permissions } = usePermissionProvider(); + + const fetchSuggestions = useCallback(async (entityFQN: string) => { + setLoading(true); + try { + const { data } = await getSuggestionsList({ + entityFQN, + }); + setSuggestions(data); + + const allUsersData = data.map( + (suggestion) => suggestion.createdBy as EntityReference + ); + const uniqueUsers = uniqWith(allUsersData, isEqual); + setAllSuggestionsUsers(uniqueUsers); + + const groupedSuggestions = data.reduce((acc, suggestion) => { + const createdBy = suggestion?.createdBy?.name ?? ''; + if (!acc.has(createdBy)) { + acc.set(createdBy, []); + } + acc.get(createdBy)?.push(suggestion); + + return acc; + }, new Map() as Map); + + setSuggestionsByUser(groupedSuggestions); + } catch (err) { + showErrorToast( + err as AxiosError, + t('server.entity-fetch-error', { + entity: t('label.lineage-data-lowercase'), + }) + ); + } finally { + setLoading(false); + } + }, []); + + const acceptRejectSuggestion = useCallback( + async (suggestion: Suggestion, status: SuggestionAction) => { + try { + await updateSuggestionStatus(suggestion, status); + await fetchSuggestions(entityFqn); + if (status === SuggestionAction.Accept) { + // call component refresh function + publish('updateDetails', suggestion); + } + } catch (err) { + showErrorToast(err as AxiosError); + } + }, + [entityFqn, refreshEntity] + ); + + const onUpdateActiveUser = useCallback( + (user: EntityReference) => { + setActiveUser(user); + }, + [suggestionsByUser] + ); + + const selectedUserSuggestions = useMemo(() => { + return suggestionsByUser.get(activeUser?.name ?? '') ?? []; + }, [activeUser, suggestionsByUser]); + + useEffect(() => { + if (!isEmpty(permissions) && !isEmpty(entityFqn)) { + fetchSuggestions(entityFqn); + } + }, [entityFqn, permissions]); + + const suggestionsContextObj = useMemo(() => { + return { + suggestions, + suggestionsByUser, + selectedUserSuggestions, + entityFqn, + loading, + allSuggestionsUsers, + onUpdateActiveUser, + fetchSuggestions, + acceptRejectSuggestion, + }; + }, [ + suggestions, + suggestionsByUser, + selectedUserSuggestions, + entityFqn, + loading, + allSuggestionsUsers, + onUpdateActiveUser, + fetchSuggestions, + acceptRejectSuggestion, + ]); + + return ( + + {children} + + ); +}; + +export const useSuggestionsContext = () => useContext(SuggestionsContext); + +export default SuggestionsProvider; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.tsx new file mode 100644 index 00000000000..cae06424948 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Suggestions/SuggestionsSlider/SuggestionsSlider.tsx @@ -0,0 +1,46 @@ +/* + * 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 { Button, Typography } from 'antd'; +import { t } from 'i18next'; +import React from 'react'; +import AvatarCarousel from '../../common/AvatarCarousel/AvatarCarousel'; +import { useSuggestionsContext } from '../SuggestionsProvider/SuggestionsProvider'; + +const SuggestionsSlider = () => { + const { selectedUserSuggestions } = useSuggestionsContext(); + + return ( +
+ + {t('label.suggested-description-plural')} + + + {selectedUserSuggestions.length > 0 && ( + <> + + + + )} +
+ ); +}; + +export default SuggestionsSlider; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.test.tsx new file mode 100644 index 00000000000..8c74fd23c21 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 AvatarCarousel from './AvatarCarousel'; + +jest.mock('../../Suggestions/SuggestionsProvider/SuggestionsProvider', () => ({ + useSuggestionsContext: jest.fn().mockImplementation(() => ({ + suggestions: [ + { + id: '1', + description: 'Test suggestion', + createdBy: { id: '1', name: 'Avatar 1', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, + { + id: '2', + description: 'Test suggestion', + createdBy: { id: '2', name: 'Avatar 2', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, + ], + allSuggestionsUsers: [ + { id: '1', name: 'Avatar 1', type: 'user' }, + { id: '2', name: 'Avatar 2', type: 'user' }, + ], + acceptRejectSuggestion: jest.fn(), + onUpdateActiveUser: jest.fn(), + })), + __esModule: true, + default: 'SuggestionsProvider', +})); + +jest.mock('../ProfilePicture/ProfilePicture', () => + jest + .fn() + .mockImplementation(({ name }) => ( + {name} + )) +); + +jest.mock('../../../rest/suggestionsAPI', () => ({ + getSuggestionsList: jest.fn().mockImplementation(() => + Promise.resolve([ + { + id: '1', + description: 'Test suggestion', + createdBy: { id: '1', name: 'Avatar 1', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, + { + id: '2', + description: 'Test suggestion', + createdBy: { id: '1', name: 'Avatar 2', type: 'user' }, + entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>', + }, + ]) + ), +})); + +describe('AvatarCarousel', () => { + it('renders without crashing', () => { + render(); + + expect(screen.getByText(/Avatar 1/i)).toBeInTheDocument(); + expect(screen.getByText(/Avatar 2/i)).toBeInTheDocument(); + expect(screen.getByTestId('prev-slide')).toBeDisabled(); + }); + + it('disables the next button when on the last slide', () => { + render(); + const nextButton = screen.getByTestId('next-slide'); + fireEvent.click(nextButton); + fireEvent.click(nextButton); + + expect(nextButton).toBeDisabled(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.tsx new file mode 100644 index 00000000000..07f232764bf --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/AvatarCarousel.tsx @@ -0,0 +1,92 @@ +/* + * 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 { LeftOutlined, RightOutlined } from '@ant-design/icons'; +import { Button, Carousel } from 'antd'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider'; +import UserPopOverCard from '../PopOverCard/UserPopOverCard'; +import ProfilePicture from '../ProfilePicture/ProfilePicture'; +import './avatar-carousel.less'; + +const AvatarCarousel = () => { + const { allSuggestionsUsers: avatarList, onUpdateActiveUser } = + useSuggestionsContext(); + const [currentSlide, setCurrentSlide] = useState(-1); + + const prevSlide = useCallback(() => { + setCurrentSlide((prev) => (prev === 0 ? avatarList.length - 1 : prev - 1)); + }, [avatarList]); + + const nextSlide = useCallback(() => { + setCurrentSlide((prev) => (prev === avatarList.length - 1 ? 0 : prev + 1)); + }, [avatarList]); + + const onProfileClick = useCallback( + (index: number) => { + const activeUser = avatarList[index]; + onUpdateActiveUser(activeUser); + }, + [avatarList] + ); + + useEffect(() => { + onProfileClick(currentSlide); + }, [currentSlide]); + + return ( +
+ + + ))} + +
+ ); +}; + +export default AvatarCarousel; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/avatar-carousel.less b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/avatar-carousel.less new file mode 100644 index 00000000000..ff03595271c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AvatarCarousel/avatar-carousel.less @@ -0,0 +1,32 @@ +/* + * 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 url('../../../styles/variables.less'); + +.avatar-item { + opacity: 0.4; + &.active { + opacity: 1; + } + &:hover { + border-color: @border-color !important; + } + &:focus { + border-color: @border-color !important; + } +} + +.avatar-carousel-container { + .slick-slide { + width: 32px !important; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx index 0bf8e34842d..1407bc638d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityDescription/DescriptionV1.tsx @@ -31,7 +31,11 @@ import { TASK_ENTITIES, } from '../../../utils/TasksUtils'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; +import SuggestionsAlert from '../../Suggestions/SuggestionsAlert/SuggestionsAlert'; +import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider'; +import SuggestionsSlider from '../../Suggestions/SuggestionsSlider/SuggestionsSlider'; import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer'; + const { Text } = Typography; interface Props { @@ -76,6 +80,9 @@ const DescriptionV1 = ({ reduceDescription, }: Props) => { const history = useHistory(); + const { suggestions = [], selectedUserSuggestions = [] } = + useSuggestionsContext(); + const handleRequestDescription = useCallback(() => { history.push( getRequestDescriptionPath(entityType as string, entityFqn as string) @@ -88,8 +95,18 @@ const DescriptionV1 = ({ ); }, [entityType, entityFqn]); - const entityLink = useMemo(() => { - return getEntityFeedLink(entityType, entityFqn, EntityField.DESCRIPTION); + const { entityLink, entityLinkWithoutField } = useMemo(() => { + const entityLink = getEntityFeedLink( + entityType, + entityFqn, + EntityField.DESCRIPTION + ); + const entityLinkWithoutField = getEntityFeedLink(entityType, entityFqn); + + return { + entityLink, + entityLinkWithoutField, + }; }, [entityType, entityFqn]); const taskActionButton = useMemo(() => { @@ -170,26 +187,55 @@ const DescriptionV1 = ({ ] ); + const suggestionData = useMemo(() => { + const activeSuggestion = selectedUserSuggestions.find( + (suggestion) => suggestion.entityLink === entityLinkWithoutField + ); + + if (activeSuggestion?.entityLink === entityLinkWithoutField) { + return ( + + ); + } + + return null; + }, [hasEditAccess, entityLinkWithoutField, selectedUserSuggestions]); + + const descriptionContent = useMemo(() => { + if (suggestionData) { + return suggestionData; + } else { + return description.trim() ? ( + + ) : ( + {t('label.no-description')} + ); + } + }, [description, suggestionData]); + const content = ( - - {t('label.description')} - {showActions && actionButtons} - +
+
+ {t('label.description')} + {showActions && actionButtons} +
+ {suggestions.length > 0 && } +
+
- {description.trim() ? ( - - ) : ( - {t('label.no-description')} - )} + {descriptionContent} = [ { name: t('label.configure'), step: 2 }, { name: t('label.schedule'), step: 3 }, ]; - -export const APP_UI_SCHEMA = { metaPilotAppType: { 'ui:widget': 'hidden' } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/usePubSub.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/usePubSub.ts new file mode 100644 index 00000000000..a7a4fff5b9e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/usePubSub.ts @@ -0,0 +1,47 @@ +/* + * 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 { EventEmitter } from 'eventemitter3'; +import { DependencyList, useEffect } from 'react'; + +type EventCallback = (data: T) => void; +type UnsubscribeFunction = () => void; + +const emitter = new EventEmitter(); + +export const useSub = ( + event: string, + callback: EventCallback, + dependencies?: DependencyList +): UnsubscribeFunction => { + const unsubscribe = () => { + emitter.off(event, callback); + }; + + useEffect(() => { + emitter.on(event, callback); + + // If dependencies are provided, remove the callback when the component unmounts + return () => { + emitter.off(event, callback); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies ?? []); + + return unsubscribe; +}; + +export const usePub = () => { + return (event: string, data: T) => { + emitter.emit(event, data); + }; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 3487c87ce90..196796ec670 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -2,6 +2,7 @@ "label": { "aborted": "Abgebrochen", "accept": "Akzeptieren", + "accept-all": "Accept All", "accept-suggestion": "Vorschlag akzeptieren", "access": "Zugriff", "access-block-time": "Access block time", @@ -869,6 +870,7 @@ "region-name": "Region Name", "registry": "Register", "reject": "Ablehnen", + "reject-all": "Reject All", "rejected": "Rejected", "related-term-plural": "Verwandte Begriffe", "relevance": "Relevanz", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "Erfolgreich hochgeladen", "suggest": "Vorschlagen", "suggest-entity": "{{entity}} vorschlagen", + "suggested-description": "Suggested Description", "suggested-description-plural": "Suggested Descriptions", "suggestion": "Vorschlag", "suggestion-lowercase-plural": "Vorschläge", + "suggestion-pending": "Suggestions Pending", "suite": "Suite", "sum": "Summe", "summary": "Zusammenfassung", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 12aedd13e0c..5ff84f732e5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -2,6 +2,7 @@ "label": { "aborted": "Aborted", "accept": "Accept", + "accept-all": "Accept All", "accept-suggestion": "Accept Suggestion", "access": "Access", "access-block-time": "Access block time", @@ -869,6 +870,7 @@ "region-name": "Region Name", "registry": "Registry", "reject": "Reject", + "reject-all": "Reject All", "rejected": "Rejected", "related-term-plural": "Related Terms", "relevance": "Relevance", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "Successfully Uploaded", "suggest": "Suggest", "suggest-entity": "Suggest {{entity}}", + "suggested-description": "Suggested Description", "suggested-description-plural": "Suggested Descriptions", "suggestion": "Suggestion", "suggestion-lowercase-plural": "suggestions", + "suggestion-pending": "Suggestions Pending", "suite": "Suite", "sum": "Sum", "summary": "Summary", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 234c7ed69e0..bc1c0c26881 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -2,6 +2,7 @@ "label": { "aborted": "Abortado", "accept": "Aceptar", + "accept-all": "Accept All", "accept-suggestion": "Aceptar sugerencia", "access": "Acceso", "access-block-time": "Tiempo de Bloqueo de Acceso", @@ -869,6 +870,7 @@ "region-name": "Nombre de la región", "registry": "Registro", "reject": "Rechazar", + "reject-all": "Reject All", "rejected": "Rechazado", "related-term-plural": "Términos relacionados", "relevance": "Relevancia", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "Cargado Exitosamente", "suggest": "Sugerir", "suggest-entity": "Sugerir {{entity}}", + "suggested-description": "Suggested Description", "suggested-description-plural": "Descripciones Sugeridas", "suggestion": "Sugerencia", "suggestion-lowercase-plural": "sugerencias", + "suggestion-pending": "Suggestions Pending", "suite": "Suite", "sum": "Suma", "summary": "Resumen", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 5f6e4b05c41..00a9ec2c176 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -2,6 +2,7 @@ "label": { "aborted": "Interrompu", "accept": "Accepter", + "accept-all": "Accept All", "accept-suggestion": "Accepter la Suggestion", "access": "Accès", "access-block-time": "Access block time", @@ -869,6 +870,7 @@ "region-name": "Nom de Région", "registry": "Registre", "reject": "Rejeter", + "reject-all": "Reject All", "rejected": "Rejected", "related-term-plural": "Termes Liés", "relevance": "Pertinence", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "Téléchargé avec succès", "suggest": "Suggérer", "suggest-entity": "Suggérer {{entity}}", + "suggested-description": "Suggested Description", "suggested-description-plural": "Suggested Descriptions", "suggestion": "Suggestion", "suggestion-lowercase-plural": "suggestions", + "suggestion-pending": "Suggestions Pending", "suite": "Ensemble", "sum": "Somme", "summary": "Résumé", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index e0bca98c5ff..7602db04239 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -2,6 +2,7 @@ "label": { "aborted": "בוטל", "accept": "קבל", + "accept-all": "Accept All", "accept-suggestion": "קבל הצעה", "access": "גישה", "access-block-time": "זמן חסימת גישה", @@ -869,6 +870,7 @@ "region-name": "שם האזור", "registry": "רשומון", "reject": "דחה", + "reject-all": "Reject All", "rejected": "נדחה", "related-term-plural": "מונחים קשורים", "relevance": "רלוונטיות", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "הועלה בהצלחה", "suggest": "הצע", "suggest-entity": "הצע {{entity}}", + "suggested-description": "Suggested Description", "suggested-description-plural": "Suggested Descriptions", "suggestion": "הצעה", "suggestion-lowercase-plural": "הצעות", + "suggestion-pending": "Suggestions Pending", "suite": "יחידת בדיקה", "sum": "סכום", "summary": "סיכום", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 1f187b25a92..18ece770195 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -2,6 +2,7 @@ "label": { "aborted": "中断", "accept": "Accept", + "accept-all": "Accept All", "accept-suggestion": "提案を受け入れる", "access": "アクセス", "access-block-time": "Access block time", @@ -869,6 +870,7 @@ "region-name": "リージョン名", "registry": "レジストリ", "reject": "Reject", + "reject-all": "Reject All", "rejected": "Rejected", "related-term-plural": "関連する用語", "relevance": "Relevance", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "アップロード成功", "suggest": "提案", "suggest-entity": "{{entity}}を提案", + "suggested-description": "Suggested Description", "suggested-description-plural": "Suggested Descriptions", "suggestion": "提案", "suggestion-lowercase-plural": "提案", + "suggestion-pending": "Suggestions Pending", "suite": "スイート", "sum": "合計", "summary": "サマリ", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 5b1e53899a5..f9b64547ce7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -2,6 +2,7 @@ "label": { "aborted": "Afgebroken", "accept": "Accepteren", + "accept-all": "Accept All", "accept-suggestion": "Suggestie Accepteren", "access": "Toegang", "access-block-time": "Blokkeertijd Toegang", @@ -869,6 +870,7 @@ "region-name": "Regionaam", "registry": "Register", "reject": "Weigeren", + "reject-all": "Reject All", "rejected": "Geweigerd", "related-term-plural": "Gerelateerde termen", "relevance": "Relevantie", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "Succesvol geüpload", "suggest": "Suggestie", "suggest-entity": "Suggereer {{entity}}", + "suggested-description": "Suggested Description", "suggested-description-plural": "Voorgestelde beschrijvingen", "suggestion": "Suggestie", "suggestion-lowercase-plural": "suggesties", + "suggestion-pending": "Suggestions Pending", "suite": "Suite", "sum": "Som", "summary": "Samenvatting", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index da1c11557f9..dfb5585d542 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -2,6 +2,7 @@ "label": { "aborted": "Abortado", "accept": "Aceitar", + "accept-all": "Accept All", "accept-suggestion": "Aceitar Sugestão", "access": "Acesso", "access-block-time": "Tempo de bloqueio de acesso", @@ -869,6 +870,7 @@ "region-name": "Nome da Região", "registry": "Registro", "reject": "Rejeitar", + "reject-all": "Reject All", "rejected": "Rejeitado", "related-term-plural": "Termos Relacionados", "relevance": "Relevância", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "Carregado com Sucesso", "suggest": "Sugerir", "suggest-entity": "Sugerir {{entity}}", + "suggested-description": "Suggested Description", "suggested-description-plural": "Suggested Descriptions", "suggestion": "Sugestão", "suggestion-lowercase-plural": "sugestões", + "suggestion-pending": "Suggestions Pending", "suite": "Conjuto de Testes", "sum": "Soma", "summary": "Resumo", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 439fbe58f3e..beab4604a2c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -2,6 +2,7 @@ "label": { "aborted": "Прервано", "accept": "Принять", + "accept-all": "Accept All", "accept-suggestion": "Согласовать предложение", "access": "Доступ", "access-block-time": "Access block time", @@ -869,6 +870,7 @@ "region-name": "Наименование региона", "registry": "Реестр", "reject": "Reject", + "reject-all": "Reject All", "rejected": "Rejected", "related-term-plural": "Связанные термины", "relevance": "Актуальность", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "Успешно загружено", "suggest": "Предложить", "suggest-entity": "Предложить {{entity}}", + "suggested-description": "Suggested Description", "suggested-description-plural": "Suggested Descriptions", "suggestion": "Предложение", "suggestion-lowercase-plural": "предложения", + "suggestion-pending": "Suggestions Pending", "suite": "Набор", "sum": "Сумма", "summary": "Сводка", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index f4d3278a4d7..a8e4787174c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -2,6 +2,7 @@ "label": { "aborted": "已中止", "accept": "接受", + "accept-all": "Accept All", "accept-suggestion": "接受建议", "access": "访问", "access-block-time": "Access block time", @@ -869,6 +870,7 @@ "region-name": "区域名称", "registry": "仓库", "reject": "Reject", + "reject-all": "Reject All", "rejected": "Rejected", "related-term-plural": "关联术语", "relevance": "相关性", @@ -1051,9 +1053,11 @@ "successfully-uploaded": "上传成功", "suggest": "建议", "suggest-entity": "建议{{entity}}", + "suggested-description": "Suggested Description", "suggested-description-plural": "Suggested Descriptions", "suggestion": "建议", "suggestion-lowercase-plural": "建议", + "suggestion-pending": "Suggestions Pending", "suite": "套件", "sum": "总和", "summary": "概要", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx index 9ddd73704d2..3327a6e19a2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.component.tsx @@ -26,10 +26,7 @@ import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import applicationSchemaClassBase from '../../components/Settings/Applications/AppDetails/ApplicationSchemaClassBase'; import AppInstallVerifyCard from '../../components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component'; import IngestionStepper from '../../components/Settings/Services/Ingestion/IngestionStepper/IngestionStepper.component'; -import { - APP_UI_SCHEMA, - STEPS_FOR_APP_INSTALL, -} from '../../constants/Applications.constant'; +import { STEPS_FOR_APP_INSTALL } from '../../constants/Applications.constant'; import { GlobalSettingOptions } from '../../constants/GlobalSettings.constants'; import { ServiceCategory } from '../../enums/service.enum'; import { AppType } from '../../generated/entity/applications/app'; @@ -61,6 +58,7 @@ const AppInstall = () => { const [activeServiceStep, setActiveServiceStep] = useState(1); const [appConfiguration, setAppConfiguration] = useState(); const [jsonSchema, setJsonSchema] = useState(); + const UiSchema = applicationSchemaClassBase.getJSONUISchema(); const stepperList = useMemo( () => @@ -177,7 +175,7 @@ const AppInstall = () => { okText={t('label.submit')} schema={jsonSchema} serviceCategory={ServiceCategory.DASHBOARD_SERVICES} - uiSchema={APP_UI_SCHEMA} + uiSchema={UiSchema} validator={validator} onCancel={() => setActiveServiceStep(1)} onSubmit={onSaveConfiguration} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx index 19025a30f08..9b2bd5a768c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AppInstall/AppInstall.test.tsx @@ -51,6 +51,7 @@ jest.mock( '../../components/Settings/Applications/AppDetails/ApplicationSchemaClassBase', () => ({ importSchema: jest.fn().mockResolvedValue({}), + getJSONUISchema: jest.fn().mockReturnValue({}), }) ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx index 9b62b0dbc0f..80be9e4b90d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx @@ -43,6 +43,10 @@ jest.mock('../../rest/tableAPI', () => ({ restoreTable: jest.fn(), })); +jest.mock('../../rest/suggestionsAPI', () => ({ + getSuggestionsList: jest.fn().mockImplementation(() => Promise.resolve([])), +})); + jest.mock('../../utils/CommonUtils', () => ({ getFeedCounts: jest.fn(), getPartialNameFromTableFQN: jest.fn().mockImplementation(() => 'fqn'), @@ -157,6 +161,25 @@ jest.mock( }) ); +jest.mock( + '../../components/Suggestions/SuggestionsProvider/SuggestionsProvider', + () => ({ + useSuggestionsContext: jest.fn().mockImplementation(() => ({ + suggestions: [], + suggestionsByUser: new Map(), + selectedUserSuggestions: [], + entityFqn: 'fqn', + loading: false, + allSuggestionsUsers: [], + onUpdateActiveUser: jest.fn(), + fetchSuggestions: jest.fn(), + acceptRejectSuggestion: jest.fn(), + })), + __esModule: true, + default: 'SuggestionsProvider', + }) +); + jest.mock('react-router-dom', () => ({ useParams: jest .fn() diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 34cf15db075..b6a44d69af3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -23,6 +23,7 @@ import { useActivityFeedProvider } from '../../components/ActivityFeed/ActivityF import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../components/AppRouter/withActivityFeed'; +import { withSuggestions } from '../../components/AppRouter/withSuggestions'; import { useAuthContext } from '../../components/Auth/AuthProviders/AuthProvider'; import { CustomPropertyTable } from '../../components/common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; @@ -65,9 +66,11 @@ import { import { CreateThread } from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { JoinedWith, Table } from '../../generated/entity/data/table'; +import { Suggestion } from '../../generated/entity/feed/suggestion'; import { ThreadType } from '../../generated/entity/feed/thread'; import { TagLabel } from '../../generated/type/tagLabel'; import { useFqn } from '../../hooks/useFqn'; +import { useSub } from '../../hooks/usePubSub'; import { FeedCounts } from '../../interface/feed.interface'; import { postThread } from '../../rest/feedsAPI'; import { getQueriesList } from '../../rest/queryAPI'; @@ -87,6 +90,7 @@ import { sortTagsCaseInsensitive, } from '../../utils/CommonUtils'; import { defaultFields } from '../../utils/DatasetDetailsUtils'; +import EntityLink from '../../utils/EntityLink'; import { getEntityName } from '../../utils/EntityUtils'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils'; @@ -96,7 +100,7 @@ import { FrequentlyJoinedTables } from './FrequentlyJoinedTables/FrequentlyJoine import './table-details-page-v1.less'; import TableConstraints from './TableConstraints/TableConstraints'; -const TableDetailsPageV1 = () => { +const TableDetailsPageV1: React.FC = () => { const { isTourOpen, activeTabForTourDatasetPage, isTourPage } = useTourProvider(); const { currentUser } = useAuthContext(); @@ -897,6 +901,49 @@ const TableDetailsPageV1 = () => { })); }, []); + const updateDescriptionFromSuggestions = useCallback( + (suggestion: Suggestion) => { + setTableDetails((prev) => { + if (!prev) { + return; + } + + const activeCol = prev?.columns.find((column) => { + return ( + EntityLink.getTableEntityLink( + prev.fullyQualifiedName ?? '', + column.name ?? '' + ) === suggestion.entityLink + ); + }); + + if (!activeCol) { + return { + ...prev, + description: suggestion.description, + }; + } else { + const updatedColumns = prev.columns.map((column) => { + if (column.fullyQualifiedName === activeCol.fullyQualifiedName) { + return { + ...column, + description: suggestion.description, + }; + } else { + return column; + } + }); + + return { + ...prev, + columns: updatedColumns, + }; + } + }); + }, + [] + ); + useEffect(() => { if (isTourOpen || isTourPage) { setTableDetails(mockDatasetData.tableDetails as unknown as Table); @@ -912,6 +959,14 @@ const TableDetailsPageV1 = () => { } }, [tableDetails?.fullyQualifiedName]); + useSub( + 'updateDetails', + (suggestion: Suggestion) => { + updateDescriptionFromSuggestions(suggestion); + }, + [tableDetails] + ); + const onThreadPanelClose = () => { setThreadLink(''); }; @@ -1014,4 +1069,4 @@ const TableDetailsPageV1 = () => { ); }; -export default withActivityFeed(TableDetailsPageV1); +export default withSuggestions(withActivityFeed(TableDetailsPageV1)); diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/suggestionsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/suggestionsAPI.ts new file mode 100644 index 00000000000..b8992491dbf --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/rest/suggestionsAPI.ts @@ -0,0 +1,41 @@ +/* + * 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 { AxiosResponse } from 'axios'; +import { PagingResponse } from 'Models'; +import { SuggestionAction } from '../components/Suggestions/SuggestionsProvider/SuggestionsProvider.interface'; +import { Suggestion } from '../generated/entity/feed/suggestion'; +import { ListParams } from '../interface/API.interface'; +import APIClient from './index'; + +const BASE_URL = '/suggestions'; + +export type ListSuggestionsParams = ListParams & { + entityFQN?: string; +}; + +export const getSuggestionsList = async (params?: ListSuggestionsParams) => { + const response = await APIClient.get>(BASE_URL, { + params, + }); + + return response.data; +}; + +export const updateSuggestionStatus = ( + data: Suggestion, + action: SuggestionAction +): Promise => { + const url = `${BASE_URL}/${data.id}/${action}`; + + return APIClient.put(url, {}); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationSchemas/MetaPilotApplication.json b/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationSchemas/MetaPilotApplication.json deleted file mode 100644 index ea86671af3b..00000000000 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ApplicationSchemas/MetaPilotApplication.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "$id": "https://open-metadata.org/schema/entity/applications/configuration/external/metaPilotAppConfig.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MetaPilotAppConfig", - "description": "Configuration for the MetaPilot External Application.", - "type": "object", - "javaType": "org.openmetadata.schema.entity.app.external.MetaPilotAppConfig", - "definitions": { - "metaPilotAppType": { - "description": "Application type.", - "type": "string", - "enum": ["MetaPilot"], - "default": "MetaPilot" - }, - "serviceDatabases": { - "title": "Service Databases", - "description": "Choose the service and its databases you want to generate descriptions from.", - "type": "object", - "properties": { - "service": { - "title": "Service Name", - "placeholder": "Search Service", - "description": "Service Name to get descriptions from.", - "type": "string", - "format": "autoComplete", - "autoCompleteType": "database_service_search_index", - "additionalProperties": true - }, - "databases": { - "title": "Databases", - "description": "List of database names from the Service to get descriptions from.", - "type": "array", - "additionalProperties": true, - "items": { - "placeholder": "Search Databases", - "type": "string", - "format": "autoComplete", - "autoCompleteType": "database_search_index", - "additionalProperties": true - } - } - }, - "additionalProperties": false, - "required": ["service", "databases"] - } - }, - "properties": { - "type": { - "title": "Application Type", - "description": "Application Type", - "$ref": "#/definitions/metaPilotAppType", - "default": "MetaPilot" - }, - "serviceDatabases": { - "title": "Service Databases", - "description": "Services and Databases configured to get the descriptions from.", - "type": "array", - "items": { - "$ref": "#/definitions/serviceDatabases" - } - } - }, - "additionalProperties": false -} diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock index 339cce48887..f0dbc8ee4c5 100644 --- a/openmetadata-ui/src/main/resources/ui/yarn.lock +++ b/openmetadata-ui/src/main/resources/ui/yarn.lock @@ -7793,6 +7793,11 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^4.0.4: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"