fix(ui): ui feedbacks for 1.2 release (#13522)

* fix(ui): ui feedbacks for 1.2 release

* fix notification panel height issue and data model expand icon issue

* Data Quality, test creation, and minimizing the side panel. It shows the name as "Setup Guide.", It should be "Data Profiler Metrics."

* changes locales

* fix: displayName for the test cases

* fix: Remove Domain shows tool tip as "Remove Owner"

* chore: remove box shadow from primary buttons

* fix: icon alignment in activity feed tab

* Long names should be truncated

* Increase the profile picture size to align with both the name and the persona

* Text in Knowledge panels should be of the same size

* Domain should be displayed on the explore card if the domain is available for any entity

* If the Domains not configure the search filter is empty. "No data available." -> "No Domains are Assigned to Tables" etc.

* fix: unit tests

* fix: unit tets

---------

Co-authored-by: Ashish Gupta <ashish@getcollate.io>
Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
Chirag Madlani 2023-10-14 19:40:01 +05:30 committed by GitHub
parent 753e182e21
commit 7c25b5fddd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 218 additions and 86 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "columnValueMaxToBeBetween", "name": "columnValueMaxToBeBetween",
"fullyQualifiedName": "columnValueMaxToBeBetween", "fullyQualifiedName": "columnValueMaxToBeBetween",
"displayName": "Column Value Max. to be Between", "displayName": "Column Value Max. To Be Between",
"description": "This schema defines the test ColumnValueMaxToBeBetween. Test the maximum value in a col is within a range.", "description": "This schema defines the test ColumnValueMaxToBeBetween. Test the maximum value in a col is within a range.",
"entityType": "COLUMN", "entityType": "COLUMN",
"testPlatforms": ["OpenMetadata", "DBT"], "testPlatforms": ["OpenMetadata", "DBT"],

View File

@ -29,7 +29,10 @@ import { ReactComponent as CheckIcon } from '../../../assets/svg/ic-check.svg';
import { ReactComponent as MentionIcon } from '../../../assets/svg/ic-mentions.svg'; import { ReactComponent as MentionIcon } from '../../../assets/svg/ic-mentions.svg';
import { ReactComponent as TaskIcon } from '../../../assets/svg/ic-task.svg'; import { ReactComponent as TaskIcon } from '../../../assets/svg/ic-task.svg';
import { ReactComponent as TaskListIcon } from '../../../assets/svg/task-ic.svg'; import { ReactComponent as TaskListIcon } from '../../../assets/svg/task-ic.svg';
import { ICON_DIMENSION } from '../../../constants/constants'; import {
COMMON_ICON_STYLES,
ICON_DIMENSION,
} from '../../../constants/constants';
import { observerOptions } from '../../../constants/Mydata.constants'; import { observerOptions } from '../../../constants/Mydata.constants';
import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { EntityTabs, EntityType } from '../../../enums/entity.enum';
import { FeedFilter } from '../../../enums/mydata.enum'; import { FeedFilter } from '../../../enums/mydata.enum';
@ -298,7 +301,10 @@ export const ActivityFeedTab = ({
label: ( label: (
<div className="d-flex justify-between"> <div className="d-flex justify-between">
<Space align="center" size="small"> <Space align="center" size="small">
<AllActivityIcon {...ICON_DIMENSION} /> <AllActivityIcon
style={COMMON_ICON_STYLES}
{...ICON_DIMENSION}
/>
<span>{t('label.all')}</span> <span>{t('label.all')}</span>
</Space> </Space>
@ -316,7 +322,7 @@ export const ActivityFeedTab = ({
{ {
label: ( label: (
<Space align="center" size="small"> <Space align="center" size="small">
<MentionIcon {...ICON_DIMENSION} /> <MentionIcon style={COMMON_ICON_STYLES} {...ICON_DIMENSION} />
<span>{t('label.mention-plural')}</span> <span>{t('label.mention-plural')}</span>
</Space> </Space>
), ),
@ -326,7 +332,10 @@ export const ActivityFeedTab = ({
label: ( label: (
<div className="d-flex justify-between"> <div className="d-flex justify-between">
<Space align="center" size="small"> <Space align="center" size="small">
<TaskListIcon {...ICON_DIMENSION} /> <TaskListIcon
style={COMMON_ICON_STYLES}
{...ICON_DIMENSION}
/>
<span>{t('label.task-plural')}</span> <span>{t('label.task-plural')}</span>
</Space> </Space>
<span>{getCountBadge(tasksCount, '', isTaskActiveTab)}</span> <span>{getCountBadge(tasksCount, '', isTaskActiveTab)}</span>

View File

@ -293,7 +293,7 @@ const AddDataQualityTestV1: React.FC<AddDataQualityTestProps> = ({
flex: 0.4, flex: 0.4,
overlay: { overlay: {
displayThreshold: 200, displayThreshold: 200,
header: t('label.setup-guide'), header: t('label.data-profiler-metrics'),
rotation: 'counter-clockwise', rotation: 'counter-clockwise',
}, },
}} }}

View File

@ -22,7 +22,7 @@ import {
Switch, Switch,
} from 'antd'; } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { isEmpty, isUndefined, map, trim } from 'lodash'; import { compact, isEmpty, map, trim } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { VALIDATION_MESSAGES } from '../../constants/constants'; import { VALIDATION_MESSAGES } from '../../constants/constants';
@ -33,6 +33,7 @@ import {
CreatePasswordType, CreatePasswordType,
CreateUser as CreateUserSchema, CreateUser as CreateUserSchema,
} from '../../generated/api/teams/createUser'; } from '../../generated/api/teams/createUser';
import { EntityReference } from '../../generated/entity/type';
import { AuthProvider } from '../../generated/settings/settings'; import { AuthProvider } from '../../generated/settings/settings';
import { checkEmailInUse, generateRandomPwd } from '../../rest/auth-API'; import { checkEmailInUse, generateRandomPwd } from '../../rest/auth-API';
import { getJWTTokenExpiryOptions } from '../../utils/BotsUtils'; import { getJWTTokenExpiryOptions } from '../../utils/BotsUtils';
@ -60,9 +61,9 @@ const CreateUser = ({
const { authConfig } = useAuthContext(); const { authConfig } = useAuthContext();
const [isAdmin, setIsAdmin] = useState(false); const [isAdmin, setIsAdmin] = useState(false);
const [isBot, setIsBot] = useState(forceBot); const [isBot, setIsBot] = useState(forceBot);
const [selectedTeams, setSelectedTeams] = useState<Array<string | undefined>>( const [selectedTeams, setSelectedTeams] = useState<
[] Array<EntityReference | undefined>
); >([]);
const [isPasswordGenerating, setIsPasswordGenerating] = useState(false); const [isPasswordGenerating, setIsPasswordGenerating] = useState(false);
const isAuthProviderBasic = useMemo( const isAuthProviderBasic = useMemo(
@ -105,9 +106,7 @@ const CreateUser = ({
const handleSave: FormProps['onFinish'] = (values) => { const handleSave: FormProps['onFinish'] = (values) => {
const isPasswordGenerated = const isPasswordGenerated =
passwordGenerator === CreatePasswordGenerator.AutomaticGenerate; passwordGenerator === CreatePasswordGenerator.AutomaticGenerate;
const validTeam = selectedTeams.filter( const validTeam = compact(selectedTeams).map((team) => team.id);
(id) => !isUndefined(id)
) as string[];
const { email, displayName, tokenExpiry, confirmPassword, description } = const { email, displayName, tokenExpiry, confirmPassword, description } =
values; values;

View File

@ -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 { Table, Typography } from 'antd'; import { Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import { cloneDeep, isUndefined } from 'lodash'; import { cloneDeep, isUndefined } from 'lodash';
import { EntityTags } from 'Models'; import { EntityTags } from 'Models';
@ -25,6 +25,7 @@ import { TagLabel, TagSource } from '../../../generated/type/tagLabel';
import { updateDataModelColumnDescription } from '../../../utils/DataModelsUtils'; import { updateDataModelColumnDescription } from '../../../utils/DataModelsUtils';
import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityName } from '../../../utils/EntityUtils';
import { updateFieldTags } from '../../../utils/TableUtils'; import { updateFieldTags } from '../../../utils/TableUtils';
import Table from '../../common/Table/Table';
import { ModelTabProps } from './ModelTab.interface'; import { ModelTabProps } from './ModelTab.interface';
const ModelTab = ({ const ModelTab = ({

View File

@ -34,6 +34,7 @@ import { showErrorToast } from '../../utils/ToastUtils';
import SearchDropdown from '../SearchDropdown/SearchDropdown'; import SearchDropdown from '../SearchDropdown/SearchDropdown';
import { SearchDropdownOption } from '../SearchDropdown/SearchDropdown.interface'; import { SearchDropdownOption } from '../SearchDropdown/SearchDropdown.interface';
import { useAdvanceSearch } from './AdvanceSearchProvider/AdvanceSearchProvider.component'; import { useAdvanceSearch } from './AdvanceSearchProvider/AdvanceSearchProvider.component';
import { ExploreSearchIndex } from './explore.interface';
import { ExploreQuickFiltersProps } from './ExploreQuickFilters.interface'; import { ExploreQuickFiltersProps } from './ExploreQuickFilters.interface';
const ExploreQuickFilters: FC<ExploreQuickFiltersProps> = ({ const ExploreQuickFilters: FC<ExploreQuickFiltersProps> = ({
@ -196,6 +197,7 @@ const ExploreQuickFilters: FC<ExploreQuickFiltersProps> = ({
<SearchDropdown <SearchDropdown
highlight highlight
fixedOrderOptions={field.key === TIER_FQN_KEY} fixedOrderOptions={field.key === TIER_FQN_KEY}
index={index as ExploreSearchIndex}
isSuggestionsLoading={isOptionsLoading} isSuggestionsLoading={isOptionsLoading}
key={field.key} key={field.key}
label={field.label} label={field.label}

View File

@ -30,6 +30,7 @@ import {
getEntityLinkFromType, getEntityLinkFromType,
getEntityName, getEntityName,
} from '../../../utils/EntityUtils'; } from '../../../utils/EntityUtils';
import { getDomainPath } from '../../../utils/RouterUtils';
import { stringToHTML } from '../../../utils/StringsUtils'; import { stringToHTML } from '../../../utils/StringsUtils';
import { getServiceIcon, getUsagePercentile } from '../../../utils/TableUtils'; import { getServiceIcon, getUsagePercentile } from '../../../utils/TableUtils';
import TitleBreadcrumb from '../../common/title-breadcrumb/title-breadcrumb.component'; import TitleBreadcrumb from '../../common/title-breadcrumb/title-breadcrumb.component';
@ -81,6 +82,18 @@ const ExploreSearchCard: React.FC<ExploreSearchCardProps> = forwardRef<
}, },
]; ];
if (source?.domain) {
const domain = getEntityName(source.domain);
const domainLink = getDomainPath(source.domain.fullyQualifiedName);
_otherDetails.push({
key: 'Domain',
value: domainLink,
placeholderText: domain,
isLink: true,
openInNewTab: false,
});
}
if ( if (
source.entityType !== EntityType.GLOSSARY_TERM && source.entityType !== EntityType.GLOSSARY_TERM &&
source.entityType !== EntityType.TAG source.entityType !== EntityType.TAG

View File

@ -49,7 +49,7 @@ const LeftSidebar = () => {
label: ( label: (
<Tooltip <Tooltip
overlayClassName="left-panel-tooltip" overlayClassName="left-panel-tooltip"
placement="right" placement="topLeft"
title={ title={
<Typography.Text className="left-panel-label"> <Typography.Text className="left-panel-label">
{t('label.govern')} {t('label.govern')}

View File

@ -146,10 +146,12 @@ const MyDataWidgetInternal = ({
); );
}) })
) : ( ) : (
<span className="text-sm">
<Transi18next <Transi18next
i18nKey="message.no-owned-data" i18nKey="message.no-owned-data"
renderElement={<Link to={ROUTES.EXPLORE} />} renderElement={<Link to={ROUTES.EXPLORE} />}
/> />
</span>
)} )}
</div> </div>
</> </>

View File

@ -168,7 +168,7 @@ const NotificationBox = ({
</div> </div>
) : ( ) : (
<List <List
className="h-min-64" className="notification-content-container"
dataSource={notificationDropDownList} dataSource={notificationDropDownList}
footer={ footer={
<Button block href={viewAllPath} type="link"> <Button block href={viewAllPath} type="link">

View File

@ -26,3 +26,12 @@
} }
} }
} }
.notification-content-container {
min-height: 16rem;
.ant-spin-nested-loading {
height: 200px;
overflow-y: scroll;
}
}

View File

@ -315,22 +315,22 @@ const SchemaTable = ({
tableConstraints, tableConstraints,
})} })}
{/* If we do not have displayName name only be shown in the bold from the below code */} {/* If we do not have displayName name only be shown in the bold from the below code */}
{!isEmpty(displayName) ? (
<Typography.Text <Typography.Text
className="m-b-0 d-block text-grey-muted" className="m-b-0 d-block text-grey-muted"
data-testid="column-name"> data-testid="column-name">
{name} {name}
</Typography.Text> </Typography.Text>
) : null} </div>
{!isEmpty(displayName) ? (
{/* It will render displayName fallback to name */} // It will render displayName fallback to name
<Typography.Text <Typography.Text
className="m-b-0 d-block" className="m-b-0 d-block"
data-testid="column-display-name" data-testid="column-display-name"
ellipsis={{ tooltip: true }}> ellipsis={{ tooltip: true }}>
{getEntityName(record)} {getEntityName(record)}
</Typography.Text> </Typography.Text>
</div> ) : null}
<Icon <Icon
className="hover-cell-icon text-left m-t-xss" className="hover-cell-icon text-left m-t-xss"
component={IconEdit} component={IconEdit}

View File

@ -221,7 +221,7 @@ describe('Test EntityTable Component', () => {
wrapper: MemoryRouter, wrapper: MemoryRouter,
}); });
const columnNames = await screen.findAllByTestId('column-display-name'); const columnNames = await screen.findAllByTestId('column-name');
expect(columnNames).toHaveLength(3); expect(columnNames).toHaveLength(3);
@ -244,12 +244,12 @@ describe('Test EntityTable Component', () => {
const columnDisplayName = await screen.findAllByTestId( const columnDisplayName = await screen.findAllByTestId(
'column-display-name' 'column-display-name'
); );
const columnName = await screen.findByTestId('column-name'); const columnName = await screen.findAllByTestId('column-name');
expect(columnDisplayName[0]).toBeInTheDocument(); expect(columnDisplayName[0]).toBeInTheDocument();
expect(columnName).toBeInTheDocument(); expect(columnName[0]).toBeInTheDocument();
expect(columnDisplayName[0].textContent).toBe('Comments'); expect(columnDisplayName[0].textContent).toBe('Comments');
expect(columnName.textContent).toBe('comments'); expect(columnName[0].textContent).toBe('comments');
}); });
}); });

View File

@ -11,6 +11,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { ExploreSearchIndex } from '../Explore/explore.interface';
export interface SearchDropdownProps { export interface SearchDropdownProps {
label: string; label: string;
isSuggestionsLoading?: boolean; isSuggestionsLoading?: boolean;
@ -20,6 +22,7 @@ export interface SearchDropdownProps {
highlight?: boolean; highlight?: boolean;
showProfilePicture?: boolean; showProfilePicture?: boolean;
fixedOrderOptions?: boolean; fixedOrderOptions?: boolean;
index?: ExploreSearchIndex;
onChange: (values: SearchDropdownOption[], searchKey: string) => void; onChange: (values: SearchDropdownOption[], searchKey: string) => void;
onGetInitialOptions?: (searchKey: string) => void; onGetInitialOptions?: (searchKey: string) => void;
onSearch: (searchText: string, searchKey: string) => void; onSearch: (searchText: string, searchKey: string) => void;

View File

@ -36,6 +36,7 @@ const mockProps: SearchDropdownProps = {
selectedKeys: [{ key: 'User 1', label: 'User 1' }], selectedKeys: [{ key: 'User 1', label: 'User 1' }],
onChange: mockOnChange, onChange: mockOnChange,
onSearch: mockOnSearch, onSearch: mockOnSearch,
index: 'table_search_index' as SearchDropdownProps['index'],
}; };
jest.mock('lodash', () => ({ jest.mock('lodash', () => ({

View File

@ -37,6 +37,7 @@ import React, {
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as DropDown } from '../../assets/svg/DropDown.svg'; import { ReactComponent as DropDown } from '../../assets/svg/DropDown.svg';
import { tabsInfo } from '../../constants/explore.constants';
import { import {
generateSearchDropdownLabel, generateSearchDropdownLabel,
getSearchDropdownLabels, getSearchDropdownLabels,
@ -61,6 +62,7 @@ const SearchDropdown: FC<SearchDropdownProps> = ({
onChange, onChange,
onGetInitialOptions, onGetInitialOptions,
onSearch, onSearch,
index,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -170,6 +172,8 @@ const SearchDropdown: FC<SearchDropdownProps> = ({
const getDropdownBody = useCallback( const getDropdownBody = useCallback(
(menuNode: ReactNode) => { (menuNode: ReactNode) => {
const entityLabel = index && tabsInfo[index]?.label;
const isDomainKey = searchKey.startsWith('domain');
if (isSuggestionsLoading) { if (isSuggestionsLoading) {
return ( return (
<Row align="middle" className="p-y-sm" justify="center"> <Row align="middle" className="p-y-sm" justify="center">
@ -185,12 +189,18 @@ const SearchDropdown: FC<SearchDropdownProps> = ({
) : ( ) : (
<Row align="middle" className="m-y-sm" justify="center"> <Row align="middle" className="m-y-sm" justify="center">
<Col> <Col>
<Typography.Text>{t('message.no-data-available')}</Typography.Text> <Typography.Text className="m-x-sm">
{isDomainKey && entityLabel
? t('message.no-domain-assigned-to-entity', {
entity: entityLabel,
})
: t('message.no-data-available')}
</Typography.Text>
</Col> </Col>
</Row> </Row>
); );
}, },
[isSuggestionsLoading, options, selectedOptions] [isSuggestionsLoading, options, selectedOptions, index, searchKey]
); );
const dropdownCardComponent = useCallback( const dropdownCardComponent = useCallback(

View File

@ -339,7 +339,7 @@ const Services = ({ serviceName }: ServicesProps) => {
serviceName serviceName
)}> )}>
<Typography.Text <Typography.Text
className="text-base text-grey-body font-medium truncate w-48" className="text-base text-grey-body font-medium truncate w-48 d-inline-block"
data-testid={`service-name-${service.name}`} data-testid={`service-name-${service.name}`}
title={getEntityName(service)}> title={getEntityName(service)}>
{getEntityName(service)} {getEntityName(service)}

View File

@ -1,3 +1,5 @@
import { EntityReference } from '../../generated/entity/type';
/* /*
* Copyright 2023 Collate. * Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -12,9 +14,9 @@
*/ */
export interface TeamsSelectableProps { export interface TeamsSelectableProps {
showTeamsAlert?: boolean; showTeamsAlert?: boolean;
onSelectionChange?: (teams: string[]) => void; onSelectionChange?: (teams: EntityReference[]) => void;
filterJoinable?: boolean; filterJoinable?: boolean;
placeholder?: string; placeholder?: string;
selectedTeams?: string[]; selectedTeams?: EntityReference[];
maxValueCount?: number; maxValueCount?: number;
} }

View File

@ -16,6 +16,7 @@ import { BaseOptionType } from 'antd/lib/select';
import { t } from 'i18next'; import { t } from 'i18next';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { TeamHierarchy } from '../../generated/entity/teams/teamHierarchy'; import { TeamHierarchy } from '../../generated/entity/teams/teamHierarchy';
import { EntityReference } from '../../generated/entity/type';
import { getTeamsHierarchy } from '../../rest/teamsAPI'; import { getTeamsHierarchy } from '../../rest/teamsAPI';
import { getEntityName } from '../../utils/EntityUtils'; import { getEntityName } from '../../utils/EntityUtils';
import { showErrorToast } from '../../utils/ToastUtils'; import { showErrorToast } from '../../utils/ToastUtils';
@ -31,19 +32,23 @@ const TeamsSelectable = ({
selectedTeams, selectedTeams,
maxValueCount, maxValueCount,
}: TeamsSelectableProps) => { }: TeamsSelectableProps) => {
const [value, setValue] = useState<Array<string>>();
const [noTeam, setNoTeam] = useState<boolean>(false); const [noTeam, setNoTeam] = useState<boolean>(false);
const [teams, setTeams] = useState<Array<TeamHierarchy>>([]); const [teams, setTeams] = useState<Array<TeamHierarchy>>([]);
const onChange = (newValue: string[]) => { const onChange = (newValue: { label: string; value: string }[]) => {
onSelectionChange && onSelectionChange(newValue); onSelectionChange &&
setValue(newValue); onSelectionChange(
newValue.map(
(val) =>
({
id: val.value,
displayName: val.label,
type: 'team',
} as EntityReference)
)
);
}; };
useEffect(() => {
setValue(selectedTeams ?? []);
}, [selectedTeams]);
const loadOptions = () => { const loadOptions = () => {
getTeamsHierarchy(filterJoinable) getTeamsHierarchy(filterJoinable)
.then((res) => { .then((res) => {
@ -84,10 +89,18 @@ const TeamsSelectable = ({
}); });
}, [teams]); }, [teams]);
const selectedTeamsInternal = useMemo(() => {
return selectedTeams?.map((selectedTeam) => ({
label: getEntityName(selectedTeam),
value: selectedTeam.id,
}));
}, [selectedTeams]);
return ( return (
<> <>
<TreeSelect <TreeSelect
allowClear allowClear
labelInValue
multiple multiple
showSearch showSearch
treeDefaultExpandAll treeDefaultExpandAll
@ -100,7 +113,7 @@ const TeamsSelectable = ({
treeData={teamsTree} treeData={teamsTree}
treeLine={{ showLeafIcon }} treeLine={{ showLeafIcon }}
treeNodeFilterProp="title" treeNodeFilterProp="title"
value={value} value={selectedTeamsInternal}
onChange={onChange} onChange={onChange}
/> />
{noTeam && ( {noTeam && (

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { CheckOutlined } from '@ant-design/icons'; import { CheckOutlined } from '@ant-design/icons';
import { Dropdown, Tag, Typography } from 'antd'; import { Dropdown, Tag, Tooltip, Typography } from 'antd';
import { ItemType } from 'antd/lib/menu/hooks/useItems'; import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import React, { import React, {
@ -293,21 +293,20 @@ export const UserProfileIcon = () => {
{isImgUrlValid ? ( {isImgUrlValid ? (
<img <img
alt="user" alt="user"
className="profile-image circle" className="app-bar-user-avatar"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
src={profilePicture ?? ''} src={profilePicture ?? ''}
width={36}
onError={handleOnImageError} onError={handleOnImageError}
/> />
) : ( ) : (
<Avatar name={userName} type="circle" width="36" /> <Avatar name={userName} type="circle" width="36" />
)} )}
<div className="d-flex flex-col flex-1"> <div className="d-flex flex-col">
<Typography.Text <Tooltip title={getEntityName(currentUser)}>
className="usename w-28" <Typography.Text className="username truncate w-max-112">
ellipsis={{ tooltip: true }}>
{getEntityName(currentUser)} {getEntityName(currentUser)}
</Typography.Text> </Typography.Text>
</Tooltip>
<Typography.Text <Typography.Text
className="text-grey-muted text-xs w-28" className="text-grey-muted text-xs w-28"
ellipsis={{ tooltip: true }}> ellipsis={{ tooltip: true }}>

View File

@ -23,6 +23,7 @@ import {
DE_ACTIVE_COLOR, DE_ACTIVE_COLOR,
ICON_DIMENSION, ICON_DIMENSION,
} from '../../../../constants/constants'; } from '../../../../constants/constants';
import { EntityReference } from '../../../../generated/entity/type';
import { useAuth } from '../../../../hooks/authHooks'; import { useAuth } from '../../../../hooks/authHooks';
import { getNonDeletedTeams } from '../../../../utils/CommonUtils'; import { getNonDeletedTeams } from '../../../../utils/CommonUtils';
import { UserProfileTeamsProps } from './UserProfileTeams.interface'; import { UserProfileTeamsProps } from './UserProfileTeams.interface';
@ -35,11 +36,11 @@ const UserProfileTeams = ({
const { isAdminUser } = useAuth(); const { isAdminUser } = useAuth();
const [isTeamsEdit, setIsTeamsEdit] = useState(false); const [isTeamsEdit, setIsTeamsEdit] = useState(false);
const [selectedTeams, setSelectedTeams] = useState<string[]>([]); const [selectedTeams, setSelectedTeams] = useState<EntityReference[]>([]);
const handleTeamsSave = () => { const handleTeamsSave = () => {
updateUserDetails({ updateUserDetails({
teams: selectedTeams.map((teamId) => ({ id: teamId, type: 'team' })), teams: selectedTeams.map((teamId) => ({ id: teamId.id, type: 'team' })),
}); });
setIsTeamsEdit(false); setIsTeamsEdit(false);
@ -57,7 +58,7 @@ const UserProfileTeams = ({
); );
useEffect(() => { useEffect(() => {
setSelectedTeams(getNonDeletedTeams(teams ?? []).map((team) => team.id)); setSelectedTeams(getNonDeletedTeams(teams ?? []));
}, [teams]); }, [teams]);
return ( return (

View File

@ -113,6 +113,9 @@ const DomainSelectableList = ({
customTagRenderer={DomainListItemRenderer} customTagRenderer={DomainListItemRenderer}
fetchOptions={fetchOptions} fetchOptions={fetchOptions}
multiSelect={false} multiSelect={false}
removeIconTooltipLabel={t('label.remove-entity', {
entity: t('label.domain-lowercase'),
})}
searchPlaceholder={t('label.search-for-type', { searchPlaceholder={t('label.search-for-type', {
type: t('label.domain'), type: t('label.domain'),
})} })}

View File

@ -20,6 +20,7 @@ import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg'; import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
import { ReactComponent as IconExternalLink } from '../../../assets/svg/external-links.svg'; import { ReactComponent as IconExternalLink } from '../../../assets/svg/external-links.svg';
import { ReactComponent as DomainIcon } from '../../../assets/svg/ic-domain.svg';
import { ReactComponent as IconTeamsGrey } from '../../../assets/svg/teams-grey.svg'; import { ReactComponent as IconTeamsGrey } from '../../../assets/svg/teams-grey.svg';
import { DE_ACTIVE_COLOR } from '../../../constants/constants'; import { DE_ACTIVE_COLOR } from '../../../constants/constants';
import { Tag } from '../../../generated/entity/classification/tag'; import { Tag } from '../../../generated/entity/classification/tag';
@ -186,6 +187,20 @@ const EntitySummaryDetails = ({
break; break;
case 'Domain':
{
retVal = (
<DomainIcon
className="d-flex"
color={DE_ACTIVE_COLOR}
height={16}
name="folder"
width={16}
/>
);
}
break;
default: default:
{ {
retVal = ( retVal = (

View File

@ -38,6 +38,7 @@ export const SelectableList = ({
searchPlaceholder, searchPlaceholder,
customTagRenderer, customTagRenderer,
searchBarDataTestId, searchBarDataTestId,
removeIconTooltipLabel,
}: SelectableListProps) => { }: SelectableListProps) => {
const [uniqueOptions, setUniqueOptions] = useState<EntityReference[]>([]); const [uniqueOptions, setUniqueOptions] = useState<EntityReference[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
@ -230,7 +231,10 @@ export const SelectableList = ({
<Checkbox checked={selectedItemsInternal.has(item.id)} /> <Checkbox checked={selectedItemsInternal.has(item.id)} />
) : ( ) : (
selectedItemsInternal.has(item.id) && ( selectedItemsInternal.has(item.id) && (
<RemoveIcon removeOwner={handleRemoveClick} /> <RemoveIcon
removeIconTooltipLabel={removeIconTooltipLabel}
removeOwner={handleRemoveClick}
/>
) )
) )
} }
@ -249,14 +253,23 @@ export const SelectableList = ({
); );
}; };
const RemoveIcon = ({ removeOwner }: { removeOwner?: () => void }) => { const RemoveIcon = ({
removeOwner,
removeIconTooltipLabel,
}: {
removeOwner?: () => void;
removeIconTooltipLabel?: string;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Tooltip <Tooltip
title={t('label.remove-entity', { title={
removeIconTooltipLabel ??
t('label.remove-entity', {
entity: t('label.owner-lowercase'), entity: t('label.owner-lowercase'),
})}> })
}>
<SVGIcons <SVGIcons
data-testid="remove-owner" data-testid="remove-owner"
icon={Icons.ICON_REMOVE_COLORED} icon={Icons.ICON_REMOVE_COLORED}

View File

@ -26,4 +26,5 @@ export interface SelectableListProps {
searchPlaceholder?: string; searchPlaceholder?: string;
customTagRenderer?: (props: EntityReference) => ReactNode; customTagRenderer?: (props: EntityReference) => ReactNode;
searchBarDataTestId?: string; searchBarDataTestId?: string;
removeIconTooltipLabel?: string;
} }

View File

@ -23,7 +23,7 @@
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
.usename { .username {
font-weight: 500; font-weight: 500;
line-height: 21px; line-height: 21px;
} }

View File

@ -43,7 +43,8 @@ type Fields =
| 'serviceType' | 'serviceType'
| 'displayName' | 'displayName'
| 'deleted' | 'deleted'
| 'service'; | 'service'
| 'domain';
export type SourceType = ( export type SourceType = (
| Pick< | Pick<

View File

@ -14,6 +14,7 @@
import { t } from 'i18next'; import { t } from 'i18next';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import Qs from 'qs'; import Qs from 'qs';
import { CSSProperties } from 'react';
import { COOKIE_VERSION } from '../components/Modals/WhatsNewModal/whatsNewData'; import { COOKIE_VERSION } from '../components/Modals/WhatsNewModal/whatsNewData';
import { EntityTabs } from '../enums/entity.enum'; import { EntityTabs } from '../enums/entity.enum';
import { SearchIndex } from '../enums/search.enum'; import { SearchIndex } from '../enums/search.enum';
@ -835,3 +836,7 @@ export const ICON_DIMENSION = {
with: 14, with: 14,
height: 14, height: 14,
}; };
export const COMMON_ICON_STYLES: CSSProperties = {
verticalAlign: 'middle',
};

View File

@ -112,6 +112,12 @@ export const tabsInfo: { [K in ExploreSearchIndex]: ExploreTabInfo } = {
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'containers', path: 'containers',
}, },
[SearchIndex.SEARCH_INDEX]: {
label: i18n.t('label.search-index-plural'),
sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD,
path: 'searchIndexes',
},
[SearchIndex.GLOSSARY]: { [SearchIndex.GLOSSARY]: {
label: i18n.t('label.glossary-plural'), label: i18n.t('label.glossary-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
@ -124,12 +130,6 @@ export const tabsInfo: { [K in ExploreSearchIndex]: ExploreTabInfo } = {
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'tags', path: 'tags',
}, },
[SearchIndex.SEARCH_INDEX]: {
label: i18n.t('label.search-index-plural'),
sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD,
path: 'searchIndexes',
},
}; };
export const COMMON_FILTERS_FOR_DIFFERENT_TABS = [ export const COMMON_FILTERS_FOR_DIFFERENT_TABS = [

View File

@ -251,6 +251,7 @@
"data-model-type": "Datenmodelltyp", "data-model-type": "Datenmodelltyp",
"data-product": "Datenprodukt", "data-product": "Datenprodukt",
"data-product-plural": "Datenprodukte", "data-product-plural": "Datenprodukte",
"data-profiler-metrics": "Data Profiler Metrics",
"data-proportion-plural": "Datenverhältnisse", "data-proportion-plural": "Datenverhältnisse",
"data-quality": "Datenqualität", "data-quality": "Datenqualität",
"data-quality-test": "Datenqualitätstest", "data-quality-test": "Datenqualitätstest",
@ -1362,6 +1363,7 @@
"no-data": "Keine Daten", "no-data": "Keine Daten",
"no-data-available": "Keine Daten verfügbar.", "no-data-available": "Keine Daten verfügbar.",
"no-data-available-for-selected-filter": "Keine Daten gefunden. Versuchen Sie, die Filter zu ändern.", "no-data-available-for-selected-filter": "Keine Daten gefunden. Versuchen Sie, die Filter zu ändern.",
"no-domain-assigned-to-entity": "No Domains are Assigned to {{entity}}",
"no-entity-activity-message": "Es gibt noch keine Aktivität auf {{entity}}. Starten Sie ein Gespräch, indem Sie auf das", "no-entity-activity-message": "Es gibt noch keine Aktivität auf {{entity}}. Starten Sie ein Gespräch, indem Sie auf das",
"no-entity-available-with-name": "Keine {{entity}} mit dem Namen verfügbar", "no-entity-available-with-name": "Keine {{entity}} mit dem Namen verfügbar",
"no-entity-data-available": "Keine {{entity}}-Daten verfügbar.", "no-entity-data-available": "Keine {{entity}}-Daten verfügbar.",

View File

@ -251,6 +251,7 @@
"data-model-type": "Data Model Type", "data-model-type": "Data Model Type",
"data-product": "Data Product", "data-product": "Data Product",
"data-product-plural": "Data Products", "data-product-plural": "Data Products",
"data-profiler-metrics": "Data Profiler Metrics",
"data-proportion-plural": "Data Proportions", "data-proportion-plural": "Data Proportions",
"data-quality": "Data Quality", "data-quality": "Data Quality",
"data-quality-test": "Data Quality Test", "data-quality-test": "Data Quality Test",
@ -1362,6 +1363,7 @@
"no-data": "No data", "no-data": "No data",
"no-data-available": "No data available.", "no-data-available": "No data available.",
"no-data-available-for-selected-filter": "No data found. Try changing the filters.", "no-data-available-for-selected-filter": "No data found. Try changing the filters.",
"no-domain-assigned-to-entity": "No Domains are Assigned to {{entity}}",
"no-entity-activity-message": "There is no activity on the {{entity}} yet. Start a conversation by clicking on the", "no-entity-activity-message": "There is no activity on the {{entity}} yet. Start a conversation by clicking on the",
"no-entity-available-with-name": "No {{entity}} available with name", "no-entity-available-with-name": "No {{entity}} available with name",
"no-entity-data-available": "No {{entity}} data available.", "no-entity-data-available": "No {{entity}} data available.",

View File

@ -251,6 +251,7 @@
"data-model-type": "Data Model Type", "data-model-type": "Data Model Type",
"data-product": "Data Product", "data-product": "Data Product",
"data-product-plural": "Data Products", "data-product-plural": "Data Products",
"data-profiler-metrics": "Data Profiler Metrics",
"data-proportion-plural": "Data Proportions", "data-proportion-plural": "Data Proportions",
"data-quality": "Data Quality", "data-quality": "Data Quality",
"data-quality-test": "Prueba de calidad de datos", "data-quality-test": "Prueba de calidad de datos",
@ -1362,6 +1363,7 @@
"no-data": "No hay datos", "no-data": "No hay datos",
"no-data-available": "No hay datos disponibles.", "no-data-available": "No hay datos disponibles.",
"no-data-available-for-selected-filter": "No se encontraron datos. Intenta cambiar los filtros.", "no-data-available-for-selected-filter": "No se encontraron datos. Intenta cambiar los filtros.",
"no-domain-assigned-to-entity": "No Domains are Assigned to {{entity}}",
"no-entity-activity-message": "No hay actividad en {{entity}} todavía. Comienza una conversación haciendo clic en", "no-entity-activity-message": "No hay actividad en {{entity}} todavía. Comienza una conversación haciendo clic en",
"no-entity-available-with-name": "No hay {{entity}} disponible con el nombre", "no-entity-available-with-name": "No hay {{entity}} disponible con el nombre",
"no-entity-data-available": "No hay datos disponibles de {{entity}}.", "no-entity-data-available": "No hay datos disponibles de {{entity}}.",

View File

@ -251,6 +251,7 @@
"data-model-type": "Type de Modèle de Données", "data-model-type": "Type de Modèle de Données",
"data-product": "Produit de Données", "data-product": "Produit de Données",
"data-product-plural": "Produits de Données", "data-product-plural": "Produits de Données",
"data-profiler-metrics": "Data Profiler Metrics",
"data-proportion-plural": "Proportions des Données", "data-proportion-plural": "Proportions des Données",
"data-quality": "Qualité des Données", "data-quality": "Qualité des Données",
"data-quality-test": "Test de Qualité des Données", "data-quality-test": "Test de Qualité des Données",
@ -1362,6 +1363,7 @@
"no-data": "Aucune donnée", "no-data": "Aucune donnée",
"no-data-available": "Aucune donnée disponible", "no-data-available": "Aucune donnée disponible",
"no-data-available-for-selected-filter": "Aucune donnée trouvée. Essayez de modifier les filtres.", "no-data-available-for-selected-filter": "Aucune donnée trouvée. Essayez de modifier les filtres.",
"no-domain-assigned-to-entity": "No Domains are Assigned to {{entity}}",
"no-entity-activity-message": "Il n'y a aucune activité sur {{entity}} pour le moment. Démarrez une conversation en cliquant sur le bouton", "no-entity-activity-message": "Il n'y a aucune activité sur {{entity}} pour le moment. Démarrez une conversation en cliquant sur le bouton",
"no-entity-available-with-name": "Aucun {{entity}} disponible avec le nom", "no-entity-available-with-name": "Aucun {{entity}} disponible avec le nom",
"no-entity-data-available": "Aucun {{entity}} disponible.", "no-entity-data-available": "Aucun {{entity}} disponible.",

View File

@ -251,6 +251,7 @@
"data-model-type": "Data Model Type", "data-model-type": "Data Model Type",
"data-product": "Data Product", "data-product": "Data Product",
"data-product-plural": "Data Products", "data-product-plural": "Data Products",
"data-profiler-metrics": "Data Profiler Metrics",
"data-proportion-plural": "Data Proportions", "data-proportion-plural": "Data Proportions",
"data-quality": "Data Quality", "data-quality": "Data Quality",
"data-quality-test": "データ品質テスト", "data-quality-test": "データ品質テスト",
@ -1362,6 +1363,7 @@
"no-data": "データがありません", "no-data": "データがありません",
"no-data-available": "利用できるデータがありません", "no-data-available": "利用できるデータがありません",
"no-data-available-for-selected-filter": "No data found. Try changing the filters.", "no-data-available-for-selected-filter": "No data found. Try changing the filters.",
"no-domain-assigned-to-entity": "No Domains are Assigned to {{entity}}",
"no-entity-activity-message": "There is no activity on the {{entity}} yet. Start a conversation by clicking on the", "no-entity-activity-message": "There is no activity on the {{entity}} yet. Start a conversation by clicking on the",
"no-entity-available-with-name": "No {{entity}} available with name", "no-entity-available-with-name": "No {{entity}} available with name",
"no-entity-data-available": "No {{entity}} data available.", "no-entity-data-available": "No {{entity}} data available.",

View File

@ -251,6 +251,7 @@
"data-model-type": "Data Model Type", "data-model-type": "Data Model Type",
"data-product": "Data Product", "data-product": "Data Product",
"data-product-plural": "Data Products", "data-product-plural": "Data Products",
"data-profiler-metrics": "Data Profiler Metrics",
"data-proportion-plural": "Data Proportions", "data-proportion-plural": "Data Proportions",
"data-quality": "Data Quality", "data-quality": "Data Quality",
"data-quality-test": "Teste de qualidade de dados", "data-quality-test": "Teste de qualidade de dados",
@ -1362,6 +1363,7 @@
"no-data": "Nenhum dado", "no-data": "Nenhum dado",
"no-data-available": "Nenhum dado disponível.", "no-data-available": "Nenhum dado disponível.",
"no-data-available-for-selected-filter": "Nenhum dado encontrado. Tente alterar os filtros.", "no-data-available-for-selected-filter": "Nenhum dado encontrado. Tente alterar os filtros.",
"no-domain-assigned-to-entity": "No Domains are Assigned to {{entity}}",
"no-entity-activity-message": "Não há atividade em {{entity}} ainda. Inicie uma conversa clicando em", "no-entity-activity-message": "Não há atividade em {{entity}} ainda. Inicie uma conversa clicando em",
"no-entity-available-with-name": "Não há {{entity}} disponível com o nome", "no-entity-available-with-name": "Não há {{entity}} disponível com o nome",
"no-entity-data-available": "Nenhum dado disponível para {{entity}}.", "no-entity-data-available": "Nenhum dado disponível para {{entity}}.",

View File

@ -251,6 +251,7 @@
"data-model-type": "Тип модели данных", "data-model-type": "Тип модели данных",
"data-product": "Data Product", "data-product": "Data Product",
"data-product-plural": "Data Products", "data-product-plural": "Data Products",
"data-profiler-metrics": "Data Profiler Metrics",
"data-proportion-plural": "Распределение данных", "data-proportion-plural": "Распределение данных",
"data-quality": "Качество данных", "data-quality": "Качество данных",
"data-quality-test": "Тест качества данных", "data-quality-test": "Тест качества данных",
@ -1362,6 +1363,7 @@
"no-data": "Нет данных", "no-data": "Нет данных",
"no-data-available": "Данные недоступны.", "no-data-available": "Данные недоступны.",
"no-data-available-for-selected-filter": "Данные не найдены. Попробуйте поменять фильтры.", "no-data-available-for-selected-filter": "Данные не найдены. Попробуйте поменять фильтры.",
"no-domain-assigned-to-entity": "No Domains are Assigned to {{entity}}",
"no-entity-activity-message": "В {{entity}} пока нет активности. Начните обсуждение, нажав на кнопку", "no-entity-activity-message": "В {{entity}} пока нет активности. Начните обсуждение, нажав на кнопку",
"no-entity-available-with-name": "Нет доступного {{entity}} с именем", "no-entity-available-with-name": "Нет доступного {{entity}} с именем",
"no-entity-data-available": "Данные {{entity}} недоступны.", "no-entity-data-available": "Данные {{entity}} недоступны.",

View File

@ -251,6 +251,7 @@
"data-model-type": "数据模型类型", "data-model-type": "数据模型类型",
"data-product": "数据产品", "data-product": "数据产品",
"data-product-plural": "数据产品", "data-product-plural": "数据产品",
"data-profiler-metrics": "Data Profiler Metrics",
"data-proportion-plural": "数据比例", "data-proportion-plural": "数据比例",
"data-quality": "数据质控", "data-quality": "数据质控",
"data-quality-test": "数据质控测试", "data-quality-test": "数据质控测试",
@ -1362,6 +1363,7 @@
"no-data": "没有数据", "no-data": "没有数据",
"no-data-available": "没有可用的数据", "no-data-available": "没有可用的数据",
"no-data-available-for-selected-filter": "未找到数据,请尝试更改筛选条件", "no-data-available-for-selected-filter": "未找到数据,请尝试更改筛选条件",
"no-domain-assigned-to-entity": "No Domains are Assigned to {{entity}}",
"no-entity-activity-message": "{{entity}}上还没有任何活动,单击开始对话", "no-entity-activity-message": "{{entity}}上还没有任何活动,单击开始对话",
"no-entity-available-with-name": "名称为{{entity}}不可用", "no-entity-available-with-name": "名称为{{entity}}不可用",
"no-entity-data-available": "没有可用的{{entity}}数据。", "no-entity-data-available": "没有可用的{{entity}}数据。",

View File

@ -738,3 +738,10 @@ a[href].link-text-grey,
padding-left: 0.25rem; padding-left: 0.25rem;
padding-right: 0.25rem; padding-right: 0.25rem;
} }
.app-bar-user-avatar {
display: inline-block;
height: 36px;
width: 36px;
border-radius: 50%;
}

View File

@ -65,3 +65,7 @@ button {
.ant-btn-group .ant-btn-primary:first-child:not(:last-child) { .ant-btn-group .ant-btn-primary:first-child:not(:last-child) {
border-right-color: #ffffff; border-right-color: #ffffff;
} }
.ant-btn-primary {
box-shadow: none;
}

View File

@ -95,6 +95,9 @@
.w-max-90 { .w-max-90 {
max-width: 90%; max-width: 90%;
} }
.w-max-112 {
max-width: 112px;
}
.w-max-200 { .w-max-200 {
max-width: 200px; max-width: 200px;
} }

View File

@ -34,15 +34,15 @@ import React from 'react';
import { ListItem } from 'react-awesome-query-builder'; import { ListItem } from 'react-awesome-query-builder';
import { LegendProps, Surface } from 'recharts'; import { LegendProps, Surface } from 'recharts';
import { SearchDropdownOption } from '../components/SearchDropdown/SearchDropdown.interface'; import { SearchDropdownOption } from '../components/SearchDropdown/SearchDropdown.interface';
import {
ENTITIES_SUMMARY_LIST,
WEB_SUMMARY_LIST,
} from '../constants/DataInsight.constants';
import { import {
GRAYED_OUT_COLOR, GRAYED_OUT_COLOR,
PLACEHOLDER_ROUTE_TAB, PLACEHOLDER_ROUTE_TAB,
ROUTES, ROUTES,
} from '../constants/constants'; } from '../constants/constants';
import {
ENTITIES_SUMMARY_LIST,
WEB_SUMMARY_LIST,
} from '../constants/DataInsight.constants';
import { KpiTargetType } from '../generated/api/dataInsight/kpi/createKpiRequest'; import { KpiTargetType } from '../generated/api/dataInsight/kpi/createKpiRequest';
import { import {
DataInsightChartResult, DataInsightChartResult,

View File

@ -180,15 +180,6 @@ export const getGlobalSettingsMenuWithPermission = (
key: 'services.mlModels', key: 'services.mlModels',
icon: <MlModelIcon className="side-panel-icons" />, icon: <MlModelIcon className="side-panel-icons" />,
}, },
{
label: i18next.t('label.metadata'),
isProtected: userPermissions.hasViewPermissions(
ResourceEntity.METADATA_SERVICE,
permissions
),
key: 'services.metadata',
icon: <OMLogo className="side-panel-icons w-4 h-4" />,
},
{ {
label: i18next.t('label.storage-plural'), label: i18next.t('label.storage-plural'),
isProtected: userPermissions.hasViewPermissions( isProtected: userPermissions.hasViewPermissions(
@ -207,6 +198,15 @@ export const getGlobalSettingsMenuWithPermission = (
key: 'services.search', key: 'services.search',
icon: <SearchOutlined className="side-panel-icons w-4 h-4" />, icon: <SearchOutlined className="side-panel-icons w-4 h-4" />,
}, },
{
label: i18next.t('label.metadata'),
isProtected: userPermissions.hasViewPermissions(
ResourceEntity.METADATA_SERVICE,
permissions
),
key: 'services.metadata',
icon: <OMLogo className="side-panel-icons w-4 h-4" />,
},
], ],
}, },