UI : Add localization (#9309)

* UI : Add localization

* address comments
This commit is contained in:
Sachin Chaurasiya 2022-12-15 19:24:47 +05:30 committed by GitHub
parent d2deb38766
commit c562be6f78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 418 additions and 630 deletions

View File

@ -301,10 +301,6 @@ jest.mock(
)
);
jest.mock('../../utils/EntityVersionUtils', () => ({
getFeedSummary: jest.fn().mockImplementation(() => <p>EntityVersionUtils</p>),
}));
jest.mock('../../utils/ServiceUtils', () => ({
getAllServices: jest
.fn()

View File

@ -286,7 +286,7 @@ const TeamDetailsV1 = ({
disabled={!entityPermissions.EditAll}
icon={
<SVGIcons
alt="Remove"
alt={t('label.remove')}
className="tw-w-4 tw-mb-2.5"
icon={Icons.ICON_REMOVE}
/>
@ -554,7 +554,7 @@ const TeamDetailsV1 = ({
showErrorToast(
error as AxiosError,
t('server.entity-fetch-error', {
entity: 'User Permissions',
entity: t('label.user-permissions'),
})
);
} finally {
@ -639,7 +639,9 @@ const TeamDetailsV1 = ({
);
const restoreIcon = useMemo(
() => <SVGIcons alt="Restore" icon={Icons.RESTORE} width="16px" />,
() => (
<SVGIcons alt={t('label.restore')} icon={Icons.RESTORE} width="16px" />
),
[currentTeam.isJoinable]
);
@ -1366,7 +1368,7 @@ const TeamDetailsV1 = ({
okText={t('label.confirm')}
title={`${t('label.remove-entity', {
entity: getEntityName(selectedEntity?.record),
})} ${t('label.from')} ${getEntityName(currentTeam)}`}
})} ${t('label.from-lowercase')} ${getEntityName(currentTeam)}`}
visible={!isUndefined(selectedEntity.record)}
onCancel={() => setEntity(undefined)}
onOk={async () => {
@ -1380,7 +1382,7 @@ const TeamDetailsV1 = ({
{t('label.sure-to-remove')}{' '}
{`${getEntityName(
selectedEntity.record
)} t('label.from') ${getEntityName(currentTeam)}?`}
)} t('label.from-lowercase') ${getEntityName(currentTeam)}?`}
</Typography.Text>
</Modal>
)}

View File

@ -81,7 +81,7 @@
"sure-to-remove": "Are you sure you want to remove the",
"confirm": "Confirm",
"remove-entity": "Remove {{entity}}",
"from": "from",
"from-lowercase": "from",
"add": "Add",
"add-policy": "Add Policy",
"go-back": "Go Back",
@ -473,6 +473,25 @@
"retention-size": "retention-size",
"clean-up-policies": "clean-up policies",
"maximum-size": "maximum size",
"add-term": "Add Term",
"accept-suggestion": "Accept Suggestion",
"edit-amp-accept-suggestion": "Edit & Accept Suggestion",
"to-lowercase": "to",
"of-lowercase": "of",
"description-lowercase": "description",
"started-following": "Started following",
"unfollowed": "Unfollowed",
"data-asset-has-been-action-type": "Data asset has been {{actionType}}",
"deleted-lowercase": "deleted",
"restored-lowercase": "restored",
"edited": "Edited",
"added-lowercase": "added",
"updated-lowercase": "updated",
"has-been-action-type-lowercase": "has been {{actionType}}",
"rules": "Rules",
"manage-rule": "Manage Rule",
"users": "Users",
"user-permissions": "User Permissions",
"openmetadata": "OpenMetadata",
"activity-feeds": "Activity Feeds",
"search": "Search",
@ -504,13 +523,20 @@
"search-entity": "Search {{entity}}",
"more": "More",
"update": "Update",
"reply-in-conversation": "Reply in conversation",
"older-reply-lowercase": "older reply",
"older-replies-lowercase": "older replies",
"created-a-task-lowercase": "created a task",
"posted-on-lowercase": "posted on",
"announcement": "Announcement",
"conversation": "Conversation",
"url-lowercase": "url",
"schemas": "Schemas",
"deleted": "Deleted {{entity}}",
"test-plural": "Tests",
"test": "Test",
"profiler": "Profiler",
"here-lowercase": "here",
"insert": "Insert",
"update": "Update",
"table-profile": "Table Profile",
"column-profile": "Column Profile",
"applied-advanced-search": "Applied advanced search"

View File

@ -81,7 +81,7 @@
"sure-to-remove": "Etes-vous sûr de vouloir supprimer le",
"confirm": "Confirmer",
"remove-entity": "Supprimer {{entity}}",
"from": "de",
"from-lowercase": "de",
"add": "Ajouter",
"add-policy": "Ajouter une Police",
"go-back": "Retour en arrière",

View File

@ -108,6 +108,12 @@ jest.mock('../../../utils/ToastUtils', () => ({
showErrorToast: jest.fn().mockReturnValue(''),
}));
jest.mock('react-i18next', () => ({
useTranslation: jest.fn().mockReturnValue({
t: (label: string) => label,
}),
}));
describe('Test Policy details page', () => {
it('Should render the policy details page component', async () => {
render(<PoliciesDetailPage />);
@ -118,11 +124,11 @@ describe('Test Policy details page', () => {
const description = await screen.findByTestId('description-data');
const rulesTab = await screen.findByText('Rules');
const rulesTab = await screen.findByText('label.rules');
const rolesTab = await screen.findByText('Roles');
const rolesTab = await screen.findByText('label.roles');
const teamsTab = await screen.findByText('Teams');
const teamsTab = await screen.findByText('label.teams');
expect(container).toBeInTheDocument();

View File

@ -21,18 +21,16 @@ import {
Modal,
Row,
Space,
Table,
Tabs,
Tooltip,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty, isUndefined, startCase } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useHistory, useParams } from 'react-router-dom';
import { useHistory, useParams } from 'react-router-dom';
import {
getPolicyByName,
getRoleByName,
@ -54,10 +52,6 @@ import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../../../constants/GlobalSettings.constants';
import {
NO_PERMISSION_FOR_ACTION,
NO_PERMISSION_TO_VIEW,
} from '../../../constants/HelperTextUtil';
import { EntityType } from '../../../enums/entity.enum';
import { Rule } from '../../../generated/api/policies/createPolicy';
import { Policy } from '../../../generated/entity/policies/policy';
@ -67,105 +61,17 @@ import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils';
import {
getAddPolicyRulePath,
getEditPolicyRulePath,
getRoleWithFqnPath,
getSettingPath,
getTeamsWithFqnPath,
} from '../../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import './PoliciesDetail.less';
import PoliciesDetailsList from './PoliciesDetailsList.component';
const { TabPane } = Tabs;
type Attribute = 'roles' | 'teams';
const List = ({
list,
type,
onDelete,
hasAccess,
}: {
list: EntityReference[];
type: 'role' | 'team';
onDelete: (record: EntityReference) => void;
hasAccess: boolean;
}) => {
const columns: ColumnsType<EntityReference> = useMemo(() => {
return [
{
title: 'Name',
dataIndex: 'name',
width: '200px',
key: 'name',
render: (_, record) => {
let link = '';
switch (type) {
case 'role':
link = getRoleWithFqnPath(record.fullyQualifiedName || '');
break;
case 'team':
link = getTeamsWithFqnPath(record.fullyQualifiedName || '');
break;
default:
break;
}
return (
<Link className="hover:tw-underline tw-cursor-pointer" to={link}>
{getEntityName(record)}
</Link>
);
},
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
render: (_, record) => (
<RichTextEditorPreviewer markdown={record?.description || ''} />
),
},
{
title: 'Actions',
dataIndex: 'actions',
width: '80px',
key: 'actions',
render: (_, record) => {
return (
<Tooltip title={hasAccess ? 'Remove' : NO_PERMISSION_FOR_ACTION}>
<Button
data-testid={`remove-action-${getEntityName(record)}`}
disabled={!hasAccess}
type="text"
onClick={() => onDelete(record)}>
<SVGIcons
alt="remove"
icon={Icons.ICON_REMOVE}
title="Remove"
/>
</Button>
</Tooltip>
);
},
},
];
}, []);
return (
<Table
bordered
className="list-table"
columns={columns}
dataSource={list}
pagination={false}
size="small"
/>
);
};
const PoliciesDetailPage = () => {
const { t } = useTranslation();
const history = useHistory();
@ -191,7 +97,7 @@ const PoliciesDetailPage = () => {
const breadcrumb = useMemo(
() => [
{
name: 'Policies',
name: t('label.policies'),
url: policiesPath,
},
{
@ -402,8 +308,8 @@ const PoliciesDetailPage = () => {
<Tooltip
title={
policyPermission.EditAll
? 'Manage Rule'
: NO_PERMISSION_FOR_ACTION
? t('label.manage-rule')
: t('message.no-permission-for-action')
}>
<Button
data-testid={`manage-button-${rule.name}`}
@ -473,7 +379,7 @@ const PoliciesDetailPage = () => {
/>
<Tabs defaultActiveKey="rules">
<TabPane key="rules" tab="Rules">
<TabPane key="rules" tab={t('label.rules')}>
{isEmpty(policy.rules) ? (
<ErrorPlaceHolder>
<p>{t('label.no-rule-found')}</p>
@ -486,7 +392,7 @@ const PoliciesDetailPage = () => {
title={
policyPermission.EditAll
? t('label.add-rule')
: NO_PERMISSION_FOR_ACTION
: t('message.no-permission-for-action')
}>
<Button
data-testid="add-rule"
@ -596,8 +502,8 @@ const PoliciesDetailPage = () => {
</Space>
)}
</TabPane>
<TabPane key="roles" tab="Roles">
<List
<TabPane key="roles" tab={t('label.roles')}>
<PoliciesDetailsList
hasAccess={policyPermission.EditAll}
list={policy.roles ?? []}
type="role"
@ -606,8 +512,8 @@ const PoliciesDetailPage = () => {
}
/>
</TabPane>
<TabPane key="teams" tab="Teams">
<List
<TabPane key="teams" tab={t('label.teams')}>
<PoliciesDetailsList
hasAccess={policyPermission.EditAll}
list={policy.teams ?? []}
type="team"
@ -621,7 +527,9 @@ const PoliciesDetailPage = () => {
)}
</>
) : (
<ErrorPlaceHolder>{NO_PERMISSION_TO_VIEW}</ErrorPlaceHolder>
<ErrorPlaceHolder>
{t('message.no-permission-to-view')}
</ErrorPlaceHolder>
)}
{selectedEntity && (
<Modal
@ -631,7 +539,7 @@ const PoliciesDetailPage = () => {
okText={t('label.confirm')}
title={`${t('label.remove-entity', {
entity: getEntityName(selectedEntity.record),
})} ${t('label.from')} ${getEntityName(policy)}`}
})} ${t('label.from-lowercase')} ${getEntityName(policy)}`}
visible={!isUndefined(selectedEntity.record)}
onCancel={() => setEntity(undefined)}
onOk={async () => {
@ -641,7 +549,7 @@ const PoliciesDetailPage = () => {
<Typography.Text>
{` ${t('label.sure-to-remove')} ${getEntityName(
selectedEntity.record
)} ${t('label.from')} ${getEntityName(policy)}?`}
)} ${t('label.from-lowercase')} ${getEntityName(policy)}?`}
</Typography.Text>
</Modal>
)}

View File

@ -0,0 +1,121 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Tooltip } from 'antd';
import Table, { ColumnsType } from 'antd/lib/table';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import { EntityReference } from '../../../generated/type/entityReference';
import { getEntityName } from '../../../utils/CommonUtils';
import {
getRoleWithFqnPath,
getTeamsWithFqnPath,
} from '../../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
const PoliciesDetailsList = ({
list,
type,
onDelete,
hasAccess,
}: {
list: EntityReference[];
type: 'role' | 'team';
onDelete: (record: EntityReference) => void;
hasAccess: boolean;
}) => {
const { t } = useTranslation();
const columns: ColumnsType<EntityReference> = useMemo(() => {
return [
{
title: t('label.name'),
dataIndex: 'name',
width: '200px',
key: 'name',
render: (_, record) => {
let link = '';
switch (type) {
case 'role':
link = getRoleWithFqnPath(record.fullyQualifiedName || '');
break;
case 'team':
link = getTeamsWithFqnPath(record.fullyQualifiedName || '');
break;
default:
break;
}
return (
<Link className="hover:tw-underline tw-cursor-pointer" to={link}>
{getEntityName(record)}
</Link>
);
},
},
{
title: t('label.description'),
dataIndex: 'description',
key: 'description',
render: (_, record) => (
<RichTextEditorPreviewer markdown={record?.description || ''} />
),
},
{
title: t('label.actions'),
dataIndex: 'actions',
width: '80px',
key: 'actions',
render: (_, record) => {
return (
<Tooltip
title={
hasAccess
? t('label.remove')
: t('message.no-permission-for-action')
}>
<Button
data-testid={`remove-action-${getEntityName(record)}`}
disabled={!hasAccess}
type="text"
onClick={() => onDelete(record)}>
<SVGIcons
alt="remove"
icon={Icons.ICON_REMOVE}
title={t('label.remove')}
/>
</Button>
</Tooltip>
);
},
},
];
}, []);
return (
<Table
bordered
className="list-table"
columns={columns}
dataSource={list}
pagination={false}
size="small"
/>
);
};
export default PoliciesDetailsList;

View File

@ -88,9 +88,9 @@ describe('Test Roles Details Page', () => {
const tabs = await screen.findByTestId('tabs');
const policiesTab = await screen.findByText('Policies');
const teamsTab = await screen.findByText('Teams');
const usersTab = await screen.findByText('Users');
const policiesTab = await screen.findByText('label.policies');
const teamsTab = await screen.findByText('label.teams');
const usersTab = await screen.findByText('label.users');
expect(container).toBeInTheDocument();

View File

@ -11,20 +11,18 @@
* limitations under the License.
*/
import { Button, Modal, Space, Table, Tabs, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { Button, Modal, Space, Tabs, Tooltip, Typography } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty, isUndefined } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useHistory, useParams } from 'react-router-dom';
import { useHistory, useParams } from 'react-router-dom';
import { getRoleByName, patchRole } from '../../../axiosAPIs/rolesAPIV1';
import { getTeamByName, patchTeamDetail } from '../../../axiosAPIs/teamsAPI';
import { getUserByName, updateUserDetail } from '../../../axiosAPIs/userAPI';
import Description from '../../../components/common/description/Description';
import ErrorPlaceHolder from '../../../components/common/error-with-placeholder/ErrorPlaceHolder';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
import Loader from '../../../components/Loader/Loader';
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
@ -32,29 +30,20 @@ import {
OperationPermission,
ResourceEntity,
} from '../../../components/PermissionProvider/PermissionProvider.interface';
import { getUserPath } from '../../../constants/constants';
import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../../../constants/GlobalSettings.constants';
import {
NO_PERMISSION_FOR_ACTION,
NO_PERMISSION_TO_VIEW,
} from '../../../constants/HelperTextUtil';
import { EntityType } from '../../../enums/entity.enum';
import { Role } from '../../../generated/entity/teams/role';
import { EntityReference } from '../../../generated/type/entityReference';
import { getEntityName } from '../../../utils/CommonUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils';
import {
getPolicyWithFqnPath,
getSettingPath,
getTeamsWithFqnPath,
} from '../../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { getSettingPath } from '../../../utils/RouterUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import AddAttributeModal from '../AddAttributeModal/AddAttributeModal';
import './RolesDetail.less';
import RolesDetailPageList from './RolesDetailPageList.component';
const { TabPane } = Tabs;
@ -65,101 +54,6 @@ interface AddAttribute {
selectedData: EntityReference[];
}
const List = ({
list,
type,
onDelete,
hasAccess,
}: {
list: EntityReference[];
type: 'policy' | 'team' | 'user';
onDelete: (record: EntityReference) => void;
hasAccess: boolean;
}) => {
const columns: ColumnsType<EntityReference> = useMemo(() => {
return [
{
title: 'Name',
dataIndex: 'name',
width: '200px',
key: 'name',
render: (_, record) => {
let link = '';
switch (type) {
case 'policy':
link = getPolicyWithFqnPath(record.fullyQualifiedName || '');
break;
case 'team':
link = getTeamsWithFqnPath(record.fullyQualifiedName || '');
break;
case 'user':
link = getUserPath(record.fullyQualifiedName || '');
break;
default:
break;
}
return (
<Link
className="hover:tw-underline tw-cursor-pointer"
data-testid="entity-name"
to={link}>
{getEntityName(record)}
</Link>
);
},
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
render: (_, record) => (
<RichTextEditorPreviewer markdown={record?.description || ''} />
),
},
{
title: 'Actions',
dataIndex: 'actions',
width: '80px',
key: 'actions',
render: (_, record) => {
return (
<Tooltip title={hasAccess ? 'Remove' : NO_PERMISSION_FOR_ACTION}>
<Button
data-testid={`remove-action-${getEntityName(record)}`}
disabled={!hasAccess}
type="text"
onClick={() => onDelete(record)}>
<SVGIcons
alt="remove"
icon={Icons.ICON_REMOVE}
title="Remove"
/>
</Button>
</Tooltip>
);
},
},
];
}, []);
return (
<Table
bordered
className="list-table"
columns={columns}
dataSource={list}
pagination={false}
rowKey="id"
size="small"
/>
);
};
const RolesDetailPage = () => {
const history = useHistory();
const { t } = useTranslation();
@ -187,7 +81,7 @@ const RolesDetailPage = () => {
const breadcrumb = useMemo(
() => [
{
name: 'Roles',
name: t('label.roles'),
url: rolesPath,
},
{
@ -393,13 +287,13 @@ const RolesDetailPage = () => {
/>
<Tabs data-testid="tabs" defaultActiveKey="policies">
<TabPane key="policies" tab="Policies">
<TabPane key="policies" tab={t('label.policies')}>
<Space className="tw-w-full" direction="vertical">
<Tooltip
title={
rolePermission.EditAll
? t('label.add-policy')
: NO_PERMISSION_FOR_ACTION
: t('message.no-permission-for-action')
}>
<Button
data-testid="add-policy"
@ -414,7 +308,7 @@ const RolesDetailPage = () => {
{t('label.add-policy')}
</Button>
</Tooltip>
<List
<RolesDetailPageList
hasAccess={rolePermission.EditAll}
list={role.policies ?? []}
type="policy"
@ -424,8 +318,8 @@ const RolesDetailPage = () => {
/>
</Space>
</TabPane>
<TabPane key="teams" tab="Teams">
<List
<TabPane key="teams" tab={t('label.teams')}>
<RolesDetailPageList
hasAccess={rolePermission.EditAll}
list={role.teams ?? []}
type="team"
@ -434,8 +328,8 @@ const RolesDetailPage = () => {
}
/>
</TabPane>
<TabPane key="users" tab="Users">
<List
<TabPane key="users" tab={t('label.users')}>
<RolesDetailPageList
hasAccess={rolePermission.EditAll}
list={role.users ?? []}
type="user"
@ -449,7 +343,9 @@ const RolesDetailPage = () => {
)}
</>
) : (
<ErrorPlaceHolder>{NO_PERMISSION_TO_VIEW}</ErrorPlaceHolder>
<ErrorPlaceHolder>
{t('message.no-permission-to-view')}
</ErrorPlaceHolder>
)}
{selectedEntity && (
<Modal
@ -459,7 +355,7 @@ const RolesDetailPage = () => {
okText={t('label.confirm')}
title={`${t('label.remove-entity', {
entity: getEntityName(selectedEntity.record),
})} ${t('label.from')} ${getEntityName(role)}`}
})} ${t('label.from-lowercase')} ${getEntityName(role)}`}
visible={!isUndefined(selectedEntity.record)}
onCancel={() => setEntity(undefined)}
onOk={async () => {
@ -469,7 +365,7 @@ const RolesDetailPage = () => {
<Typography.Text>
{t('label.sure-to-remove')}{' '}
{`${getEntityName(selectedEntity.record)} ${t(
'label.from'
'label.from-lowercase'
)} ${getEntityName(role)}?`}
</Typography.Text>
</Modal>

View File

@ -0,0 +1,129 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Table, Tooltip } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import { getUserPath } from '../../../constants/constants';
import { EntityReference } from '../../../generated/type/entityReference';
import { getEntityName } from '../../../utils/CommonUtils';
import {
getPolicyWithFqnPath,
getTeamsWithFqnPath,
} from '../../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
const RolesDetailPageList = ({
list,
type,
onDelete,
hasAccess,
}: {
list: EntityReference[];
type: 'policy' | 'team' | 'user';
onDelete: (record: EntityReference) => void;
hasAccess: boolean;
}) => {
const { t } = useTranslation();
const columns: ColumnsType<EntityReference> = useMemo(() => {
return [
{
title: t('label.name'),
dataIndex: 'name',
width: '200px',
key: 'name',
render: (_, record) => {
let link = '';
switch (type) {
case 'policy':
link = getPolicyWithFqnPath(record.fullyQualifiedName || '');
break;
case 'team':
link = getTeamsWithFqnPath(record.fullyQualifiedName || '');
break;
case 'user':
link = getUserPath(record.fullyQualifiedName || '');
break;
default:
break;
}
return (
<Link
className="hover:tw-underline tw-cursor-pointer"
data-testid="entity-name"
to={link}>
{getEntityName(record)}
</Link>
);
},
},
{
title: t('label.description'),
dataIndex: 'description',
key: 'description',
render: (_, record) => (
<RichTextEditorPreviewer markdown={record?.description || ''} />
),
},
{
title: t('label.actions'),
dataIndex: 'actions',
width: '80px',
key: 'actions',
render: (_, record) => {
return (
<Tooltip
title={
hasAccess
? t('label.remove')
: t('message.no-permission-for-action')
}>
<Button
data-testid={`remove-action-${getEntityName(record)}`}
disabled={!hasAccess}
type="text"
onClick={() => onDelete(record)}>
<SVGIcons
alt="remove"
icon={Icons.ICON_REMOVE}
title={t('label.remove')}
/>
</Button>
</Tooltip>
);
},
},
];
}, []);
return (
<Table
bordered
className="list-table"
columns={columns}
dataSource={list}
pagination={false}
rowKey="id"
size="small"
/>
);
};
export default RolesDetailPageList;

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import i18next from 'i18next';
import { TabSpecificField } from '../enums/entity.enum';
import { ChartType } from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
import { sortTagsCaseInsensitive } from './CommonUtils';
@ -20,21 +21,21 @@ ${TabSpecificField.USAGE_SUMMARY}, ${TabSpecificField.CHARTS},${TabSpecificField
export const dashboardDetailsTabs = [
{
name: 'Details',
name: i18next.t('label.details'),
path: 'details',
},
{
name: 'Activity Feeds & Tasks',
name: i18next.t('label.activity-feed-and-task-plural'),
path: 'activity_feed',
field: TabSpecificField.ACTIVITY_FEED,
},
{
name: 'Lineage',
name: i18next.t('label.lineage'),
path: 'lineage',
field: TabSpecificField.LINEAGE,
},
{
name: 'Custom Properties',
name: i18next.t('label.custom-properties'),
path: 'custom_properties',
},
];

View File

@ -1,12 +1,13 @@
import i18next from 'i18next';
import { TabSpecificField } from '../enums/entity.enum';
export const databaseDetailsTabs = [
{
name: 'Schemas',
name: i18next.t('label.schemas'),
path: 'schemas',
},
{
name: 'Activity Feeds',
name: i18next.t('label.activity-feeds'),
path: 'activity_feed',
field: TabSpecificField.ACTIVITY_FEED,
},

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import i18next from 'i18next';
import { TabSpecificField } from '../enums/entity.enum';
export const defaultFields = `${TabSpecificField.COLUMNS}, ${TabSpecificField.USAGE_SUMMARY},
@ -19,41 +20,41 @@ ${TabSpecificField.DATAMODEL},${TabSpecificField.TABLE_CONSTRAINTS},${TabSpecifi
export const datasetTableTabs = [
{
name: 'Schema',
name: i18next.t('label.schema'),
path: 'schema',
},
{
name: 'Activity Feeds & Tasks',
name: i18next.t('label.activity-feed-and-task-plural'),
path: 'activity_feed',
field: TabSpecificField.ACTIVITY_FEED,
},
{
name: 'Sample Data',
name: i18next.t('label.sample-data'),
path: 'sample_data',
},
{
name: 'Queries',
name: i18next.t('label.query-plural'),
path: 'table_queries',
},
{
name: 'Profiler',
name: i18next.t('label.profiler'),
path: 'profiler',
},
{
name: 'Data Quality',
name: i18next.t('label.data-quality'),
path: 'data-quality',
},
{
name: 'Lineage',
name: i18next.t('label.lineage'),
path: 'lineage',
field: TabSpecificField.LINEAGE,
},
{
name: 'DBT',
name: i18next.t('label.dbt-uppercase'),
path: 'dbt',
},
{
name: 'Custom Properties',
name: i18next.t('label.custom-properties'),
path: 'custom_properties',
},
];

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import i18next from 'i18next';
import { isEmpty, isNil, isUndefined, lowerCase, startCase } from 'lodash';
import { Bucket, LeafNodes, LineagePos } from 'Models';
import React, { Fragment } from 'react';
@ -113,7 +114,7 @@ export const getEntityOverview = (
const overview = [
{
name: 'Service',
name: i18next.t('label.service'),
value: service,
url: getServiceDetailsPath(
service,
@ -122,7 +123,7 @@ export const getEntityOverview = (
isLink: true,
},
{
name: 'Database',
name: i18next.t('label.database'),
value: database,
url: getDatabaseDetailsPath(
getPartialNameFromTableFQN(
@ -134,7 +135,7 @@ export const getEntityOverview = (
isLink: true,
},
{
name: 'Schema',
name: i18next.t('label.schema'),
value: schema,
url: getDatabaseSchemaDetailsPath(
getPartialNameFromTableFQN(
@ -146,33 +147,33 @@ export const getEntityOverview = (
isLink: true,
},
{
name: 'Owner',
name: i18next.t('label.owner'),
value: getEntityName(owner) || '--',
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: owner ? owner.type === 'team' : false,
},
{
name: 'Tier',
name: i18next.t('label.tier'),
value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : '--',
isLink: false,
},
{
name: 'Usage',
name: i18next.t('label.usage'),
value: usage,
isLink: false,
},
{
name: 'Queries',
name: i18next.t('label.query-plural'),
value: `${queries} past week`,
isLink: false,
},
{
name: 'Columns',
name: i18next.t('label.columns-plural'),
value: columns ? columns.length : '--',
isLink: false,
},
{
name: 'Rows',
name: i18next.t('label.row-plural'),
value: profile && profile?.rowCount ? profile.rowCount : '--',
isLink: false,
},
@ -188,7 +189,7 @@ export const getEntityOverview = (
const overview = [
{
name: 'Service',
name: i18next.t('label.service'),
value: service?.name as string,
url: getServiceDetailsPath(
service?.name as string,
@ -197,18 +198,18 @@ export const getEntityOverview = (
isLink: true,
},
{
name: 'Owner',
name: i18next.t('label.owner'),
value: getEntityName(owner) || '--',
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: owner ? owner.type === 'team' : false,
},
{
name: 'Tier',
name: i18next.t('label.tier'),
value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : '--',
isLink: false,
},
{
name: `${serviceType} url`,
name: `${serviceType} ${i18next.t('label.url-lowercase')}`,
value: fullyQualifiedName?.split(FQN_SEPARATOR_CHAR)[1] as string,
url: pipelineUrl as string,
isLink: true,
@ -231,7 +232,7 @@ export const getEntityOverview = (
const overview = [
{
name: 'Service',
name: i18next.t('label.service'),
value: service?.name as string,
url: getServiceDetailsPath(
service?.name as string,
@ -240,18 +241,18 @@ export const getEntityOverview = (
isLink: true,
},
{
name: 'Owner',
name: i18next.t('label.owner'),
value: getEntityName(owner) || '--',
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: owner ? owner.type === 'team' : false,
},
{
name: 'Tier',
name: i18next.t('label.tier'),
value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : '--',
isLink: false,
},
{
name: `${serviceType} url`,
name: `${serviceType} ${i18next.t('label.url-lowercase')}`,
value:
displayName ||
(fullyQualifiedName?.split(FQN_SEPARATOR_CHAR)[1] as string),

View File

@ -19,27 +19,19 @@ import {
diffWords,
diffWordsWithSpace,
} from 'diff';
import { isEmpty, isUndefined, uniqueId } from 'lodash';
import { t } from 'i18next';
import { isUndefined, uniqueId } from 'lodash';
import React, { Fragment } from 'react';
import ReactDOMServer from 'react-dom/server';
import { Link } from 'react-router-dom';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import {
DESCRIPTIONLENGTH,
getTeamAndUserDetailsPath,
} from '../constants/constants';
import { EntityField } from '../constants/Feeds.constants';
import { ChangeType } from '../enums/entity.enum';
import { Column } from '../generated/entity/data/table';
import {
ChangeDescription,
FieldChange,
} from '../generated/entity/services/databaseService';
import { TagLabel } from '../generated/type/tagLabel';
import { getEntityName } from './CommonUtils';
import { TagLabelWithStatus } from './EntityVersionUtils.interface';
import { isValidJSONString } from './StringsUtils';
import { getEntityLink } from './TableUtils';
export const getDiffByFieldName = (
name: string,
@ -143,328 +135,6 @@ export const getTagsDiff = (
return result;
};
export const getPreposition = (type: ChangeType) => {
switch (type) {
case 'Added':
return 'to';
case 'Removed':
return 'from';
case 'Updated':
return 'of';
default:
return '';
}
};
const getColumnName = (column: string) => {
const name = column.split(FQN_SEPARATOR_CHAR);
const length = name.length;
return name
.slice(length > 1 ? 1 : 0, length > 1 ? length - 1 : length)
.join(FQN_SEPARATOR_CHAR);
};
const getLinkWithColumn = (column: string, eFqn: string, eType: string) => {
return (
<Link
className="tw-pl-1"
to={`${getEntityLink(eType, eFqn)}.${getColumnName(column)}`}>
{getColumnName(column)}
</Link>
);
};
const getDescriptionText = (value: string) => {
const length = value.length;
return `${value.slice(0, DESCRIPTIONLENGTH)}${
length > DESCRIPTIONLENGTH ? '...' : ''
}`;
};
const getDescriptionElement = (fieldChange: FieldChange) => {
return fieldChange?.newValue && fieldChange?.oldValue ? (
<Fragment>
&nbsp;
<span className="tw-italic feed-change-description">{`${getDescriptionText(
fieldChange?.newValue
)}`}</span>
</Fragment>
) : fieldChange?.newValue ? (
<Fragment>
&nbsp;
<span className="tw-italic feed-change-description">
{`${getDescriptionText(fieldChange?.newValue)}`}
</span>
</Fragment>
) : (
<Fragment>
&nbsp;
<span className="tw-italic feed-change-description">
{`${getDescriptionText(fieldChange?.oldValue)}`}
</span>
</Fragment>
);
};
export const feedSummaryFromatter = (
fieldChange: FieldChange,
type: ChangeType,
_entityName: string,
entityType: string,
entityFQN: string
) => {
const value = JSON.parse(
isValidJSONString(fieldChange?.newValue)
? fieldChange?.newValue
: isValidJSONString(fieldChange?.oldValue)
? fieldChange?.oldValue
: '{}'
);
const oldValue = JSON.parse(
isValidJSONString(fieldChange?.oldValue) ? fieldChange?.oldValue : '{}'
);
const newValue = JSON.parse(
isValidJSONString(fieldChange?.newValue) ? fieldChange?.newValue : '{}'
);
let summary: JSX.Element;
switch (true) {
case fieldChange?.name?.startsWith('column'): {
if (fieldChange?.name?.endsWith('tags')) {
summary = (
<p key={uniqueId()}>
{`${type} tags ${value
?.map((val: TagLabel) => val?.tagFQN)
?.join(', ')} ${getPreposition(type)} column`}
{getLinkWithColumn(
fieldChange?.name as string,
entityFQN,
entityType
)}
</p>
);
break;
} else if (fieldChange?.name?.endsWith(EntityField.DESCRIPTION)) {
summary = (
<p key={uniqueId()}>
{`${
fieldChange?.newValue && fieldChange?.oldValue
? type
: fieldChange?.newValue
? 'Added'
: 'Removed'
} column description for`}
{getLinkWithColumn(
fieldChange?.name as string,
entityFQN,
entityType
)}
{isEmpty(value) ? getDescriptionElement(fieldChange) : ''}
</p>
);
break;
} else if (fieldChange?.name === EntityField.COLUMNS) {
const length = value?.length ?? 0;
summary = (
<p key={uniqueId()}>
{`${type} ${fieldChange?.name}`}{' '}
{value?.map((column: Column, i: number) => (
<span key={uniqueId()}>
{getLinkWithColumn(column.name, entityFQN, entityType)}{' '}
{i !== length - 1 ? ', ' : ''}
</span>
))}
</p>
);
break;
} else {
summary = (
<p key={uniqueId()}>
{`${type}`}
{getLinkWithColumn(
fieldChange?.name as string,
entityFQN,
entityType
)}
</p>
);
break;
}
}
case fieldChange?.name === 'tags': {
const tier = value?.find((t: TagLabel) => t?.tagFQN?.startsWith('Tier'));
const tags = value?.filter(
(t: TagLabel) => !t?.tagFQN?.startsWith('Tier')
);
summary = (
<div>
{tags?.length > 0 ? (
<p key={uniqueId()}>{`${type} tags ${tags
?.map((val: TagLabel) => val?.tagFQN)
?.join(', ')}`}</p>
) : null}
{tier ? (
<p key={uniqueId()}>{`${type} tier ${
tier?.tagFQN?.split(FQN_SEPARATOR_CHAR)[1]
}`}</p>
) : null}
</div>
);
break;
}
case fieldChange?.name === 'owner': {
const ownerName = getEntityName(newValue) || getEntityName(value);
const ownerText =
!isEmpty(oldValue) && !isEmpty(newValue) ? (
<Fragment>
{newValue?.type === 'team' ? (
<Link
className="tw-pl-1"
to={getTeamAndUserDetailsPath(newValue?.name || '')}>
<span title={ownerName}>{ownerName}</span>
</Link>
) : (
<span className="tw-pl-1" title={ownerName}>
{ownerName}
</span>
)}
</Fragment>
) : (
<Fragment>
{value?.type === 'team' ? (
<Link
className="tw-pl-1"
to={getTeamAndUserDetailsPath(value?.name || '')}>
<span title={ownerName}>{ownerName}</span>
</Link>
) : (
<span className="tw-pl-1" title={ownerName}>
{ownerName}
</span>
)}
</Fragment>
);
summary = (
<p
className={classNames('tw-truncate', {
'tw-w-52': ownerName.length > 32,
})}
key={uniqueId()}>
{`Assigned ownership to ${ownerText}`}
</p>
);
break;
}
case fieldChange?.name === EntityField.DESCRIPTION: {
summary = (
<p key={uniqueId()}>
{`${
fieldChange?.newValue && fieldChange?.oldValue
? type
: fieldChange?.newValue
? 'Added'
: 'Removed'
} description`}
{getDescriptionElement(fieldChange)}
</p>
);
break;
}
case fieldChange?.name === 'followers': {
summary = (
<p key={uniqueId()}>{`${
fieldChange?.newValue ? 'Started following' : 'Unfollowed'
} ${_entityName}`}</p>
);
break;
}
default:
summary = <p key={uniqueId()}>{`${type} ${fieldChange?.name}`}</p>;
break;
}
return summary;
};
export const getFeedSummary = (
changeDescription: ChangeDescription,
entityName: string,
entityType: string,
entityFQN: string
) => {
const fieldsAdded = [...(changeDescription?.fieldsAdded || [])];
const fieldsDeleted = [...(changeDescription?.fieldsDeleted || [])];
const fieldsUpdated = [...(changeDescription?.fieldsUpdated || [])];
return (
<Fragment>
{fieldsDeleted?.length ? (
<div className="tw-mb-2">
{fieldsDeleted?.map((d) => (
<Fragment key={uniqueId()}>
{feedSummaryFromatter(
d,
ChangeType.REMOVED,
entityName,
entityType,
entityFQN
)}
</Fragment>
))}
</div>
) : null}
{fieldsAdded?.length > 0 ? (
<div className="tw-mb-2">
{fieldsAdded?.map((a) => (
<Fragment key={uniqueId()}>
{feedSummaryFromatter(
a,
ChangeType.ADDED,
entityName,
entityType,
entityFQN
)}
</Fragment>
))}
</div>
) : null}
{fieldsUpdated?.length ? (
<div className="tw-mb-2">
{fieldsUpdated?.map((u) => (
<Fragment key={uniqueId()}>
{feedSummaryFromatter(
u,
ChangeType.UPDATED,
entityName,
entityType,
entityFQN
)}
</Fragment>
))}
</div>
) : null}
</Fragment>
);
};
export const summaryFormatter = (fieldChange: FieldChange) => {
const value = JSON.parse(
isValidJSONString(fieldChange?.newValue)
@ -511,31 +181,53 @@ export const getSummary = (
{isDeleteUpdated
.map((field) => {
return field.newValue
? 'Data asset has been deleted'
: 'Data asset has been restored';
? t('label.data-asset-has-been-action-type', {
actionType: t('label.deleted-lowercase'),
})
: t('label.data-asset-has-been-action-type', {
actionType: t('label.restored-lowercase'),
});
})
.join(', ')}
</p>
) : null}
{fieldsAdded?.length > 0 ? (
<p className="tw-mb-2">
{`${isPrefix ? '+ Added' : ''} ${fieldsAdded
{`${isPrefix ? `+ ${t('label.added')}` : ''} ${fieldsAdded
.map(summaryFormatter)
.join(', ')} ${!isPrefix ? `has been added` : ''}`}{' '}
.join(', ')} ${
!isPrefix
? t('label.has-been-action-type-lowercase', {
actionType: t('label.added-lowercase'),
})
: ''
}`}{' '}
</p>
) : null}
{fieldsUpdated?.length ? (
<p className="tw-mb-2">
{`${isPrefix ? 'Edited' : ''} ${fieldsUpdated
{`${isPrefix ? t('label.edited') : ''} ${fieldsUpdated
.map(summaryFormatter)
.join(', ')} ${!isPrefix ? `has been updated` : ''}`}{' '}
.join(', ')} ${
!isPrefix
? t('label.has-been-action-type-lowercase', {
actionType: t('label.updated-lowercase'),
})
: ''
}`}{' '}
</p>
) : null}
{fieldsDeleted?.length ? (
<p className="tw-mb-2">
{`${isPrefix ? '- Removed' : ''} ${fieldsDeleted
.map(summaryFormatter)
.join(', ')} ${!isPrefix ? `has been Deleted` : ''}`}{' '}
.join(', ')} ${
!isPrefix
? t('label.has-been-action-type-lowercase', {
actionType: t('label.deleted-lowercase'),
})
: ''
}`}{' '}
</p>
) : null}
</Fragment>

View File

@ -15,6 +15,7 @@ import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { AxiosError } from 'axios';
import { Operation } from 'fast-json-patch';
import i18next from 'i18next';
import { isEqual } from 'lodash';
import {
EntityFieldThreadCount,
@ -112,10 +113,15 @@ export const getReplyText = (
singular?: string,
plural?: string
) => {
if (count === 0) return 'Reply in conversation';
if (count === 1) return `${count} ${singular ? singular : 'older reply'}`;
if (count === 0) return i18next.t('label.reply-in-conversation');
if (count === 1)
return `${count} ${
singular ? singular : i18next.t('label.older-reply-lowercase')
}`;
return `${count} ${plural ? plural : 'older replies'}`;
return `${count} ${
plural ? plural : i18next.t('label.older-replies-lowercase')
}`;
};
export const getEntityFieldThreadCounts = (
@ -481,10 +487,10 @@ export const updateThreadData = (
export const getFeedAction = (type: ThreadType) => {
if (type === ThreadType.Task) {
return 'created a task';
return i18next.t('label.created-a-task-lowercase');
}
return 'posted on';
return i18next.t('label.posted-on-lowercase');
};
export const prepareFeedLink = (entityType: string, entityFQN: string) => {
@ -553,11 +559,11 @@ export const getFeedPanelHeaderText = (
) => {
switch (threadType) {
case ThreadType.Announcement:
return 'Announcement';
return i18next.t('label.announcement');
case ThreadType.Task:
return 'Task';
return i18next.t('label.task');
case ThreadType.Conversation:
default:
return 'Conversation';
return i18next.t('label.Conversation');
}
};

View File

@ -12,6 +12,7 @@
*/
import { AxiosError } from 'axios';
import { t } from 'i18next';
import { cloneDeep, isEmpty } from 'lodash';
import { FormattedGlossarySuggestion } from 'Models';
import { DataNode } from 'rc-tree/lib/interface';
@ -230,7 +231,7 @@ export const updateGlossaryListBySearchedTerms = (
export const getActionsList = () => {
return [
{
name: 'Add Term',
name: t('label.add-term'),
value: 'add_term',
},
];

View File

@ -13,6 +13,7 @@
import { AxiosError } from 'axios';
import { Change, diffWordsWithSpace } from 'diff';
import { t } from 'i18next';
import { isEqual, isUndefined } from 'lodash';
import { getDashboardByFqn } from '../axiosAPIs/dashboardAPI';
import { getUserSuggestions } from '../axiosAPIs/miscAPI';
@ -311,11 +312,11 @@ export const fetchEntityDetail = (
export const TASK_ACTION_LIST = [
{
label: 'Accept Suggestion',
label: t('label.accept-suggestion'),
key: TaskActionMode.VIEW,
},
{
label: 'Edit & Accept Suggestion',
label: t('label.edit-amp-accept-suggestion'),
key: TaskActionMode.EDIT,
},
];