mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-16 18:15:17 +00:00
Suggestions feedback (#15723)
* use entity store for storing fqn * fix feedbacks * fix tests * store cleanup * cleanup * review comments * minor label change * minor change * fix flaky lineage cypress
This commit is contained in:
parent
884029f031
commit
e22b8f7741
@ -82,6 +82,30 @@ const deleteNode = (node) => {
|
|||||||
verifyResponseStatusCode('@lineageDeleteApi', 200);
|
verifyResponseStatusCode('@lineageDeleteApi', 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteEdge = (fromNode, toNode) => {
|
||||||
|
interceptURL('DELETE', '/api/v1/lineage/**', 'lineageDeleteApi');
|
||||||
|
cy.get(`[data-testid="edge-${fromNode.fqn}-${toNode.fqn}"]`).click({
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
['Table', 'Topic'].indexOf(fromNode.entityType) > -1 &&
|
||||||
|
['Table', 'Topic'].indexOf(toNode.entityType) > -1
|
||||||
|
) {
|
||||||
|
cy.get('[data-testid="add-pipeline"]').click();
|
||||||
|
|
||||||
|
cy.get(
|
||||||
|
'[data-testid="add-edge-modal"] [data-testid="remove-edge-button"]'
|
||||||
|
).click();
|
||||||
|
} else {
|
||||||
|
cy.get('[data-testid="delete-button"]').click();
|
||||||
|
}
|
||||||
|
cy.get(
|
||||||
|
'[data-testid="delete-edge-confirmation-modal"] .ant-btn-primary'
|
||||||
|
).click();
|
||||||
|
verifyResponseStatusCode('@lineageDeleteApi', 200);
|
||||||
|
};
|
||||||
|
|
||||||
const applyPipelineFromModal = (fromNode, toNode, pipelineData) => {
|
const applyPipelineFromModal = (fromNode, toNode, pipelineData) => {
|
||||||
interceptURL('PUT', '/api/v1/lineage', 'lineageApi');
|
interceptURL('PUT', '/api/v1/lineage', 'lineageApi');
|
||||||
cy.get(`[data-testid="edge-${fromNode.fqn}-${toNode.fqn}"]`).click({
|
cy.get(`[data-testid="edge-${fromNode.fqn}-${toNode.fqn}"]`).click({
|
||||||
@ -244,10 +268,10 @@ describe('Lineage verification', { tags: 'DataAssets' }, () => {
|
|||||||
// Delete Nodes
|
// Delete Nodes
|
||||||
for (let i = 0; i < LINEAGE_ITEMS.length; i++) {
|
for (let i = 0; i < LINEAGE_ITEMS.length; i++) {
|
||||||
if (i !== index) {
|
if (i !== index) {
|
||||||
deleteNode(LINEAGE_ITEMS[i]);
|
deleteEdge(entity, LINEAGE_ITEMS[i]);
|
||||||
cy.get(`[data-testid="lineage-node-${LINEAGE_ITEMS[i].fqn}"]`).should(
|
cy.get(
|
||||||
'not.exist'
|
`[data-testid="edge-${entity.fqn}-${LINEAGE_ITEMS[i].fqn}"]`
|
||||||
);
|
).should('not.exist');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import SignUpPage from '../../pages/SignUp/SignUpPage';
|
|||||||
import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase';
|
import applicationRoutesClass from '../../utils/ApplicationRoutesClassBase';
|
||||||
import Appbar from '../AppBar/Appbar';
|
import Appbar from '../AppBar/Appbar';
|
||||||
import LeftSidebar from '../MyData/LeftSidebar/LeftSidebar.component';
|
import LeftSidebar from '../MyData/LeftSidebar/LeftSidebar.component';
|
||||||
|
import applicationsClassBase from '../Settings/Applications/AppDetails/ApplicationsClassBase';
|
||||||
import './app-container.less';
|
import './app-container.less';
|
||||||
|
|
||||||
const AppContainer = () => {
|
const AppContainer = () => {
|
||||||
@ -29,7 +30,7 @@ const AppContainer = () => {
|
|||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
const { currentUser } = useApplicationStore();
|
const { currentUser } = useApplicationStore();
|
||||||
const AuthenticatedRouter = applicationRoutesClass.getRouteElements();
|
const AuthenticatedRouter = applicationRoutesClass.getRouteElements();
|
||||||
|
const ApplicationExtras = applicationsClassBase.getApplicationExtension();
|
||||||
const isDirectionRTL = useMemo(() => i18n.dir() === 'rtl', [i18n]);
|
const isDirectionRTL = useMemo(() => i18n.dir() === 'rtl', [i18n]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -53,6 +54,7 @@ const AppContainer = () => {
|
|||||||
<Layout>
|
<Layout>
|
||||||
<Content className="main-content">
|
<Content className="main-content">
|
||||||
<AuthenticatedRouter />
|
<AuthenticatedRouter />
|
||||||
|
{ApplicationExtras && <ApplicationExtras />}
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@ -11,13 +11,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Button } from 'antd';
|
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { lowerCase } from 'lodash';
|
import { lowerCase } from 'lodash';
|
||||||
import React, { Fragment, FunctionComponent, useMemo, useState } from 'react';
|
import React, { Fragment, FunctionComponent, useState } from 'react';
|
||||||
import EntityLink from '../../../utils/EntityLink';
|
|
||||||
import Searchbar from '../../common/SearchBarComponent/SearchBar.component';
|
import Searchbar from '../../common/SearchBarComponent/SearchBar.component';
|
||||||
import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider';
|
|
||||||
import SchemaTable from '../SchemaTable/SchemaTable.component';
|
import SchemaTable from '../SchemaTable/SchemaTable.component';
|
||||||
import { Props } from './SchemaTab.interfaces';
|
import { Props } from './SchemaTab.interfaces';
|
||||||
|
|
||||||
@ -34,20 +31,11 @@ const SchemaTab: FunctionComponent<Props> = ({
|
|||||||
tableConstraints,
|
tableConstraints,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const { selectedUserSuggestions } = useSuggestionsContext();
|
|
||||||
|
|
||||||
const handleSearchAction = (searchValue: string) => {
|
const handleSearchAction = (searchValue: string) => {
|
||||||
setSearchText(searchValue);
|
setSearchText(searchValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columnSuggestions = useMemo(
|
|
||||||
() =>
|
|
||||||
selectedUserSuggestions?.filter(
|
|
||||||
(item) => EntityLink.getTableColumnName(item.entityLink) !== undefined
|
|
||||||
) ?? [],
|
|
||||||
[selectedUserSuggestions]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="d-flex items-center justify-between">
|
<div className="d-flex items-center justify-between">
|
||||||
@ -60,11 +48,6 @@ const SchemaTab: FunctionComponent<Props> = ({
|
|||||||
onSearch={handleSearchAction}
|
onSearch={handleSearchAction}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{columnSuggestions.length > 0 && (
|
|
||||||
<Button className="suggestion-pending-btn">
|
|
||||||
{columnSuggestions.length} {t('label.suggestion-pending')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<SchemaTable
|
<SchemaTable
|
||||||
columnName={columnName}
|
columnName={columnName}
|
||||||
|
|||||||
@ -60,6 +60,7 @@ const TableDescription = ({
|
|||||||
<SuggestionsAlert
|
<SuggestionsAlert
|
||||||
hasEditAccess={hasEditPermission}
|
hasEditAccess={hasEditPermission}
|
||||||
maxLength={40}
|
maxLength={40}
|
||||||
|
showSuggestedBy={false}
|
||||||
suggestion={activeSuggestion}
|
suggestion={activeSuggestion}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -86,7 +87,7 @@ const TableDescription = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Space
|
<Space
|
||||||
className="hover-icon-group w-full"
|
className="hover-icon-group w-full d-flex"
|
||||||
data-testid="description"
|
data-testid="description"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
id={`field-description-${index}`}>
|
id={`field-description-${index}`}>
|
||||||
|
|||||||
@ -76,7 +76,7 @@ import AppSchedule from '../AppSchedule/AppSchedule.component';
|
|||||||
import { ApplicationTabs } from '../MarketPlaceAppDetails/MarketPlaceAppDetails.interface';
|
import { ApplicationTabs } from '../MarketPlaceAppDetails/MarketPlaceAppDetails.interface';
|
||||||
import './app-details.less';
|
import './app-details.less';
|
||||||
import { AppAction } from './AppDetails.interface';
|
import { AppAction } from './AppDetails.interface';
|
||||||
import applicationSchemaClassBase from './ApplicationSchemaClassBase';
|
import applicationsClassBase from './ApplicationsClassBase';
|
||||||
|
|
||||||
const AppDetails = () => {
|
const AppDetails = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -93,7 +93,7 @@ const AppDetails = () => {
|
|||||||
isRunLoading: false,
|
isRunLoading: false,
|
||||||
isSaveLoading: false,
|
isSaveLoading: false,
|
||||||
});
|
});
|
||||||
const UiSchema = applicationSchemaClassBase.getJSONUISchema();
|
const UiSchema = applicationsClassBase.getJSONUISchema();
|
||||||
|
|
||||||
const fetchAppDetails = useCallback(async () => {
|
const fetchAppDetails = useCallback(async () => {
|
||||||
setLoadingState((prev) => ({ ...prev, isFetchLoading: true }));
|
setLoadingState((prev) => ({ ...prev, isFetchLoading: true }));
|
||||||
@ -104,7 +104,7 @@ const AppDetails = () => {
|
|||||||
});
|
});
|
||||||
setAppData(data);
|
setAppData(data);
|
||||||
|
|
||||||
const schema = await applicationSchemaClassBase.importSchema(fqn);
|
const schema = await applicationsClassBase.importSchema(fqn);
|
||||||
|
|
||||||
setJsonSchema(schema.default);
|
setJsonSchema(schema.default);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -138,7 +138,7 @@ jest.mock('../AppSchedule/AppSchedule.component', () =>
|
|||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.mock('./ApplicationSchemaClassBase', () => ({
|
jest.mock('./ApplicationsClassBase', () => ({
|
||||||
importSchema: jest.fn().mockReturnValue({ default: ['table'] }),
|
importSchema: jest.fn().mockReturnValue({ default: ['table'] }),
|
||||||
getJSONUISchema: jest.fn().mockReturnValue({}),
|
getJSONUISchema: jest.fn().mockReturnValue({}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -11,7 +11,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class ApplicationSchemaClassBase {
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
class ApplicationsClassBase {
|
||||||
public importSchema(fqn: string) {
|
public importSchema(fqn: string) {
|
||||||
return import(`../../../../utils/ApplicationSchemas/${fqn}.json`);
|
return import(`../../../../utils/ApplicationSchemas/${fqn}.json`);
|
||||||
}
|
}
|
||||||
@ -21,9 +23,17 @@ class ApplicationSchemaClassBase {
|
|||||||
public importAppLogo(appName: string) {
|
public importAppLogo(appName: string) {
|
||||||
return import(`../../../../assets/svg/${appName}.svg`);
|
return import(`../../../../assets/svg/${appName}.svg`);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Used to pass extra elements from installed Apps.
|
||||||
|
*
|
||||||
|
* @return {FC | null} The application extension, or null if none exists.
|
||||||
|
*/
|
||||||
|
public getApplicationExtension(): FC | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationSchemaClassBase = new ApplicationSchemaClassBase();
|
const applicationsClassBase = new ApplicationsClassBase();
|
||||||
|
|
||||||
export default applicationSchemaClassBase;
|
export default applicationsClassBase;
|
||||||
export { ApplicationSchemaClassBase };
|
export { ApplicationsClassBase };
|
||||||
@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import { Avatar } from 'antd';
|
import { Avatar } from 'antd';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import applicationSchemaClassBase from '../AppDetails/ApplicationSchemaClassBase';
|
import applicationsClassBase from '../AppDetails/ApplicationsClassBase';
|
||||||
|
|
||||||
const AppLogo = ({
|
const AppLogo = ({
|
||||||
logo,
|
logo,
|
||||||
@ -25,7 +25,7 @@ const AppLogo = ({
|
|||||||
|
|
||||||
const fetchLogo = useCallback(async () => {
|
const fetchLogo = useCallback(async () => {
|
||||||
if (!logo) {
|
if (!logo) {
|
||||||
const data = await applicationSchemaClassBase.importAppLogo(appName);
|
const data = await applicationsClassBase.importAppLogo(appName);
|
||||||
const Icon = data.ReactComponent as React.ComponentType<
|
const Icon = data.ReactComponent as React.ComponentType<
|
||||||
JSX.IntrinsicElements['svg']
|
JSX.IntrinsicElements['svg']
|
||||||
>;
|
>;
|
||||||
|
|||||||
@ -16,4 +16,5 @@ export interface SuggestionsAlertProps {
|
|||||||
suggestion: Suggestion;
|
suggestion: Suggestion;
|
||||||
hasEditAccess?: boolean;
|
hasEditAccess?: boolean;
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
|
showSuggestedBy?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
import { Button, Card, Space, Typography } from 'antd';
|
import { Button, Card, Typography } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReactComponent as StarIcon } from '../../../assets/svg/ic-suggestions.svg';
|
import { ReactComponent as StarIcon } from '../../../assets/svg/ic-suggestions.svg';
|
||||||
@ -27,6 +27,7 @@ const SuggestionsAlert = ({
|
|||||||
suggestion,
|
suggestion,
|
||||||
hasEditAccess = false,
|
hasEditAccess = false,
|
||||||
maxLength,
|
maxLength,
|
||||||
|
showSuggestedBy = true,
|
||||||
}: SuggestionsAlertProps) => {
|
}: SuggestionsAlertProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { acceptRejectSuggestion } = useSuggestionsContext();
|
const { acceptRejectSuggestion } = useSuggestionsContext();
|
||||||
@ -37,11 +38,6 @@ const SuggestionsAlert = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space
|
|
||||||
className="schema-description d-flex"
|
|
||||||
data-testid="asset-description-container"
|
|
||||||
direction="vertical"
|
|
||||||
size={12}>
|
|
||||||
<Card className="suggested-description-card card-padding-0">
|
<Card className="suggested-description-card card-padding-0">
|
||||||
<div className="suggested-alert-content">
|
<div className="suggested-alert-content">
|
||||||
<RichTextEditorPreviewer
|
<RichTextEditorPreviewer
|
||||||
@ -51,6 +47,8 @@ const SuggestionsAlert = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="suggested-alert-footer d-flex justify-between">
|
<div className="suggested-alert-footer d-flex justify-between">
|
||||||
<div className="d-flex items-center gap-2 ">
|
<div className="d-flex items-center gap-2 ">
|
||||||
|
{showSuggestedBy && (
|
||||||
|
<>
|
||||||
<StarIcon width={14} />
|
<StarIcon width={14} />
|
||||||
<Typography.Text className="text-grey-muted font-italic">
|
<Typography.Text className="text-grey-muted font-italic">
|
||||||
{t('label.suggested-by')}
|
{t('label.suggested-by')}
|
||||||
@ -64,7 +62,10 @@ const SuggestionsAlert = ({
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</UserPopOverCard>
|
</UserPopOverCard>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasEditAccess && (
|
{hasEditAccess && (
|
||||||
<div className="d-flex justify-end gap-2">
|
<div className="d-flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
@ -90,7 +91,6 @@ const SuggestionsAlert = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Space>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export interface SuggestionsContextType {
|
|||||||
loadingAccept: boolean;
|
loadingAccept: boolean;
|
||||||
loadingReject: boolean;
|
loadingReject: boolean;
|
||||||
allSuggestionsUsers: EntityReference[];
|
allSuggestionsUsers: EntityReference[];
|
||||||
onUpdateActiveUser: (user: EntityReference) => void;
|
onUpdateActiveUser: (user?: EntityReference) => void;
|
||||||
fetchSuggestions: (entityFqn: string) => void;
|
fetchSuggestions: (entityFqn: string) => void;
|
||||||
acceptRejectSuggestion: (
|
acceptRejectSuggestion: (
|
||||||
suggestion: Suggestion,
|
suggestion: Suggestion,
|
||||||
|
|||||||
@ -119,7 +119,7 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onUpdateActiveUser = useCallback(
|
const onUpdateActiveUser = useCallback(
|
||||||
(user: EntityReference) => {
|
(user?: EntityReference) => {
|
||||||
setActiveUser(user);
|
setActiveUser(user);
|
||||||
},
|
},
|
||||||
[suggestionsByUser]
|
[suggestionsByUser]
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Button, Typography } from 'antd';
|
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Space, Typography } from 'antd';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SuggestionType } from '../../../generated/entity/feed/suggestion';
|
import { SuggestionType } from '../../../generated/entity/feed/suggestion';
|
||||||
@ -24,6 +25,7 @@ const SuggestionsSlider = () => {
|
|||||||
acceptRejectAllSuggestions,
|
acceptRejectAllSuggestions,
|
||||||
loadingAccept,
|
loadingAccept,
|
||||||
loadingReject,
|
loadingReject,
|
||||||
|
onUpdateActiveUser,
|
||||||
} = useSuggestionsContext();
|
} = useSuggestionsContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -33,11 +35,13 @@ const SuggestionsSlider = () => {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<AvatarCarousel />
|
<AvatarCarousel />
|
||||||
{selectedUserSuggestions.length > 0 && (
|
{selectedUserSuggestions.length > 0 && (
|
||||||
<>
|
<Space className="slider-btn-container m-l-xss">
|
||||||
<Button
|
<Button
|
||||||
|
ghost
|
||||||
|
className="text-xs text-primary font-medium"
|
||||||
data-testid="accept-all-suggestions"
|
data-testid="accept-all-suggestions"
|
||||||
|
icon={<CheckOutlined />}
|
||||||
loading={loadingAccept}
|
loading={loadingAccept}
|
||||||
size="small"
|
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
acceptRejectAllSuggestions(
|
acceptRejectAllSuggestions(
|
||||||
@ -45,15 +49,14 @@ const SuggestionsSlider = () => {
|
|||||||
SuggestionAction.Accept
|
SuggestionAction.Accept
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
<Typography.Text className="text-xs text-white">
|
|
||||||
{t('label.accept-all')}
|
{t('label.accept-all')}
|
||||||
</Typography.Text>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
ghost
|
ghost
|
||||||
|
className="text-xs text-primary font-medium"
|
||||||
data-testid="reject-all-suggestions"
|
data-testid="reject-all-suggestions"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
loading={loadingReject}
|
loading={loadingReject}
|
||||||
size="small"
|
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
acceptRejectAllSuggestions(
|
acceptRejectAllSuggestions(
|
||||||
@ -61,11 +64,18 @@ const SuggestionsSlider = () => {
|
|||||||
SuggestionAction.Reject
|
SuggestionAction.Reject
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
<Typography.Text className="text-xs text-primary">
|
|
||||||
{t('label.reject-all')}
|
{t('label.reject-all')}
|
||||||
</Typography.Text>
|
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
<Button
|
||||||
|
ghost
|
||||||
|
className="text-xs text-primary font-medium"
|
||||||
|
data-testid="close-suggestion"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => onUpdateActiveUser()}>
|
||||||
|
{t('label.close')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AvatarCarousel from './AvatarCarousel';
|
import AvatarCarousel from './AvatarCarousel';
|
||||||
|
|
||||||
@ -35,6 +35,7 @@ jest.mock('../../Suggestions/SuggestionsProvider/SuggestionsProvider', () => ({
|
|||||||
{ id: '2', name: 'Avatar 2', type: 'user' },
|
{ id: '2', name: 'Avatar 2', type: 'user' },
|
||||||
],
|
],
|
||||||
acceptRejectSuggestion: jest.fn(),
|
acceptRejectSuggestion: jest.fn(),
|
||||||
|
selectedUserSuggestions: [],
|
||||||
onUpdateActiveUser: jest.fn(),
|
onUpdateActiveUser: jest.fn(),
|
||||||
})),
|
})),
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@ -70,19 +71,10 @@ jest.mock('../../../rest/suggestionsAPI', () => ({
|
|||||||
|
|
||||||
describe('AvatarCarousel', () => {
|
describe('AvatarCarousel', () => {
|
||||||
it('renders without crashing', () => {
|
it('renders without crashing', () => {
|
||||||
render(<AvatarCarousel />);
|
render(<AvatarCarousel showArrows />);
|
||||||
|
|
||||||
expect(screen.getByText(/Avatar 1/i)).toBeInTheDocument();
|
expect(screen.getByText(/Avatar 1/i)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/Avatar 2/i)).toBeInTheDocument();
|
expect(screen.getByText(/Avatar 2/i)).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('prev-slide')).toBeDisabled();
|
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,16 +11,24 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
import { Button, Carousel } from 'antd';
|
import { Badge, Button, Carousel } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider';
|
import { useSuggestionsContext } from '../../Suggestions/SuggestionsProvider/SuggestionsProvider';
|
||||||
import UserPopOverCard from '../PopOverCard/UserPopOverCard';
|
import UserPopOverCard from '../PopOverCard/UserPopOverCard';
|
||||||
import ProfilePicture from '../ProfilePicture/ProfilePicture';
|
import ProfilePicture from '../ProfilePicture/ProfilePicture';
|
||||||
import './avatar-carousel.less';
|
import './avatar-carousel.less';
|
||||||
|
|
||||||
const AvatarCarousel = () => {
|
interface AvatarCarouselProps {
|
||||||
const { allSuggestionsUsers: avatarList, onUpdateActiveUser } =
|
showArrows?: boolean;
|
||||||
useSuggestionsContext();
|
}
|
||||||
|
|
||||||
|
const AvatarCarousel = ({ showArrows = false }: AvatarCarouselProps) => {
|
||||||
|
const {
|
||||||
|
allSuggestionsUsers: avatarList,
|
||||||
|
onUpdateActiveUser,
|
||||||
|
selectedUserSuggestions,
|
||||||
|
} = useSuggestionsContext();
|
||||||
const [currentSlide, setCurrentSlide] = useState(-1);
|
const [currentSlide, setCurrentSlide] = useState(-1);
|
||||||
|
|
||||||
const prevSlide = useCallback(() => {
|
const prevSlide = useCallback(() => {
|
||||||
@ -43,8 +51,15 @@ const AvatarCarousel = () => {
|
|||||||
onProfileClick(currentSlide);
|
onProfileClick(currentSlide);
|
||||||
}, [currentSlide]);
|
}, [currentSlide]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedUserSuggestions.length === 0) {
|
||||||
|
setCurrentSlide(-1);
|
||||||
|
}
|
||||||
|
}, [selectedUserSuggestions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="avatar-carousel-container d-flex items-center">
|
<div className="avatar-carousel-container d-flex items-center">
|
||||||
|
{showArrows && (
|
||||||
<Button
|
<Button
|
||||||
className="carousel-arrow"
|
className="carousel-arrow"
|
||||||
data-testid="prev-slide"
|
data-testid="prev-slide"
|
||||||
@ -54,26 +69,39 @@ const AvatarCarousel = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
onClick={prevSlide}
|
onClick={prevSlide}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Carousel
|
<Carousel
|
||||||
afterChange={(current) => setCurrentSlide(current)}
|
afterChange={(current) => setCurrentSlide(current)}
|
||||||
dots={false}
|
dots={false}
|
||||||
slidesToShow={avatarList.length < 3 ? avatarList.length : 3}>
|
slidesToShow={avatarList.length < 3 ? avatarList.length : 3}>
|
||||||
{avatarList.map((avatar, index) => (
|
{avatarList.map((avatar, index) => {
|
||||||
<UserPopOverCard
|
const isActive = currentSlide === index;
|
||||||
className=""
|
|
||||||
key={avatar.id}
|
const button = (
|
||||||
userName={avatar?.name ?? ''}>
|
|
||||||
<Button
|
<Button
|
||||||
className={`p-0 m-r-xss avatar-item ${
|
className={classNames('p-0 m-r-xss avatar-item', {
|
||||||
currentSlide === index ? 'active' : ''
|
active: isActive,
|
||||||
}`}
|
})}
|
||||||
shape="circle"
|
shape="circle"
|
||||||
onClick={() => setCurrentSlide(index)}>
|
onClick={() => setCurrentSlide(index)}>
|
||||||
<ProfilePicture name={avatar.name ?? ''} width="30" />
|
<ProfilePicture name={avatar.name ?? ''} width="28" />
|
||||||
</Button>
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserPopOverCard key={avatar.id} userName={avatar?.name ?? ''}>
|
||||||
|
{isActive ? ( // Show Badge only for active item
|
||||||
|
<Badge count={selectedUserSuggestions.length}>{button}</Badge>
|
||||||
|
) : (
|
||||||
|
button
|
||||||
|
)}
|
||||||
</UserPopOverCard>
|
</UserPopOverCard>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
|
|
||||||
|
{showArrows && (
|
||||||
<Button
|
<Button
|
||||||
className="carousel-arrow"
|
className="carousel-arrow"
|
||||||
data-testid="next-slide"
|
data-testid="next-slide"
|
||||||
@ -85,6 +113,7 @@ const AvatarCarousel = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
onClick={nextSlide}
|
onClick={nextSlide}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,20 +13,42 @@
|
|||||||
@import url('../../../styles/variables.less');
|
@import url('../../../styles/variables.less');
|
||||||
|
|
||||||
.avatar-item {
|
.avatar-item {
|
||||||
opacity: 0.4;
|
position: relative;
|
||||||
&.active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: @border-color !important;
|
border-color: @border-color !important;
|
||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: @border-color !important;
|
border-color: @border-color !important;
|
||||||
}
|
}
|
||||||
|
&.ant-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
min-width: 28px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-carousel-container {
|
.avatar-carousel-container {
|
||||||
.slick-slide {
|
.slick-slide {
|
||||||
width: 32px !important;
|
width: 32px !important;
|
||||||
}
|
}
|
||||||
|
.slick-list {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
.ant-badge-count {
|
||||||
|
right: 4px;
|
||||||
|
background-color: @red-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-btn-container {
|
||||||
|
.ant-btn {
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
.ant-btn.exit-suggestion {
|
||||||
|
color: @grey-4;
|
||||||
|
border-color: @grey-4;
|
||||||
|
padding: 0;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -205,7 +205,7 @@ const DescriptionV1 = ({
|
|||||||
data-testid="asset-description-container"
|
data-testid="asset-description-container"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
size={16}>
|
size={16}>
|
||||||
<div className="d-flex justify-between">
|
<div className="d-flex justify-between flex-wrap">
|
||||||
<div className="d-flex items-center gap-2">
|
<div className="d-flex items-center gap-2">
|
||||||
<Text className="right-panel-label">{t('label.description')}</Text>
|
<Text className="right-panel-label">{t('label.description')}</Text>
|
||||||
{showActions && actionButtons}
|
{showActions && actionButtons}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import FormBuilder from '../../components/common/FormBuilder/FormBuilder';
|
|||||||
import Loader from '../../components/common/Loader/Loader';
|
import Loader from '../../components/common/Loader/Loader';
|
||||||
import TestSuiteScheduler from '../../components/DataQuality/AddDataQualityTest/components/TestSuiteScheduler';
|
import TestSuiteScheduler from '../../components/DataQuality/AddDataQualityTest/components/TestSuiteScheduler';
|
||||||
import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
|
import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
|
||||||
import applicationSchemaClassBase from '../../components/Settings/Applications/AppDetails/ApplicationSchemaClassBase';
|
import applicationSchemaClassBase from '../../components/Settings/Applications/AppDetails/ApplicationsClassBase';
|
||||||
import AppInstallVerifyCard from '../../components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component';
|
import AppInstallVerifyCard from '../../components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component';
|
||||||
import IngestionStepper from '../../components/Settings/Services/Ingestion/IngestionStepper/IngestionStepper.component';
|
import IngestionStepper from '../../components/Settings/Services/Ingestion/IngestionStepper/IngestionStepper.component';
|
||||||
import { STEPS_FOR_APP_INSTALL } from '../../constants/Applications.constant';
|
import { STEPS_FOR_APP_INSTALL } from '../../constants/Applications.constant';
|
||||||
|
|||||||
@ -48,7 +48,7 @@ jest.mock(
|
|||||||
);
|
);
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'../../components/Settings/Applications/AppDetails/ApplicationSchemaClassBase',
|
'../../components/Settings/Applications/AppDetails/ApplicationsClassBase',
|
||||||
() => ({
|
() => ({
|
||||||
importSchema: jest.fn().mockResolvedValue({}),
|
importSchema: jest.fn().mockResolvedValue({}),
|
||||||
getJSONUISchema: jest.fn().mockReturnValue({}),
|
getJSONUISchema: jest.fn().mockReturnValue({}),
|
||||||
|
|||||||
@ -104,9 +104,6 @@ import TableConstraints from './TableConstraints/TableConstraints';
|
|||||||
const TableDetailsPageV1: React.FC = () => {
|
const TableDetailsPageV1: React.FC = () => {
|
||||||
const { isTourOpen, activeTabForTourDatasetPage, isTourPage } =
|
const { isTourOpen, activeTabForTourDatasetPage, isTourPage } =
|
||||||
useTourProvider();
|
useTourProvider();
|
||||||
const FloatingButton = entityUtilClassBase.getEntityFloatingButton(
|
|
||||||
EntityType.TABLE
|
|
||||||
);
|
|
||||||
const { currentUser } = useApplicationStore();
|
const { currentUser } = useApplicationStore();
|
||||||
const [tableDetails, setTableDetails] = useState<Table>();
|
const [tableDetails, setTableDetails] = useState<Table>();
|
||||||
const { tab: activeTab = EntityTabs.SCHEMA } =
|
const { tab: activeTab = EntityTabs.SCHEMA } =
|
||||||
@ -1075,8 +1072,6 @@ const TableDetailsPageV1: React.FC = () => {
|
|||||||
onCancel={onThreadPanelClose}
|
onCancel={onThreadPanelClose}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{FloatingButton && <FloatingButton />}
|
|
||||||
</Row>
|
</Row>
|
||||||
</PageLayoutV1>
|
</PageLayoutV1>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user