mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-02 02:26:00 +00:00
fix(ui): Fix duplicate owners & tier field in project explorer card (#24297)
* fix(ui): fix duplicate owner field in overview section in explore card * nit * nit * hide tier row in overview section * refactor code
This commit is contained in:
parent
e18c848851
commit
7745cc1330
@ -26,4 +26,5 @@ export interface CommonEntitySummaryInfoV1Props {
|
|||||||
entityInfo: EntityInfoItemV1[];
|
entityInfo: EntityInfoItemV1[];
|
||||||
componentType: string;
|
componentType: string;
|
||||||
isDomainVisible?: boolean;
|
isDomainVisible?: boolean;
|
||||||
|
excludedItems?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import CommonEntitySummaryInfoV1 from './CommonEntitySummaryInfoV1';
|
import CommonEntitySummaryInfoV1 from './CommonEntitySummaryInfoV1';
|
||||||
|
import { EntityInfoItemV1 } from './CommonEntitySummaryInfoV1.interface';
|
||||||
|
|
||||||
// Mock i18n
|
// Mock i18n
|
||||||
jest.mock('react-i18next', () => ({
|
jest.mock('react-i18next', () => ({
|
||||||
@ -38,7 +39,7 @@ jest.mock('react-router-dom', () => ({
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const defaultItems = [
|
const defaultItems: EntityInfoItemV1[] = [
|
||||||
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
||||||
{ name: 'Rows', value: 1000, visible: ['explore'] },
|
{ name: 'Rows', value: 1000, visible: ['explore'] },
|
||||||
{ name: 'Columns', value: 15, visible: ['explore'] },
|
{ name: 'Columns', value: 15, visible: ['explore'] },
|
||||||
@ -53,7 +54,7 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
const { container } = render(
|
const { container } = render(
|
||||||
<CommonEntitySummaryInfoV1
|
<CommonEntitySummaryInfoV1
|
||||||
componentType="explore"
|
componentType="explore"
|
||||||
entityInfo={defaultItems as any}
|
entityInfo={defaultItems}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -72,16 +73,13 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('filters out items not visible for the componentType', () => {
|
it('filters out items not visible for the componentType', () => {
|
||||||
const items = [
|
const items: EntityInfoItemV1[] = [
|
||||||
{ name: 'Visible', value: 'Yes', visible: ['explore'] },
|
{ name: 'Visible', value: 'Yes', visible: ['explore'] },
|
||||||
{ name: 'Hidden', value: 'No', visible: ['other'] },
|
{ name: 'Hidden', value: 'No', visible: ['other'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<CommonEntitySummaryInfoV1
|
<CommonEntitySummaryInfoV1 componentType="explore" entityInfo={items} />
|
||||||
componentType="explore"
|
|
||||||
entityInfo={items as any}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTestId('Visible-label')).toBeInTheDocument();
|
expect(screen.getByTestId('Visible-label')).toBeInTheDocument();
|
||||||
@ -89,7 +87,7 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows domain item when isDomainVisible is true regardless of visibility array', () => {
|
it('shows domain item when isDomainVisible is true regardless of visibility array', () => {
|
||||||
const items = [
|
const items: EntityInfoItemV1[] = [
|
||||||
{ name: 'label.domain-plural', value: 'Domain A', visible: ['other'] },
|
{ name: 'label.domain-plural', value: 'Domain A', visible: ['other'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -97,7 +95,7 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
<CommonEntitySummaryInfoV1
|
<CommonEntitySummaryInfoV1
|
||||||
isDomainVisible
|
isDomainVisible
|
||||||
componentType="explore"
|
componentType="explore"
|
||||||
entityInfo={items as any}
|
entityInfo={items}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -108,7 +106,7 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders internal link when isLink is true and isExternal is false', () => {
|
it('renders internal link when isLink is true and isExternal is false', () => {
|
||||||
const items = [
|
const items: EntityInfoItemV1[] = [
|
||||||
{
|
{
|
||||||
name: 'Docs',
|
name: 'Docs',
|
||||||
value: 'OpenMetadata',
|
value: 'OpenMetadata',
|
||||||
@ -120,10 +118,7 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<CommonEntitySummaryInfoV1
|
<CommonEntitySummaryInfoV1 componentType="explore" entityInfo={items} />
|
||||||
componentType="explore"
|
|
||||||
entityInfo={items as any}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const link = screen.getByTestId('internal-link');
|
const link = screen.getByTestId('internal-link');
|
||||||
@ -134,7 +129,7 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders external link with icon when isExternal is true', () => {
|
it('renders external link with icon when isExternal is true', () => {
|
||||||
const items = [
|
const items: EntityInfoItemV1[] = [
|
||||||
{
|
{
|
||||||
name: 'Website',
|
name: 'Website',
|
||||||
value: 'OpenMetadata',
|
value: 'OpenMetadata',
|
||||||
@ -146,10 +141,7 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<CommonEntitySummaryInfoV1
|
<CommonEntitySummaryInfoV1 componentType="explore" entityInfo={items} />
|
||||||
componentType="explore"
|
|
||||||
entityInfo={items as any}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const anchor = container.querySelector('a.summary-item-link');
|
const anchor = container.querySelector('a.summary-item-link');
|
||||||
@ -160,16 +152,13 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders dash when value is null or undefined', () => {
|
it('renders dash when value is null or undefined', () => {
|
||||||
const items = [
|
const items: EntityInfoItemV1[] = [
|
||||||
{ name: 'Empty', value: undefined, visible: ['explore'] },
|
{ name: 'Empty', value: undefined, visible: ['explore'] },
|
||||||
{ name: 'AlsoEmpty', value: null, visible: ['explore'] },
|
{ name: 'AlsoEmpty', value: null, visible: ['explore'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<CommonEntitySummaryInfoV1
|
<CommonEntitySummaryInfoV1 componentType="explore" entityInfo={items} />
|
||||||
componentType="explore"
|
|
||||||
entityInfo={items as any}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByTestId('Empty-value')).toHaveTextContent('-');
|
expect(getByTestId('Empty-value')).toHaveTextContent('-');
|
||||||
@ -177,16 +166,13 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('supports labels with special characters in testids', () => {
|
it('supports labels with special characters in testids', () => {
|
||||||
const items = [
|
const items: EntityInfoItemV1[] = [
|
||||||
{ name: 'Label & Value', value: 'Test', visible: ['explore'] },
|
{ name: 'Label & Value', value: 'Test', visible: ['explore'] },
|
||||||
{ name: 'Label < > " \'', value: 'Again', visible: ['explore'] },
|
{ name: 'Label < > " \'', value: 'Again', visible: ['explore'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<CommonEntitySummaryInfoV1
|
<CommonEntitySummaryInfoV1 componentType="explore" entityInfo={items} />
|
||||||
componentType="explore"
|
|
||||||
entityInfo={items as any}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTestId('Label & Value-value')).toHaveTextContent('Test');
|
expect(screen.getByTestId('Label & Value-value')).toHaveTextContent('Test');
|
||||||
@ -194,4 +180,99 @@ describe('CommonEntitySummaryInfoV1', () => {
|
|||||||
'Again'
|
'Again'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('excludes items specified in excludedItems prop', () => {
|
||||||
|
const items: EntityInfoItemV1[] = [
|
||||||
|
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
||||||
|
{ name: 'Owners', value: 'John Doe', visible: ['explore'] },
|
||||||
|
{ name: 'Tier', value: 'Gold', visible: ['explore'] },
|
||||||
|
{ name: 'Rows', value: 1000, visible: ['explore'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CommonEntitySummaryInfoV1
|
||||||
|
componentType="explore"
|
||||||
|
entityInfo={items}
|
||||||
|
excludedItems={['Owners', 'Tier']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('Type-label')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('Rows-label')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('Owners-label')).toBeNull();
|
||||||
|
expect(screen.queryByTestId('Tier-label')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all items when excludedItems is empty array', () => {
|
||||||
|
const items: EntityInfoItemV1[] = [
|
||||||
|
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
||||||
|
{ name: 'Owners', value: 'John Doe', visible: ['explore'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CommonEntitySummaryInfoV1
|
||||||
|
componentType="explore"
|
||||||
|
entityInfo={items}
|
||||||
|
excludedItems={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('Type-label')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('Owners-label')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all items when excludedItems is not provided', () => {
|
||||||
|
const items: EntityInfoItemV1[] = [
|
||||||
|
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
||||||
|
{ name: 'Owners', value: 'John Doe', visible: ['explore'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CommonEntitySummaryInfoV1 componentType="explore" entityInfo={items} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('Type-label')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('Owners-label')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('combines excludedItems with visibility filtering', () => {
|
||||||
|
const items: EntityInfoItemV1[] = [
|
||||||
|
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
||||||
|
{ name: 'Owners', value: 'John Doe', visible: ['explore'] },
|
||||||
|
{ name: 'Hidden', value: 'Secret', visible: ['other'] },
|
||||||
|
{ name: 'Tier', value: 'Gold', visible: ['explore'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CommonEntitySummaryInfoV1
|
||||||
|
componentType="explore"
|
||||||
|
entityInfo={items}
|
||||||
|
excludedItems={['Owners', 'Tier']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('Type-label')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('Owners-label')).toBeNull();
|
||||||
|
expect(screen.queryByTestId('Hidden-label')).toBeNull();
|
||||||
|
expect(screen.queryByTestId('Tier-label')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows domain item when isDomainVisible is true even if in excludedItems', () => {
|
||||||
|
const items: EntityInfoItemV1[] = [
|
||||||
|
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
||||||
|
{ name: 'label.domain-plural', value: 'Domain A', visible: ['other'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CommonEntitySummaryInfoV1
|
||||||
|
isDomainVisible
|
||||||
|
componentType="explore"
|
||||||
|
entityInfo={items}
|
||||||
|
excludedItems={['label.domain-plural']}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('Type-label')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('label.domain-plural-label')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
import Icon from '@ant-design/icons/lib/components/Icon';
|
import Icon from '@ant-design/icons/lib/components/Icon';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isNil } from 'lodash';
|
import { isNil } from 'lodash';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ReactComponent as IconExternalLink } from '../../../assets/svg/external-links.svg';
|
import { ReactComponent as IconExternalLink } from '../../../assets/svg/external-links.svg';
|
||||||
@ -27,14 +28,23 @@ const CommonEntitySummaryInfoV1: React.FC<CommonEntitySummaryInfoV1Props> = ({
|
|||||||
entityInfo,
|
entityInfo,
|
||||||
componentType,
|
componentType,
|
||||||
isDomainVisible = false,
|
isDomainVisible = false,
|
||||||
|
excludedItems = [],
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const isItemVisible = (item: EntityInfoItemV1) => {
|
const visibleEntityInfo = useMemo(() => {
|
||||||
const isDomain = isDomainVisible && item.name === t('label.domain-plural');
|
return entityInfo.filter((info) => {
|
||||||
|
if (excludedItems.includes(info.name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (item.visible || []).includes(componentType) || isDomain;
|
const isDomain =
|
||||||
};
|
isDomainVisible && info.name === t('label.domain-plural');
|
||||||
|
const isVisibleInComponent = (info.visible ?? []).includes(componentType);
|
||||||
|
|
||||||
|
return isVisibleInComponent || isDomain;
|
||||||
|
});
|
||||||
|
}, [entityInfo, componentType, isDomainVisible, excludedItems]);
|
||||||
|
|
||||||
const renderInfoValue = (info: EntityInfoItemV1) => {
|
const renderInfoValue = (info: EntityInfoItemV1) => {
|
||||||
if (!info.isLink) {
|
if (!info.isLink) {
|
||||||
@ -64,7 +74,7 @@ const CommonEntitySummaryInfoV1: React.FC<CommonEntitySummaryInfoV1Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overview-section">
|
<div className="overview-section">
|
||||||
{entityInfo.filter(isItemVisible).map((info) => (
|
{visibleEntityInfo.map((info) => (
|
||||||
<div className="overview-row" key={info.name}>
|
<div className="overview-row" key={info.name}>
|
||||||
<span
|
<span
|
||||||
className={classNames('overview-label')}
|
className={classNames('overview-label')}
|
||||||
|
|||||||
@ -37,6 +37,42 @@ jest.mock('../SectionWithEdit/SectionWithEdit', () => {
|
|||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mock CommonEntitySummaryInfoV1 component
|
||||||
|
jest.mock('./CommonEntitySummaryInfoV1', () => {
|
||||||
|
return jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(({ entityInfo, excludedItems = [], componentType }) => {
|
||||||
|
const filteredInfo = entityInfo.filter(
|
||||||
|
(item: { name: string; visible?: string[] }) => {
|
||||||
|
if (excludedItems.includes(item.name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !componentType || (item.visible ?? []).includes(componentType);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overview-section">
|
||||||
|
{filteredInfo.map((info: { name: string; value: unknown }) => (
|
||||||
|
<div className="overview-row" key={info.name}>
|
||||||
|
<span
|
||||||
|
className="overview-label"
|
||||||
|
data-testid={`${info.name}-label`}>
|
||||||
|
{info.name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="overview-value text-grey-body"
|
||||||
|
data-testid={`${info.name}-value`}>
|
||||||
|
{String(info.value)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const entityInfoV1 = [
|
const entityInfoV1 = [
|
||||||
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
||||||
{ name: 'Rows', value: 1000, visible: ['explore'] },
|
{ name: 'Rows', value: 1000, visible: ['explore'] },
|
||||||
@ -116,12 +152,25 @@ describe('OverviewSection', () => {
|
|||||||
expect(values).toHaveLength(5);
|
expect(values).toHaveLength(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide when nothing visible for given componentType', () => {
|
it('should exclude Owners and Tier items via excludedItems prop', () => {
|
||||||
|
const infoWithExcluded = [
|
||||||
|
{ name: 'Type', value: 'Table', visible: ['explore'] },
|
||||||
|
{ name: 'Owners', value: 'John Doe', visible: ['explore'] },
|
||||||
|
{ name: 'Tier', value: 'Gold', visible: ['explore'] },
|
||||||
|
{ name: 'Rows', value: 1000, visible: ['explore'] },
|
||||||
|
];
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<OverviewSection componentType="other" entityInfoV1={entityInfoV1} />
|
<OverviewSection
|
||||||
|
componentType="explore"
|
||||||
|
entityInfoV1={infoWithExcluded}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByTestId('section-with-edit')).toBeNull();
|
expect(screen.getByTestId('Type-label')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('Rows-label')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('Owners-label')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('Tier-label')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle single item', () => {
|
it('should handle single item', () => {
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import CommonEntitySummaryInfoV1 from './CommonEntitySummaryInfoV1';
|
|||||||
import { OverviewSectionProps } from './OverviewSection.interface';
|
import { OverviewSectionProps } from './OverviewSection.interface';
|
||||||
import './OverviewSection.less';
|
import './OverviewSection.less';
|
||||||
|
|
||||||
|
const EXCLUDED_ITEMS = ['Owners', 'Tier'];
|
||||||
|
|
||||||
const OverviewSection: React.FC<OverviewSectionProps> = ({
|
const OverviewSection: React.FC<OverviewSectionProps> = ({
|
||||||
onEdit,
|
onEdit,
|
||||||
showEditButton = false,
|
showEditButton = false,
|
||||||
@ -25,20 +27,7 @@ const OverviewSection: React.FC<OverviewSectionProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Compute visible rows when using entityInfoV1
|
if (!entityInfoV1 || entityInfoV1.length === 0) {
|
||||||
const visibleEntityInfo = entityInfoV1
|
|
||||||
? entityInfoV1.filter((info) => {
|
|
||||||
const isDomain =
|
|
||||||
isDomainVisible && info.name === t('label.domain-plural');
|
|
||||||
|
|
||||||
return (info.visible || []).includes(componentType) || isDomain;
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Hide the entire section (including title) when there's no content
|
|
||||||
const hasContent = entityInfoV1 && visibleEntityInfo.length > 0;
|
|
||||||
|
|
||||||
if (!hasContent) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,13 +36,12 @@ const OverviewSection: React.FC<OverviewSectionProps> = ({
|
|||||||
showEditButton={showEditButton}
|
showEditButton={showEditButton}
|
||||||
title={t('label.overview')}
|
title={t('label.overview')}
|
||||||
onEdit={onEdit}>
|
onEdit={onEdit}>
|
||||||
{entityInfoV1 && (
|
|
||||||
<CommonEntitySummaryInfoV1
|
<CommonEntitySummaryInfoV1
|
||||||
componentType={componentType}
|
componentType={componentType}
|
||||||
entityInfo={entityInfoV1}
|
entityInfo={entityInfoV1}
|
||||||
|
excludedItems={EXCLUDED_ITEMS}
|
||||||
isDomainVisible={isDomainVisible}
|
isDomainVisible={isDomainVisible}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</SectionWithEdit>
|
</SectionWithEdit>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user