fix(ui): revamp teams page added supported subscription webhooks (#13296)

* revamp teams page added supported subscription webhook

* minor changes

* minor changes

* changes teams header page layout and subscription

* minor changes

* fix cypress and addressed comments

* fix cypress for teams hierarchy (#13352)

* fix sonar errors and users not showing in teams having space

* code smell and bugs fixes

* fix teams page cypress

---------

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Ashish Gupta 2023-10-02 18:12:23 +05:30 committed by GitHub
parent e879d512d3
commit 16a4033645
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1358 additions and 796 deletions

View File

@ -37,7 +37,7 @@ export const BASE_WAIT_TIME = 20000;
const ADMIN = 'admin';
const RETRIES_COUNT = 4;
const TEAM_TYPES = ['BusinessUnit', 'Department', 'Division', 'Group'];
const TEAM_TYPES = ['Department', 'Division', 'Group'];
export const replaceAllSpacialCharWith_ = (text) => {
return text.replaceAll(/[&/\\#, +()$~%.'":*?<>{}]/g, '_');
@ -53,8 +53,31 @@ export const checkServiceFieldSectionHighlighting = (field) => {
);
};
const checkTeamTypeOptions = () => {
for (const teamType of TEAM_TYPES) {
const getTeamType = (currentTeam) => {
switch (currentTeam) {
case 'BusinessUnit':
return {
childTeamType: 'Division',
teamTypeOptions: TEAM_TYPES,
};
case 'Division':
return {
childTeamType: 'Department',
teamTypeOptions: TEAM_TYPES,
};
case 'Department':
return {
childTeamType: 'Group',
teamTypeOptions: ['Department', 'Group'],
};
}
};
const checkTeamTypeOptions = (type) => {
cy.log('check', type);
for (const teamType of getTeamType(type).teamTypeOptions) {
cy.get(`.ant-select-dropdown [title="${teamType}"]`)
.should('exist')
.should('be.visible');
@ -961,12 +984,28 @@ export const addTeam = (TEAM_DETAILS, index) => {
.should('be.visible')
.click();
checkTeamTypeOptions();
if (index > 0) {
cy.get('[data-testid="team-type"]')
.invoke('text')
.then((text) => {
cy.log(text);
checkTeamTypeOptions(text);
cy.log('check type', text);
cy.get(
`.ant-select-dropdown [title="${getTeamType(text).childTeamType}"]`
)
.should('exist')
.should('be.visible')
.click();
});
} else {
checkTeamTypeOptions('BusinessUnit');
cy.get(`.ant-select-dropdown [title="${TEAM_DETAILS.teamType}"]`)
.should('exist')
.should('be.visible')
.click();
cy.get(`.ant-select-dropdown [title='BusinessUnit']`)
.should('exist')
.should('be.visible')
.click();
}
cy.get(descriptionBox)
.should('exist')

View File

@ -24,12 +24,12 @@ import {
verifyResponseStatusCode,
} from '../../common/common';
const updateddescription = 'This is updated description';
const updatedDescription = 'This is updated description';
const teamName = 'team-group-test-430116' ?? `team-ct-test-${uuid()}`;
const TEAM_DETAILS = {
name: teamName,
updatedname: `${teamName}-updated`,
updatedName: `${teamName}-updated`,
teamType: 'Group',
description: `This is ${teamName} description`,
username: 'Aaron Johnson',
@ -125,10 +125,8 @@ describe('Teams flow should work properly', () => {
.contains(TEAM_DETAILS.name)
.click();
verifyResponseStatusCode('@permissions', 200);
cy.get('[data-testid="add-new-user"]')
.should('be.visible')
.scrollIntoView();
cy.get('[data-testid="add-new-user"]').click();
cy.get('[data-testid="users"]').click();
cy.get('[data-testid="add-new-user"]').scrollIntoView().click();
verifyResponseStatusCode('@getUsers', 200);
cy.get('[data-testid="selectable-list"] [data-testid="searchbar"]').type(
TEAM_DETAILS.username
@ -154,6 +152,7 @@ describe('Teams flow should work properly', () => {
cy.get(`[data-row-key="${TEAM_DETAILS.name}"]`)
.contains(TEAM_DETAILS.name)
.click();
cy.get('[data-testid="users"]').click();
verifyResponseStatusCode('@getUserDetails', 200);
verifyResponseStatusCode('@permissions', 200);
cy.get('[data-testid="add-new-user"]').should('be.visible').click();
@ -176,10 +175,15 @@ describe('Teams flow should work properly', () => {
.contains(TEAM_DETAILS.name)
.click();
cy.get('[data-testid="users"]').click();
verifyResponseStatusCode('@getUsers', 200);
// Click on join teams button
cy.get('[data-testid="join-teams"]').should('be.visible').click();
cy.get('[data-testid="join-teams"]')
.scrollIntoView()
.should('be.visible')
.click();
// Verify toast notification
toastNotification('Team joined successfully!');
@ -201,14 +205,14 @@ describe('Teams flow should work properly', () => {
verifyResponseStatusCode('@getSelectedTeam', 200);
// Click on edit display name
cy.get('[data-testid="edit-synonyms"]').should('be.visible').click();
cy.get('[data-testid="edit-team-name"]').should('be.visible').click();
// Enter the updated team name
cy.get('[data-testid="synonyms"]')
cy.get('[data-testid="team-name-input"]')
.should('exist')
.should('be.visible')
.clear()
.type(TEAM_DETAILS.updatedname);
.type(TEAM_DETAILS.updatedName);
// Save the updated display name
cy.get('[data-testid="saveAssociatedTag"]')
@ -220,12 +224,12 @@ describe('Teams flow should work properly', () => {
verifyResponseStatusCode('@getSelectedTeam', 200);
// Validate the updated display name
cy.get('[data-testid="team-heading"]').then(($el) => {
cy.wrap($el).should('have.text', TEAM_DETAILS.updatedname);
cy.wrap($el).should('have.text', TEAM_DETAILS.updatedName);
});
cy.get('[data-testid="inactive-link"]')
.should('be.visible')
.should('contain', TEAM_DETAILS.updatedname);
.scrollIntoView()
.should('contain', TEAM_DETAILS.updatedName);
});
it('Update description for created team', () => {
@ -245,12 +249,12 @@ describe('Teams flow should work properly', () => {
// Validate the updated display name
cy.get('[data-testid="team-heading"]').should(
'contain',
`${TEAM_DETAILS.updatedname}`
`${TEAM_DETAILS.updatedName}`
);
cy.get('[data-testid="inactive-link"]')
.should('be.visible')
.should('contain', TEAM_DETAILS.updatedname);
.should('contain', TEAM_DETAILS.updatedName);
// Click on edit description button
cy.get('[data-testid="edit-description"]')
@ -258,7 +262,7 @@ describe('Teams flow should work properly', () => {
.click({ force: true });
// Entering updated description
cy.get(descriptionBox).clear().type(updateddescription);
cy.get(descriptionBox).clear().type(updatedDescription);
cy.get('[data-testid="save"]').should('be.visible').click();
verifyResponseStatusCode('@patchDescription', 200);
@ -266,7 +270,7 @@ describe('Teams flow should work properly', () => {
// Validating the updated description
cy.get('[data-testid="description"] p').should(
'contain',
updateddescription
updatedDescription
);
});
@ -287,7 +291,7 @@ describe('Teams flow should work properly', () => {
.should('be.visible')
.contains(TEAM_DETAILS.name);
// //Click on Leave team
cy.get('[data-testid="leave-team-button"]').should('be.visible').click();
cy.get('[data-testid="leave-team-button"]').click();
// //Click on confirm button
cy.get('[data-testid="save-button"]').should('be.visible').click();
@ -312,8 +316,8 @@ describe('Teams flow should work properly', () => {
verifyResponseStatusCode('@getSelectedTeam', 200);
cy.get('[data-testid="team-heading"]')
.should('be.visible')
.contains(TEAM_DETAILS.updatedname);
cy.get('[data-testid="header"] [data-testid="manage-button"]')
.contains(TEAM_DETAILS.updatedName);
cy.get('[data-testid="manage-button"]')
.should('exist')
.should('be.visible')
.click();
@ -380,9 +384,9 @@ describe('Teams flow should work properly', () => {
cy.get('[data-testid="team-heading"]')
.should('be.visible')
.contains(TEAM_DETAILS.updatedname);
.contains(TEAM_DETAILS.updatedName);
cy.get('[data-testid="header"] [data-testid="manage-button"]')
cy.get('[data-testid="manage-button"]')
.should('exist')
.should('be.visible')
.click();
@ -436,7 +440,7 @@ describe('Teams flow should work properly', () => {
.click();
verifyResponseStatusCode('@getSelectedTeam', 200);
cy.get('[data-testid="header"] [data-testid="manage-button"]')
cy.get('[data-testid="manage-button"]')
.should('exist')
.should('be.visible')
.click();

View File

@ -0,0 +1,12 @@
<svg viewBox="0 0 25 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4154_24860)">
<path d="M5.67376 14.5249V6.05078H1.97121C0.88131 6.05078 0 6.93731 0 8.02721V25.5439C0 26.4252 1.06383 26.8633 1.6844 26.2427L5.7572 22.1699H17.355C18.4449 22.1699 19.3262 21.2886 19.3262 20.1987V16.4961H7.65019C6.56028 16.4961 5.67376 15.6148 5.67376 14.5249Z" fill="#00AC47"/>
<path d="M23.0288 0.460938H7.65017C6.56026 0.460938 5.67896 1.34225 5.67896 2.43215V6.05126H17.355C18.4449 6.05126 19.3262 6.93257 19.3262 8.02247V16.4914H23.0288C24.1187 16.4914 25 15.6101 25 14.5202V2.43215C25 1.34225 24.1187 0.460938 23.0288 0.460938Z" fill="#5BB974"/>
<path d="M17.355 6.05078H5.67371V14.5197C5.67371 15.6096 6.55502 16.4909 7.64492 16.4909H19.321V8.02721C19.3262 6.93731 18.4449 6.05078 17.355 6.05078Z" fill="#00832D"/>
</g>
<defs>
<clipPath id="clip0_4154_24860">
<rect width="25" height="26.0743" fill="white" transform="translate(0 0.460938)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 991 B

View File

@ -0,0 +1,5 @@
<svg viewBox="0 0 105 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M36.9019 2.53811C29.7245 1.986 15.3697 2.5371 15.3697 18.5493C15.3697 32.6833 21.995 36.9517 25.3076 37.3192V43.3924L10.4007 51.674C7.82417 53.1463 2.67114 57.416 2.67114 62.7162H25.3076L44.0794 52.7782V44.4966C36.9019 37.3192 35.2456 30.0108 35.2456 20.2038C35.2456 8.54045 41.3188 2.53811 52.9131 2.53811" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<path d="M68.3725 2.53811C75.5499 1.986 89.9047 2.5371 89.9047 18.5493C89.9047 32.6833 83.2794 36.9517 79.9668 37.3192V43.3924L94.8737 51.674C97.4502 53.1463 102.603 57.416 102.603 62.7162H79.9668L61.195 52.7782V44.4966C68.3725 37.3192 70.0288 30.0108 70.0288 20.2038C70.0288 8.54045 63.9556 2.53811 52.3613 2.53811" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 844 B

View File

@ -138,7 +138,7 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
),
},
];
}, [data, onTeamExpand]);
}, [data, isFetchingAllTeamAdvancedDetails, onTeamExpand]);
const handleMoveRow = useCallback(
async (dragRecord: Team, dropRecord: Team) => {

View File

@ -0,0 +1,357 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { Button, Divider, Form, Input, Space, Tooltip, Typography } from 'antd';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import Icon from '@ant-design/icons/lib/components/Icon';
import { useAuthContext } from 'components/authentication/auth-provider/AuthProvider';
import { OwnerLabel } from 'components/common/OwnerLabel/OwnerLabel.component';
import { EMAIL_REG_EX } from 'constants/regex.constants';
import { useAuth } from 'hooks/authHooks';
import { isEmpty, last } from 'lodash';
import { useTranslation } from 'react-i18next';
import { hasEditAccess } from 'utils/CommonUtils';
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
import classNames from 'classnames';
import TeamTypeSelect from 'components/common/TeamTypeSelect/TeamTypeSelect.component';
import { NO_DATA_PLACEHOLDER } from 'constants/constants';
import { Team, TeamType } from 'generated/entity/teams/team';
import { EntityReference } from 'generated/entity/type';
import { TeamsInfoProps } from '../team.interface';
const TeamsInfo = ({
parentTeams,
isGroupType,
childTeamsCount,
entityPermissions,
currentTeam,
updateTeamHandler,
}: TeamsInfoProps) => {
const { t } = useTranslation();
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const [isHeadingEditing, setIsHeadingEditing] = useState(false);
const [isEmailEdit, setIsEmailEdit] = useState<boolean>(false);
const [showTypeSelector, setShowTypeSelector] = useState(false);
const [heading, setHeading] = useState(
currentTeam ? currentTeam.displayName : ''
);
const { email, owner, teamType } = useMemo(() => currentTeam, [currentTeam]);
const { hasEditPermission, hasEditDisplayNamePermission, hasAccess } =
useMemo(
() => ({
hasEditPermission: entityPermissions.EditAll,
hasEditDisplayNamePermission:
entityPermissions.EditDisplayName || entityPermissions.EditAll,
hasAccess: isAuthDisabled || isAdminUser,
}),
[entityPermissions]
);
/**
* Check if current team is the owner or not
* @returns - True true or false based on hasEditAccess response
*/
const isCurrentTeamOwner = useMemo(
() => hasEditAccess(owner?.type ?? '', owner?.id ?? ''),
[owner]
);
const onHeadingSave = async (): Promise<void> => {
if (heading && currentTeam) {
const updatedData: Team = {
...currentTeam,
displayName: heading,
};
await updateTeamHandler(updatedData);
}
setIsHeadingEditing(false);
};
const onEmailSave = async (data: { email: string }) => {
if (currentTeam) {
const updatedData: Team = {
...currentTeam,
email: isEmpty(data.email) ? undefined : data.email,
};
await updateTeamHandler(updatedData);
}
setIsEmailEdit(false);
};
const updateOwner = useCallback(
async (owner?: EntityReference) => {
if (currentTeam) {
const updatedData: Team = {
...currentTeam,
owner,
};
await updateTeamHandler(updatedData);
}
},
[currentTeam]
);
const updateTeamType = async (type: TeamType): Promise<void> => {
if (currentTeam) {
const updatedData: Team = {
...currentTeam,
teamType: type,
};
await updateTeamHandler(updatedData);
setShowTypeSelector(false);
}
};
const teamHeadingRender = useMemo(
() =>
isHeadingEditing ? (
<Space>
<Input
className="w-48"
data-testid="team-name-input"
placeholder={t('message.enter-comma-separated-field', {
field: t('label.term-lowercase'),
})}
type="text"
value={heading}
onChange={(e) => setHeading(e.target.value)}
/>
<Space data-testid="buttons" size={4}>
<Button
className="rounded-4 text-sm p-xss"
data-testid="cancelAssociatedTag"
type="primary"
onMouseDown={() => setIsHeadingEditing(false)}>
<CloseOutlined />
</Button>
<Button
className="rounded-4 text-sm p-xss"
data-testid="saveAssociatedTag"
type="primary"
onMouseDown={onHeadingSave}>
<CheckOutlined />
</Button>
</Space>
</Space>
) : (
<Space align="baseline">
<Typography.Title
className="m-b-0 w-max-200"
data-testid="team-heading"
ellipsis={{ tooltip: true }}
level={5}>
{heading}
</Typography.Title>
{(hasAccess || isCurrentTeamOwner) && (
<Tooltip
placement="right"
title={
hasEditDisplayNamePermission
? t('label.edit-entity', {
entity: t('label.display-name'),
})
: t('message.no-permission-for-action')
}>
<Icon
className="align-middle"
component={EditIcon}
data-testid="edit-team-name"
disabled={!hasEditDisplayNamePermission}
style={{ fontSize: '16px' }}
onClick={() => setIsHeadingEditing(true)}
/>
</Tooltip>
)}
</Space>
),
[heading, isHeadingEditing, hasEditDisplayNamePermission]
);
const emailRender = useMemo(
() => (
<Space align="center" size={4}>
<Typography.Text className="text-grey-muted">{`${t(
'label.email'
)} :`}</Typography.Text>
{isEmailEdit ? (
<Form initialValues={{ email }} onFinish={onEmailSave}>
<Space align="baseline">
<Form.Item
className="m-b-0"
name="email"
rules={[
{
pattern: EMAIL_REG_EX,
type: 'email',
message: t('message.field-text-is-invalid', {
fieldText: t('label.email'),
}),
},
]}>
<Input
className="w-48"
data-testid="email-input"
placeholder={t('label.enter-entity', {
entity: t('label.email-lowercase'),
})}
/>
</Form.Item>
<Space size={4}>
<Button
className="h-8 p-x-xss"
data-testid="cancel-edit-email"
size="small"
type="primary"
onClick={() => setIsEmailEdit(false)}>
<CloseOutlined />
</Button>
<Button
className="h-8 p-x-xss"
data-testid="save-edit-email"
htmlType="submit"
size="small"
type="primary">
<CheckOutlined />
</Button>
</Space>
</Space>
</Form>
) : (
<Space align="center">
<Typography.Text className="font-medium" data-testid="email-value">
{email ?? NO_DATA_PLACEHOLDER}
</Typography.Text>
{hasEditPermission && (
<Tooltip
placement="right"
title={
hasEditPermission
? t('label.edit-entity', {
entity: t('label.email'),
})
: t('message.no-permission-for-action')
}>
<Icon
className="toolbar-button align-middle"
component={EditIcon}
data-testid="edit-email"
style={{ fontSize: '16px' }}
onClick={() => setIsEmailEdit(true)}
/>
</Tooltip>
)}
</Space>
)}
</Space>
),
[email, isEmailEdit, hasEditPermission]
);
const teamTypeElement = useMemo(() => {
if (teamType === TeamType.Organization) {
return null;
}
return (
<Space size={4}>
<Divider type="vertical" />
<Typography.Text className="text-grey-muted">
{`${t('label.type')} :`}
</Typography.Text>
{showTypeSelector ? (
<TeamTypeSelect
handleShowTypeSelector={setShowTypeSelector}
parentTeamType={
last(parentTeams)?.teamType ?? TeamType.Organization
}
showGroupOption={!childTeamsCount}
teamType={teamType ?? TeamType.Department}
updateTeamType={hasEditPermission ? updateTeamType : undefined}
/>
) : (
<>
<Typography.Text className="font-medium" data-testid="team-type">
{teamType}
</Typography.Text>
{hasEditPermission && (
<Icon
className={classNames('vertical-middle m-l-xs', {
'opacity-50': isGroupType,
})}
data-testid="edit-team-type-icon"
title={
isGroupType
? t('message.group-team-type-change-message')
: t('label.edit-entity', {
entity: t('label.team-type'),
})
}
onClick={
isGroupType ? undefined : () => setShowTypeSelector(true)
}>
<EditIcon />
</Icon>
)}
</>
)}
</Space>
);
}, [
teamType,
parentTeams,
isGroupType,
childTeamsCount,
showTypeSelector,
hasEditPermission,
updateTeamType,
setShowTypeSelector,
]);
useEffect(() => {
if (currentTeam) {
setHeading(currentTeam.displayName ?? currentTeam.name);
}
}, [currentTeam]);
return (
<Space size={4}>
{teamHeadingRender}
<Divider type="vertical" />
<OwnerLabel
className="text-sm"
hasPermission={hasAccess}
owner={owner}
onUpdate={updateOwner}
/>
<Divider type="vertical" />
{emailRender}
{teamTypeElement}
</Space>
);
};
export default TeamsInfo;

View File

@ -0,0 +1,187 @@
/*
* Copyright 2023 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 { Form, Input, Modal, Select, Space, Typography } from 'antd';
import { DE_ACTIVE_COLOR, ICON_DIMENSION } from 'constants/constants';
import {
SUBSCRIPTION_WEBHOOK,
SUBSCRIPTION_WEBHOOK_OPTIONS,
} from 'constants/Teams.constants';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
import { useForm } from 'antd/lib/form/Form';
import TagsV1 from 'components/Tag/TagsV1/TagsV1.component';
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
import { Webhook } from 'generated/type/profile';
import { isEmpty } from 'lodash';
import { getWebhookIcon } from 'utils/TeamUtils';
import { SubscriptionWebhook, TeamsSubscriptionProps } from '../team.interface';
const TeamsSubscription = ({
subscription,
hasEditPermission,
updateTeamSubscription,
}: TeamsSubscriptionProps) => {
const [form] = useForm();
const { t } = useTranslation();
const [editSubscription, setEditSubscription] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const getWebhookIconByKey = useCallback((item: SUBSCRIPTION_WEBHOOK) => {
const Icon = getWebhookIcon(item);
return <Icon data-testid={`${item}-icon`} height={20} width={20} />;
}, []);
// Watchers
const webhooks: {
webhook: string;
endpoint: string;
}[] = Form.useWatch(['subscriptions'], form);
// Run time values needed for conditional rendering
const subscriptionOptions = useMemo(() => {
const exitingWebhook = webhooks?.map((f) => f?.webhook) ?? [];
return SUBSCRIPTION_WEBHOOK_OPTIONS.map((func) => ({
label: func.label,
value: func.value,
disabled: exitingWebhook.includes(func.value),
}));
}, [webhooks]);
const cellItem = useCallback(
(key: string, value: Webhook) => (
<Space align="start">
{getWebhookIconByKey(key as SUBSCRIPTION_WEBHOOK)}
<Typography.Text className="text-xs text-grey-muted">
{value.endpoint}
</Typography.Text>
</Space>
),
[]
);
const subscriptionRenderElement = useMemo(() => {
const webhook = Object.entries(subscription ?? {})?.[0];
return isEmpty(subscription) && hasEditPermission ? (
<div onClick={() => setEditSubscription(true)}>
<TagsV1 startWith={TAG_START_WITH.PLUS} tag={TAG_CONSTANT} />
</div>
) : (
cellItem(webhook[0], webhook[1])
);
}, [subscription]);
const handleSave = async (values: SubscriptionWebhook) => {
setIsLoading(true);
try {
await updateTeamSubscription(values);
} catch {
// parent block will throw error
} finally {
setEditSubscription(false);
setIsLoading(false);
}
};
useEffect(() => {
if (subscription) {
const data = Object.entries(subscription)[0];
form.setFieldsValue({
webhook: data[0],
endpoint: data[1].endpoint,
});
}
}, [subscription, editSubscription]);
return (
<Space align="start" data-testid="teams-subscription">
<Typography.Text className="right-panel-label font-normal">
{`${t('label.subscription')} :`}
</Typography.Text>
{subscriptionRenderElement}
{!editSubscription && !isEmpty(subscription) && hasEditPermission && (
<EditIcon
className="cursor-pointer align-middle"
color={DE_ACTIVE_COLOR}
data-testid="edit-roles"
{...ICON_DIMENSION}
onClick={() => setEditSubscription(true)}
/>
)}
{editSubscription && (
<Modal
centered
open
closable={false}
confirmLoading={isLoading}
maskClosable={false}
okButtonProps={{
form: 'subscription-form',
type: 'primary',
htmlType: 'submit',
}}
okText={t('label.confirm')}
title={t('label.add-entity', {
entity: t('label.subscription'),
})}
onCancel={() => setEditSubscription(false)}>
<Form
data-testid="subscription-modal"
form={form}
id="subscription-form"
layout="vertical"
onFinish={handleSave}>
<Form.Item label={t('label.webhook')} name="webhook">
<Select
options={subscriptionOptions}
placeholder={t('label.select-field', {
field: t('label.condition'),
})}
/>
</Form.Item>
<Form.Item
label={t('label.endpoint')}
name="endpoint"
rules={[
{
required: true,
message: t('label.field-required-plural', {
field: t('label.endpoint'),
}),
},
{
type: 'url',
message: t('message.endpoint-should-be-valid'),
},
]}>
<Input
placeholder={t('label.enter-entity-value', {
entity: t('label.endpoint'),
})}
/>
</Form.Item>
</Form>
</Modal>
)}
</Space>
);
};
export default TeamsSubscription;

View File

@ -214,7 +214,7 @@ export const UserTab = ({
}
return (
<Row gutter={[16, 16]}>
<Row className="p-md" gutter={[16, 16]}>
<Col span={24}>
<Row justify="space-between">
<Col span={8}>

View File

@ -11,6 +11,8 @@
* limitations under the License.
*/
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
import { MessagingProvider } from 'generated/type/profile';
import { Team } from '../../../generated/entity/teams/team';
export interface TeamHierarchyProps {
@ -55,3 +57,23 @@ export enum TeamsPageTab {
ROLES = 'roles',
POLICIES = 'policies',
}
export interface TeamsInfoProps {
parentTeams: Team[];
isGroupType: boolean;
childTeamsCount: number;
currentTeam: Team;
entityPermissions: OperationPermission;
updateTeamHandler: (data: Team) => Promise<void>;
}
export interface TeamsSubscriptionProps {
hasEditPermission: boolean;
subscription?: MessagingProvider;
updateTeamSubscription: (value: SubscriptionWebhook) => Promise<void>;
}
export interface SubscriptionWebhook {
webhook: string;
endpoint: string;
}

View File

@ -14,6 +14,7 @@
@import url('../../../styles/variables.less');
.team-list-container {
padding: 20px;
.ant-btn {
border-radius: 4px;
}
@ -76,9 +77,47 @@
}
}
.teams-layout {
margin: -16px -16px 0 -16px;
.ant-card-head-title {
padding-top: 0;
padding-bottom: 12px;
}
.teams-profile {
background-color: @team-avatar-bg;
}
.teams-profile-container {
background: @user-profile-background;
.ant-card {
background: none;
}
}
.teams-tabs-content-container {
width: 100%;
.teams-scroll-component {
width: 100%;
height: calc(100vh - 120px);
overflow-y: scroll;
}
}
.site-collapse-custom-collapse .site-collapse-custom-panel {
overflow: hidden;
padding: 0;
background: @user-profile-background;
border: 0px;
.ant-collapse-content-box {
padding: 0;
}
}
}
.team-assets-right-panel {
margin-top: -24px;
margin-bottom: -24px;
.summary-panel-container {
height: 100%;
border: 0;

View File

@ -14,6 +14,7 @@ import Icon from '@ant-design/icons';
import { Typography } from 'antd';
import { ReactComponent as IconTeamsGrey } from 'assets/svg/teams-grey.svg';
import { ReactComponent as IconUser } from 'assets/svg/user.svg';
import classNames from 'classnames';
import { getTeamAndUserDetailsPath, getUserPath } from 'constants/constants';
import { OwnerType } from 'enums/user.enum';
import { EntityReference } from 'generated/entity/data/table';
@ -27,11 +28,13 @@ import { UserTeamSelectableList } from '../UserTeamSelectableList/UserTeamSelect
export const OwnerLabel = ({
owner,
className,
onUpdate,
hasPermission,
ownerDisplayName,
}: {
owner?: EntityReference;
className?: string;
onUpdate?: (owner?: EntityReference) => void;
hasPermission?: boolean;
ownerDisplayName?: ReactNode;
@ -74,7 +77,10 @@ export const OwnerLabel = ({
{displayName ? (
<Link
className="text-primary font-medium text-xs no-underline"
className={classNames(
'text-primary font-medium text-xs no-underline',
className
)}
data-testid="owner-link"
to={
owner?.type === 'team'
@ -85,7 +91,7 @@ export const OwnerLabel = ({
</Link>
) : (
<Typography.Text
className="font-medium text-xs"
className={classNames('font-medium text-xs', className)}
data-testid="owner-link">
{t('label.no-entity', { entity: t('label.owner') })}
</Typography.Text>

View File

@ -17,7 +17,6 @@ import React, { useMemo, useState } from 'react';
import { getTeamOptionsFromType } from 'utils/TeamUtils';
import { TeamType } from '../../../generated/entity/teams/team';
import { TeamTypeSelectProps } from './TeamTypeSelect.interface';
import './TeamTypeSelect.style.less';
function TeamTypeSelect({
handleShowTypeSelector,
@ -63,17 +62,23 @@ function TeamTypeSelect({
value={value}
onSelect={handleSelect}
/>
<Space className="edit-team-type-buttons" size={4}>
<Space className="m-l-xs" size={4}>
<Button
className="h-8 p-x-xss"
data-testid="cancel-btn"
icon={<CloseOutlined />}
onClick={handleCancel}
/>
size="small"
type="primary"
onClick={handleCancel}>
<CloseOutlined />
</Button>
<Button
className="h-8 p-x-xss"
data-testid="save-btn"
icon={<CheckOutlined />}
onClick={handleSubmit}
/>
size="small"
type="primary"
onClick={handleSubmit}>
<CheckOutlined />
</Button>
</Space>
</Space>
);

View File

@ -1,33 +0,0 @@
/*
* 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 url('../../../styles/variables.less');
.team-type-select {
.ant-select {
width: 150px;
}
.edit-team-type-buttons {
.ant-btn {
background-color: @primary-color;
color: white;
width: 24px;
height: 24px;
}
}
.edit-team-icons {
font-size: 12px;
}
}

View File

@ -12,6 +12,7 @@
*/
import DraggableBodyRow from 'components/Team/TeamDetails/DraggableBodyRow';
import { t } from 'i18next';
export const DRAGGABLE_BODY_ROW = 'DraggableBodyRow';
@ -20,3 +21,25 @@ export const TABLE_CONSTANTS = {
row: DraggableBodyRow,
},
};
export enum SUBSCRIPTION_WEBHOOK {
MS_TEAMS = 'msTeams',
SLACK = 'slack',
G_CHAT = 'gChat',
GENERIC = 'generic',
}
export const SUBSCRIPTION_WEBHOOK_OPTIONS = [
{
label: t('label.ms-team-plural'),
value: SUBSCRIPTION_WEBHOOK.MS_TEAMS,
},
{
label: t('label.slack'),
value: SUBSCRIPTION_WEBHOOK.SLACK,
},
{
label: t('label.g-chat'),
value: SUBSCRIPTION_WEBHOOK.G_CHAT,
},
];

View File

@ -102,7 +102,6 @@ export interface TeamDetailsProp {
teamUsersSearchText: string;
isDescriptionEditable: boolean;
isTeamMemberLoading: number;
hasAccess: boolean;
isFetchingAdvancedDetails: boolean;
isFetchingAllTeamAdvancedDetails: boolean;
entityPermissions: OperationPermission;

View File

@ -345,6 +345,7 @@
"enter": "Eingeben",
"enter-entity": "{{entity}} eingeben",
"enter-entity-name": "Geben Sie einen Namen für {{entity}} ein",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Geben Sie eine Beschreibung für {{field}} ein",
"enter-property-value": "Geben Sie einen Wert für die Eigenschaft ein",
"enter-type-password": "{{type}}-Passwort eingeben",
@ -382,6 +383,7 @@
"feature-plural": "funktionen",
"feature-plural-used": "verwendete funktionen",
"february": "Februar",
"feed-filter-plural": "Feed filters",
"feed-lowercase": "feed",
"feed-plural": "feeds",
"field": "Feld",
@ -903,6 +905,7 @@
"sub-domain-plural": "Subdomains",
"sub-team-plural": "Unter-Teams",
"submit": "Einreichen",
"subscription": "Subscription",
"success": "Erfolg",
"successfully-lowercase": "erfolgreich",
"successfully-uploaded": "Erfolgreich hochgeladen",
@ -1129,6 +1132,7 @@
"connection-test-warning": "Der Test der Verbindung war teilweise erfolgreich: Einige Schritte hatten Fehler, es werden nur teilweise Metadaten übernommen.",
"copied-to-clipboard": "In die Zwischenablage kopiert",
"copy-to-clipboard": "Link in die Zwischenablage kopiert",
"create-new-domain-guide": "A data mesh is a decentralized data architecture that organizes data by a specific business domain following the concepts of domain-oriented design. Teams take ownership of both operational and analytical data that belongs to the domain. Based on the Data Contract, consumers are provided with Data as a Product. It is powered by Domain-agnostic self-serve data infrastructure. There are three types of domains: Consumer-aligned, Aggregate, and Source-aligned. Domains have Enabling Teams for consulting.",
"create-new-glossary-guide": "Ein Glossar ist ein kontrolliertes Vokabular, das verwendet wird, um die Konzepte und Terminologie in einer Organisation zu definieren. Glossare können spezifisch für einen bestimmten Bereich sein (z. B. Business Glossar, Technisches Glossar). Im Glossar können die Standardbegriffe und Konzepte definiert werden, zusammen mit Synonymen und verwandten Begriffen. Es kann festgelegt werden, wie und von wem Begriffe im Glossar hinzugefügt werden können.",
"create-or-update-email-account-for-bot": "Die Änderung der Kontaktemail aktualisiert oder erstellt einen neuen Bot-Benutzer.",
"created-this-task-lowercase": "hat diese Aufgabe erstellt",
@ -1215,6 +1219,9 @@
"error-while-fetching-access-token": "Fehler beim Abrufen des Zugriffstokens.",
"export-entity-help": "Laden Sie alle Ihre {{entity}} als CSV-Datei herunter und teilen Sie sie mit Ihrem Team.",
"failed-status-for-entity-deploy": "<0>{{entity}}</0> wurde {{entityStatus}}, aber das Bereitstellen ist fehlgeschlagen.",
"feed-filter-all": "Feeds for all the data assets that you own and follow",
"feed-filter-following": "Feeds for all the data assets that you follow",
"feed-filter-owner": "Feeds for all the data assets that you own",
"fetch-dbt-files": "Hier sind die verfügbaren Quellen zum Abrufen von dbt-Katalog- und Manifestdateien.",
"fetch-pipeline-status-error": "Fehler beim Abrufen des Pipeline-Status.",
"field-ca-certs-description": "Der Zertifikatpfad muss in der Konfiguration hinzugefügt werden. Der Pfad sollte lokal im Ingestion-Container sein.",

View File

@ -345,6 +345,7 @@
"enter": "Enter",
"enter-entity": "Enter {{entity}}",
"enter-entity-name": "Enter {{entity}} name",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Enter {{field}} description",
"enter-property-value": "Enter Property Value",
"enter-type-password": "Enter {{type}} Password",
@ -904,6 +905,7 @@
"sub-domain-plural": "Sub Domains",
"sub-team-plural": "Sub Teams",
"submit": "Submit",
"subscription": "Subscription",
"success": "Success",
"successfully-lowercase": "successfully",
"successfully-uploaded": "Successfully Uploaded",

View File

@ -345,6 +345,7 @@
"enter": "Entrar",
"enter-entity": "Ingrese {{entity}}",
"enter-entity-name": "Ingrese el nombre de {{entity}}",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Ingrese la descripción de {{field}}",
"enter-property-value": "Ingrese el valor de la propiedad",
"enter-type-password": "Ingrese la contraseña de {{type}}",
@ -904,6 +905,7 @@
"sub-domain-plural": "Sub Domains",
"sub-team-plural": "Sub Equipos",
"submit": "Enviar",
"subscription": "Subscription",
"success": "Éxito",
"successfully-lowercase": "successfully",
"successfully-uploaded": "Cargado Exitosamente",

View File

@ -345,6 +345,7 @@
"enter": "Entrer",
"enter-entity": "Entrer {{entity}}",
"enter-entity-name": "Entrer un nom pour {{entity}}",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Entrer une description pour {{field}}",
"enter-property-value": "Entrer une valeur pour la propriété",
"enter-type-password": "Entrer un mot de passe {{type}}",
@ -904,6 +905,7 @@
"sub-domain-plural": "Sous-Domaines",
"sub-team-plural": "Sous-Équipes",
"submit": "Soumettre",
"subscription": "Subscription",
"success": "Succès",
"successfully-lowercase": "avec succès",
"successfully-uploaded": "Téléchargé avec succès",
@ -1130,7 +1132,7 @@
"connection-test-warning": "Le test de connexion a réussi partiellement : Certaines étapes ont échoué, nous n'ingérerons que les métadonnées partielles.",
"copied-to-clipboard": "Copié dans le presse-papiers",
"copy-to-clipboard": "Lien copié dans le presse-papiers",
"create-new-domain-guide": "A data mesh is a decentralized data architecture that organizes data by a specific business domain following the concepts of domain-oriented design. Teams take ownership of both operational and analytical data that belongs to the domain. Based on the Data Contract, consumers are provided with Data as a Product. It is powered by Domain-agnostic self-serve data infrastructure. There are three types of domains: Consumer-aligned, Aggregate, and Source-aligned. Domains have Enabling Teams for consulting.",
"create-new-domain-guide": "A data mesh is a decentralized data architecture that organizes data by a specific business domain following the concepts of domain-oriented design. Teams take ownership of both operational and analytical data that belongs to the domain. Based on the Data Contract, consumers are provided with Data as a Product. It is powered by Domain-agnostic self-serve data infrastructure. There are three types of domains: Consumer-aligned, Aggregate, and Source-aligned. Domains have Enabling Teams for consulting.",
"create-new-glossary-guide": "Un Glossaire est un recueil de termes et vocabulaire utilisé pour définir des concepts et terminologies. Glossaires peuvent être spécifiques à certains domaines (e.g., Glossaire Business, Glossaire Technique, etc.). Dans le glossaire, les termes et concepts peuvent être définis tout en spécifiant des synonymes et des termes liés. Il est possible de contrôler qui peut ajouter des termes dans le dans le glossaire et comment ces termes peuvent être ajoutés.",
"create-or-update-email-account-for-bot": "Changer l'email créera un nouveau ou mettra à jour l'agent numérique",
"created-this-task-lowercase": "a créé cette tâche",

View File

@ -345,6 +345,7 @@
"enter": "入力",
"enter-entity": "{{entity}}を入力",
"enter-entity-name": "{{entity}}の名前を入力",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "{{field}}の説明を入力",
"enter-property-value": "プロパティの値を入力",
"enter-type-password": "{{type}} のパスワードを入力",
@ -904,6 +905,7 @@
"sub-domain-plural": "Sub Domains",
"sub-team-plural": "サブチーム",
"submit": "Submit",
"subscription": "Subscription",
"success": "成功",
"successfully-lowercase": "successfully",
"successfully-uploaded": "アップロード成功",

View File

@ -345,6 +345,7 @@
"enter": "Enter",
"enter-entity": "Introduzir {{entity}}",
"enter-entity-name": "Introduzir nome da {{entity}}",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Introduzir descrição do {{entity}}",
"enter-property-value": "Introduzir valor da propriedade",
"enter-type-password": "Introduzir {{type}} da senha",
@ -904,6 +905,7 @@
"sub-domain-plural": "Sub Domains",
"sub-team-plural": "Sub-equipes",
"submit": "Enviar",
"subscription": "Subscription",
"success": "Sucesso",
"successfully-lowercase": "successfully",
"successfully-uploaded": "Enviado com sucesso",

View File

@ -345,6 +345,7 @@
"enter": "Введите",
"enter-entity": "Введите {{entity}}",
"enter-entity-name": "Введите имя {{entity}}",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "Введите описание {{field}}",
"enter-property-value": "Введите значение свойства",
"enter-type-password": "Введите {{type}} пароль",
@ -904,6 +905,7 @@
"sub-domain-plural": "Sub Domains",
"sub-team-plural": "Подгруппы",
"submit": "Подтвердить",
"subscription": "Subscription",
"success": "Успешно",
"successfully-lowercase": "successfully",
"successfully-uploaded": "Успешно загружено",

View File

@ -345,6 +345,7 @@
"enter": "输入",
"enter-entity": "输入{{entity}}",
"enter-entity-name": "输入{{entity}}的名称",
"enter-entity-value": "Enter {{entity}} Value",
"enter-field-description": "输入{{field}}的描述",
"enter-property-value": "输入属性值",
"enter-type-password": "输入{{type}}密码",
@ -904,6 +905,7 @@
"sub-domain-plural": "子域",
"sub-team-plural": "子团队",
"submit": "提交",
"subscription": "Subscription",
"success": "成功",
"successfully-lowercase": "successfully",
"successfully-uploaded": "上传成功",

View File

@ -12,7 +12,6 @@
*/
import { AxiosError } from 'axios';
import { useAuthContext } from 'components/authentication/auth-provider/AuthProvider';
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
import { PagingHandlerParams } from 'components/common/next-previous/NextPrevious.interface';
import Loader from 'components/Loader/Loader';
@ -39,7 +38,7 @@ import {
patchTeamDetail,
} from 'rest/teamsAPI';
import { getUsers, updateUserDetail } from 'rest/userAPI';
import { getEncodedFqn } from 'utils/StringsUtils';
import { getDecodedFqn, getEncodedFqn } from 'utils/StringsUtils';
import AppState from '../../AppState';
import {
INITIAL_PAGING_VALUE,
@ -52,7 +51,6 @@ import { EntityReference } from '../../generated/entity/data/table';
import { Team } from '../../generated/entity/teams/team';
import { User } from '../../generated/entity/teams/user';
import { Paging } from '../../generated/type/paging';
import { useAuth } from '../../hooks/authHooks';
import { SearchResponse } from '../../interface/search.interface';
import { formatUsersResponse } from '../../utils/APIUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
@ -64,8 +62,6 @@ const TeamsPage = () => {
const history = useHistory();
const { t } = useTranslation();
const { getEntityPermissionByFqn } = usePermissionProvider();
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const { fqn } = useParams<{ fqn: string }>();
const [currentFqn, setCurrentFqn] = useState<string>('');
const [allTeam, setAllTeam] = useState<Team[]>([]);
@ -223,7 +219,7 @@ const TeamsPage = () => {
getUsers({
fields: 'teams,roles',
limit: PAGE_SIZE_BASE,
team,
team: getDecodedFqn(team),
...paging,
})
.then((res) => {
@ -285,7 +281,11 @@ const TeamsPage = () => {
const fetchTeamBasicDetails = async (name: string, loadPage = false) => {
setIsPageLoading(loadPage);
try {
const data = await getTeamByName(name, ['owner', 'parents'], 'all');
const data = await getTeamByName(
name,
['owner', 'parents', 'profile'],
'all'
);
setSelectedTeam(data);
if (!isEmpty(data.parents) && data.parents?.[0].name) {
@ -621,7 +621,6 @@ const TeamsPage = () => {
handleJoinTeamClick={handleJoinTeamClick}
handleLeaveTeamClick={handleLeaveTeamClick}
handleTeamUsersSearchAction={handleUsersSearchAction}
hasAccess={isAuthDisabled || isAdminUser}
isDescriptionEditable={isDescriptionEditable}
isFetchingAdvancedDetails={isFetchingAdvancedDetails}
isFetchingAllTeamAdvancedDetails={isFetchAllTeamAdvancedDetails}

View File

@ -175,6 +175,12 @@ a[href].link-text-grey,
border-color: @warning-color;
}
// Line height
.line-height-0 {
line-height: 0;
}
.line-height-16 {
line-height: 16px;
}

View File

@ -48,6 +48,7 @@ pre {
}
.text-sm {
font-size: 14px;
line-height: 1rem /* 16px */;
}
.text-md {
font-size: 16px;

View File

@ -84,7 +84,7 @@
@tag-background-color: rgba(0, 0, 0, 0.03);
@trigger-btn-hover-bg: #efefef;
@text-highlighter: #ffc34e40;
@team-avatar-bg: #0950c51a;
@navbar-height: 64px;
@sidebar-width: 60px;

View File

@ -21,6 +21,11 @@ import {
} from '../generated/entity/teams/team';
import { getEntityIdArray } from './CommonUtils';
import { SUBSCRIPTION_WEBHOOK } from 'constants/Teams.constants';
import { ReactComponent as GChatIcon } from '../assets/svg/gchat.svg';
import { ReactComponent as MsTeamsIcon } from '../assets/svg/ms-teams.svg';
import { ReactComponent as SlackIcon } from '../assets/svg/slack.svg';
/**
* To get filtered list of non-deleted(active) users
* @param users List of users
@ -86,6 +91,23 @@ export const getMovedTeamData = (team: Team, parents: string[]): CreateTeam => {
} as CreateTeam;
};
/**
* To get webhook svg icon
* @param item webhook key
* @returns SvgComponent
*/
export const getWebhookIcon = (item: SUBSCRIPTION_WEBHOOK): SvgComponent => {
switch (item) {
case SUBSCRIPTION_WEBHOOK.SLACK:
return SlackIcon;
case SUBSCRIPTION_WEBHOOK.G_CHAT:
return GChatIcon;
default:
return MsTeamsIcon;
}
};
export const getTeamOptionsFromType = (parentType: TeamType) => {
switch (parentType) {
case TeamType.Organization: