mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-12 07:48:14 +00:00
User and applications suggestions for entities (#15345)
* add metapilot app oss side config * add metapilot app oss side config * suggestions changes * locales * pushing progress * localisation * add suggestions count button * locales * fix tests * add tests * fix sonar issues * fix tests and cleanup * fix unit tests
This commit is contained in:
parent
7805a0b609
commit
14f280bd06
@ -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');
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 = () => {
|
||||
<PermissionProvider>
|
||||
<WebSocketProvider>
|
||||
<GlobalSearchProvider>
|
||||
<DomainProvider>
|
||||
<EntityExportModalProvider>
|
||||
<AppRouter />
|
||||
</EntityExportModalProvider>
|
||||
</DomainProvider>
|
||||
<ApplicationsProvider>
|
||||
<DomainProvider>
|
||||
<EntityExportModalProvider>
|
||||
<AppRouter />
|
||||
</EntityExportModalProvider>
|
||||
</DomainProvider>
|
||||
</ApplicationsProvider>
|
||||
</GlobalSearchProvider>
|
||||
</WebSocketProvider>
|
||||
</PermissionProvider>
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
<svg viewBox="0 0 31 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.3979 30C7.12698 30 0.398438 23.2711 0.398438 15.0002C0.398438 6.72933 7.12698 0 15.3979 0C23.6688 0 30.3984 6.72917 30.3984 15.0002C30.3984 23.2713 23.6693 30 15.3979 30Z" fill="#0950C5"/>
|
||||
<path d="M15.0182 8.96743V6.69274C14.5804 6.53726 16.2117 6.53824 15.7731 6.69405V8.96743C16.6561 8.96743 18.0137 10.3968 18.0137 11.945H12.7793C12.7793 10.3968 13.9073 8.96743 15.0182 8.96743Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.186 23.0629C23.4765 23.0629 24.7781 21.7618 24.7781 18.4699V13.974C24.7781 10.6821 23.4767 9.3811 20.186 9.3811H10.6123C7.32162 9.3811 6.02197 10.6821 6.02197 13.974V18.4699C6.02197 21.7618 7.32162 23.0629 10.6123 23.0629H16.654V25.4997L19.0896 23.0629H20.186Z" fill="white"/>
|
||||
<path d="M20.1872 9.38159H19.2131C22.5038 9.38159 23.8051 10.6825 23.8051 13.9745V18.4703C23.8051 21.7623 22.5036 23.0634 19.2131 23.0634H20.1872C23.4778 23.0634 24.7793 21.7623 24.7793 18.4703V13.9745C24.7793 10.6825 23.4777 9.38159 20.1872 9.38159Z" fill="#CAE7FF"/>
|
||||
<path d="M23.8176 13.4954C25.0211 13.66 25.8974 13.6886 25.8974 16.06C25.8974 18.4314 25.1007 18.3953 23.9835 18.5972L23.8176 13.4954Z" fill="#CAE7FF"/>
|
||||
<path d="M6.97723 13.4954C5.77341 13.66 4.89746 13.6886 4.89746 16.06C4.89746 18.4314 5.69399 18.3953 6.81116 18.5972L6.97723 13.4954Z" fill="white"/>
|
||||
<path d="M11.0264 19.8942C9.0023 19.8942 8.32983 19.2207 8.32983 17.1957V15.2469C8.32983 13.2221 9.0023 12.5493 11.0264 12.5493H19.7656C21.775 12.5493 22.4373 13.1975 22.4615 15.2015C22.4615 15.2166 22.4615 17.1956 22.4615 17.1956C22.4615 19.2205 21.7901 19.894 19.7654 19.894H11.0263L11.0264 19.8942Z" fill="#CAE7FF"/>
|
||||
<path d="M17.7087 16.2222C17.7087 15.6791 18.15 15.2397 18.6914 15.2397C19.2327 15.2397 19.6753 15.6793 19.6753 16.2222C19.6753 16.7651 19.2357 17.2046 18.6914 17.2046C18.147 17.2046 17.7087 16.7661 17.7087 16.2222Z" fill="#0950C5"/>
|
||||
<path d="M11.1255 16.2224C11.1255 15.6793 11.5648 15.24 12.1071 15.24C12.6495 15.24 13.0897 15.6795 13.0897 16.2224C13.0897 16.7654 12.6501 17.2049 12.1071 17.2049C11.5641 17.2049 11.1255 16.7664 11.1255 16.2224Z" fill="#0950C5"/>
|
||||
<path d="M16.5282 5.62996C16.5282 5.00575 16.0229 4.50024 15.3993 4.50024C14.7758 4.50024 14.2705 5.00575 14.2705 5.62996C14.2705 6.25416 14.7759 6.7595 15.3993 6.7595C16.0227 6.7595 16.5282 6.25383 16.5282 5.62996Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
@ -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 (
|
||||
<SuggestionsProvider>
|
||||
<Component {...props} />
|
||||
</SuggestionsProvider>
|
||||
);
|
||||
};
|
||||
@ -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<T extends unknown>(
|
||||
Component: FC<T>
|
||||
) {
|
||||
return function DefaultFallback(
|
||||
props: JSX.IntrinsicAttributes & { children?: React.ReactNode } & T
|
||||
) {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
@ -11,10 +11,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button } from 'antd';
|
||||
import { t } from 'i18next';
|
||||
import { lowerCase } from 'lodash';
|
||||
import React, { Fragment, FunctionComponent, useState } from 'react';
|
||||
import React, { Fragment, FunctionComponent, useMemo, useState } from 'react';
|
||||
import EntityLink from '../../../utils/EntityLink';
|
||||
import Searchbar from '../../common/SearchBarComponent/SearchBar.component';
|
||||
import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider';
|
||||
import SchemaTable from '../SchemaTable/SchemaTable.component';
|
||||
import { Props } from './SchemaTab.interfaces';
|
||||
|
||||
@ -31,23 +34,38 @@ const SchemaTab: FunctionComponent<Props> = ({
|
||||
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 (
|
||||
<Fragment>
|
||||
<div className="w-1/2">
|
||||
<Searchbar
|
||||
removeMargin
|
||||
placeholder={`${t('message.find-in-table')}`}
|
||||
searchValue={searchText}
|
||||
typingInterval={500}
|
||||
onSearch={handleSearchAction}
|
||||
/>
|
||||
<div className="d-flex items-center justify-between">
|
||||
<div className="w-1/2">
|
||||
<Searchbar
|
||||
removeMargin
|
||||
placeholder={`${t('message.find-in-table')}`}
|
||||
searchValue={searchText}
|
||||
typingInterval={500}
|
||||
onSearch={handleSearchAction}
|
||||
/>
|
||||
</div>
|
||||
{columnSuggestions.length > 0 && (
|
||||
<Button className="suggestion-pending-btn">
|
||||
{columnSuggestions.length} {t('label.suggestion-pending')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SchemaTable
|
||||
columnName={columnName}
|
||||
entityFqn={entityFqn}
|
||||
|
||||
@ -12,13 +12,18 @@
|
||||
*/
|
||||
|
||||
import { Button, Space, Tooltip } from 'antd';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
|
||||
import { DE_ACTIVE_COLOR, ICON_DIMENSION } from '../../../constants/constants';
|
||||
import { EntityField } from '../../../constants/Feeds.constants';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import EntityTasks from '../../../pages/TasksPage/EntityTasks/EntityTasks.component';
|
||||
import EntityLink from '../../../utils/EntityLink';
|
||||
import { getEntityFeedLink } from '../../../utils/EntityUtils';
|
||||
import RichTextEditorPreviewer from '../../common/RichTextEditor/RichTextEditorPreviewer';
|
||||
import SuggestionsAlert from '../../Suggestions/SuggestionsAlert/SuggestionsAlert';
|
||||
import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider';
|
||||
import { TableDescriptionProps } from './TableDescription.interface';
|
||||
|
||||
const TableDescription = ({
|
||||
@ -32,23 +37,62 @@ const TableDescription = ({
|
||||
onThreadLinkSelect,
|
||||
}: TableDescriptionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedUserSuggestions = [] } = useSuggestionsContext();
|
||||
|
||||
return (
|
||||
<Space
|
||||
className="hover-icon-group"
|
||||
data-testid="description"
|
||||
direction="vertical"
|
||||
id={`field-description-${index}`}>
|
||||
{columnData.field ? (
|
||||
<RichTextEditorPreviewer markdown={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 (
|
||||
<SuggestionsAlert
|
||||
hasEditAccess={hasEditPermission}
|
||||
maxLength={40}
|
||||
suggestion={activeSuggestion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [hasEditPermission, entityLink, selectedUserSuggestions]);
|
||||
|
||||
const descriptionContent = useMemo(() => {
|
||||
if (suggestionData) {
|
||||
return suggestionData;
|
||||
} else if (columnData.field) {
|
||||
return <RichTextEditorPreviewer markdown={columnData.field} />;
|
||||
} else {
|
||||
return (
|
||||
<span className="text-grey-muted">
|
||||
{t('label.no-entity', {
|
||||
entity: t('label.description'),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{!isReadOnly ? (
|
||||
);
|
||||
}
|
||||
}, [columnData, suggestionData]);
|
||||
|
||||
return (
|
||||
<Space
|
||||
className="hover-icon-group w-full"
|
||||
data-testid="description"
|
||||
direction="vertical"
|
||||
id={`field-description-${index}`}>
|
||||
{descriptionContent}
|
||||
|
||||
{!suggestionData && !isReadOnly ? (
|
||||
<Space align="baseline" size="middle">
|
||||
{hasEditPermission && (
|
||||
<Tooltip
|
||||
|
||||
@ -40,7 +40,7 @@ import { ReactComponent as IconExternalLink } from '../../../../assets/svg/exter
|
||||
import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg';
|
||||
import { ReactComponent as IconRestore } from '../../../../assets/svg/ic-restore.svg';
|
||||
import { ReactComponent as IconDropdown } from '../../../../assets/svg/menu.svg';
|
||||
import { APP_UI_SCHEMA } from '../../../../constants/Applications.constant';
|
||||
|
||||
import { DE_ACTIVE_COLOR } from '../../../../constants/constants';
|
||||
import { GlobalSettingOptions } from '../../../../constants/GlobalSettings.constants';
|
||||
import { ServiceCategory } from '../../../../enums/service.enum';
|
||||
@ -93,6 +93,7 @@ const AppDetails = () => {
|
||||
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}
|
||||
|
||||
@ -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', () => ({
|
||||
|
||||
@ -15,6 +15,9 @@ class ApplicationSchemaClassBase {
|
||||
public importSchema(fqn: string) {
|
||||
return import(`../../../../utils/ApplicationSchemas/${fqn}.json`);
|
||||
}
|
||||
public getJSONUISchema() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const applicationSchemaClassBase = new ApplicationSchemaClassBase();
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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<App[]>([]);
|
||||
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 (
|
||||
<ApplicationsContext.Provider value={appContext}>
|
||||
{children}
|
||||
</ApplicationsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useApplicationsProvider = () => useContext(ApplicationsContext);
|
||||
|
||||
export default ApplicationsProvider;
|
||||
@ -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;
|
||||
}
|
||||
@ -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(() => <p>ProfilePicture</p>);
|
||||
});
|
||||
|
||||
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(
|
||||
<SuggestionsProvider>
|
||||
<SuggestionsAlert hasEditAccess={false} suggestion={mockSuggestion} />
|
||||
</SuggestionsProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Test suggestion/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Test User/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders alert with access', () => {
|
||||
render(
|
||||
<SuggestionsProvider>
|
||||
<SuggestionsAlert hasEditAccess suggestion={mockSuggestion} />
|
||||
</SuggestionsProvider>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@ -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 (
|
||||
<Space
|
||||
className="schema-description d-flex"
|
||||
data-testid="asset-description-container"
|
||||
direction="vertical"
|
||||
size={12}>
|
||||
<Card className="suggested-description-card">
|
||||
<div className="d-flex m-b-xs justify-between">
|
||||
<div className="d-flex items-center">
|
||||
<UserPopOverCard userName={userName}>
|
||||
<span className="m-r-xs">
|
||||
<ProfilePicture name={userName} width="28" />
|
||||
</span>
|
||||
</UserPopOverCard>
|
||||
|
||||
<Typography.Text className="m-b-0 font-medium">
|
||||
{`${userName} ${t('label.suggested-description')}`}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<RichTextEditorPreviewer
|
||||
markdown={suggestion.description ?? ''}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
{hasEditAccess && (
|
||||
<div className="d-flex justify-end p-t-xss gap-2">
|
||||
<Button
|
||||
ghost
|
||||
data-testid="reject-suggestion"
|
||||
icon={<CloseOutlined />}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
acceptRejectSuggestion(suggestion, SuggestionAction.Reject)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
data-testid="accept-suggestion"
|
||||
icon={<CheckOutlined />}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
acceptRejectSuggestion(suggestion, SuggestionAction.Accept)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsAlert;
|
||||
@ -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;
|
||||
}
|
||||
@ -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<string, Suggestion[]>;
|
||||
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',
|
||||
}
|
||||
@ -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<EntityReference>();
|
||||
const [allSuggestionsUsers, setAllSuggestionsUsers] = useState<
|
||||
EntityReference[]
|
||||
>([]);
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
|
||||
const [suggestionsByUser, setSuggestionsByUser] = useState<
|
||||
Map<string, Suggestion[]>
|
||||
>(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<string, Suggestion[]>);
|
||||
|
||||
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 (
|
||||
<SuggestionsContext.Provider value={suggestionsContextObj}>
|
||||
{children}
|
||||
</SuggestionsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useSuggestionsContext = () => useContext(SuggestionsContext);
|
||||
|
||||
export default SuggestionsProvider;
|
||||
@ -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 (
|
||||
<div className="d-flex items-center gap-2">
|
||||
<Typography.Text className="right-panel-label">
|
||||
{t('label.suggested-description-plural')}
|
||||
</Typography.Text>
|
||||
<AvatarCarousel />
|
||||
{selectedUserSuggestions.length > 0 && (
|
||||
<>
|
||||
<Button size="small" type="primary">
|
||||
<Typography.Text className="text-xs text-white">
|
||||
{t('label.accept-all')}
|
||||
</Typography.Text>
|
||||
</Button>
|
||||
<Button ghost size="small" type="primary">
|
||||
<Typography.Text className="text-xs text-primary">
|
||||
{t('label.reject-all')}
|
||||
</Typography.Text>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionsSlider;
|
||||
@ -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 }) => (
|
||||
<span data-testid="mocked-profile-picture">{name}</span>
|
||||
))
|
||||
);
|
||||
|
||||
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(<AvatarCarousel />);
|
||||
|
||||
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(<AvatarCarousel />);
|
||||
const nextButton = screen.getByTestId('next-slide');
|
||||
fireEvent.click(nextButton);
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
expect(nextButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@ -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 (
|
||||
<div className="avatar-carousel-container d-flex items-center">
|
||||
<Button
|
||||
className="carousel-arrow"
|
||||
data-testid="prev-slide"
|
||||
disabled={avatarList.length <= 1 || currentSlide <= 0}
|
||||
icon={<LeftOutlined />}
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={prevSlide}
|
||||
/>
|
||||
<Carousel
|
||||
afterChange={(current) => setCurrentSlide(current)}
|
||||
dots={false}
|
||||
slidesToShow={avatarList.length < 3 ? avatarList.length : 3}>
|
||||
{avatarList.map((avatar, index) => (
|
||||
<UserPopOverCard
|
||||
className=""
|
||||
key={avatar.id}
|
||||
userName={avatar?.name ?? ''}>
|
||||
<Button
|
||||
className={`p-0 m-r-xss avatar-item ${
|
||||
currentSlide === index ? 'active' : ''
|
||||
}`}
|
||||
shape="circle"
|
||||
onClick={() => setCurrentSlide(index)}>
|
||||
<ProfilePicture name={avatar.name ?? ''} width="30" />
|
||||
</Button>
|
||||
</UserPopOverCard>
|
||||
))}
|
||||
</Carousel>
|
||||
<Button
|
||||
className="carousel-arrow"
|
||||
data-testid="next-slide"
|
||||
disabled={
|
||||
avatarList.length <= 1 || currentSlide === avatarList.length - 1
|
||||
}
|
||||
icon={<RightOutlined />}
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={nextSlide}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarCarousel;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 (
|
||||
<SuggestionsAlert
|
||||
hasEditAccess={hasEditAccess}
|
||||
suggestion={activeSuggestion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [hasEditAccess, entityLinkWithoutField, selectedUserSuggestions]);
|
||||
|
||||
const descriptionContent = useMemo(() => {
|
||||
if (suggestionData) {
|
||||
return suggestionData;
|
||||
} else {
|
||||
return description.trim() ? (
|
||||
<RichTextEditorPreviewer
|
||||
className={reduceDescription ? 'max-two-lines' : ''}
|
||||
enableSeeMoreVariant={!removeBlur}
|
||||
markdown={description}
|
||||
/>
|
||||
) : (
|
||||
<span>{t('label.no-description')}</span>
|
||||
);
|
||||
}
|
||||
}, [description, suggestionData]);
|
||||
|
||||
const content = (
|
||||
<Space
|
||||
className={classNames('schema-description d-flex', className)}
|
||||
data-testid="asset-description-container"
|
||||
direction="vertical"
|
||||
size={16}>
|
||||
<Space size="middle">
|
||||
<Text className="right-panel-label">{t('label.description')}</Text>
|
||||
{showActions && actionButtons}
|
||||
</Space>
|
||||
<div className="d-flex justify-between">
|
||||
<div className="d-flex items-center gap-2">
|
||||
<Text className="right-panel-label">{t('label.description')}</Text>
|
||||
{showActions && actionButtons}
|
||||
</div>
|
||||
{suggestions.length > 0 && <SuggestionsSlider />}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{description.trim() ? (
|
||||
<RichTextEditorPreviewer
|
||||
className={reduceDescription ? 'max-two-lines' : ''}
|
||||
enableSeeMoreVariant={!removeBlur}
|
||||
markdown={description}
|
||||
/>
|
||||
) : (
|
||||
<span>{t('label.no-description')}</span>
|
||||
)}
|
||||
{descriptionContent}
|
||||
<ModalWithMarkdownEditor
|
||||
header={t('label.edit-description-for', { entityName })}
|
||||
placeholder={t('label.enter-entity', {
|
||||
|
||||
@ -21,5 +21,3 @@ export const STEPS_FOR_APP_INSTALL: Array<StepperStepType> = [
|
||||
{ name: t('label.configure'), step: 2 },
|
||||
{ name: t('label.schedule'), step: 3 },
|
||||
];
|
||||
|
||||
export const APP_UI_SCHEMA = { metaPilotAppType: { 'ui:widget': 'hidden' } };
|
||||
|
||||
47
openmetadata-ui/src/main/resources/ui/src/hooks/usePubSub.ts
Normal file
47
openmetadata-ui/src/main/resources/ui/src/hooks/usePubSub.ts
Normal file
@ -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<T = any> = (data: T) => void;
|
||||
type UnsubscribeFunction = () => void;
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
export const useSub = <T = any>(
|
||||
event: string,
|
||||
callback: EventCallback<T>,
|
||||
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 <T = any>(event: string, data: T) => {
|
||||
emitter.emit(event, data);
|
||||
};
|
||||
};
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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é",
|
||||
|
||||
@ -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": "סיכום",
|
||||
|
||||
@ -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": "サマリ",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": "Сводка",
|
||||
|
||||
@ -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": "概要",
|
||||
|
||||
@ -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<RJSFSchema>();
|
||||
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}
|
||||
|
||||
@ -51,6 +51,7 @@ jest.mock(
|
||||
'../../components/Settings/Applications/AppDetails/ApplicationSchemaClassBase',
|
||||
() => ({
|
||||
importSchema: jest.fn().mockResolvedValue({}),
|
||||
getJSONUISchema: jest.fn().mockReturnValue({}),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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<PagingResponse<Suggestion[]>>(BASE_URL, {
|
||||
params,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const updateSuggestionStatus = (
|
||||
data: Suggestion,
|
||||
action: SuggestionAction
|
||||
): Promise<AxiosResponse> => {
|
||||
const url = `${BASE_URL}/${data.id}/${action}`;
|
||||
|
||||
return APIClient.put(url, {});
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user