Minor: Address feedback of Incident manager (#14997)

* Minor: Address feedback of Incident manager

* added provision to delete and update display name from test case detail page

* address the feedback

* added option to clear severity
This commit is contained in:
Shailesh Parmar 2024-02-03 11:54:51 +05:30 committed by GitHub
parent 5839efcbf1
commit 4b43fa2079
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 184 additions and 44 deletions

View File

@ -269,10 +269,10 @@ describe('Incident Manager', () => {
verifyResponseStatusCode('@getTestCase', 200);
cy.get('[data-testid="incident"]').click();
verifyResponseStatusCode('@getTaskFeed', 200);
cy.get('[data-testid="task-cta-buttons"]')
.contains('Reassign')
cy.get('[data-testid="task-cta-buttons"] [role="img"]')
.scrollIntoView()
.click();
cy.get('[role="menu"').find('[data-menu-id*="re-assign"]').click();
interceptURL(
'GET',
'/api/v1/search/suggest?q=admin&index=*user_search_index*',
@ -309,10 +309,10 @@ describe('Incident Manager', () => {
verifyResponseStatusCode('@getTestCase', 200);
cy.get('[data-testid="incident"]').click();
verifyResponseStatusCode('@getTaskFeed', 200);
cy.get('[data-testid="task-cta-buttons"] [role="img"]')
cy.get('[data-testid="task-cta-buttons"]')
.contains('Resolve')
.scrollIntoView()
.click();
cy.get('[role="menu"').find('[data-menu-id*="resolve"]').click();
cy.get('#testCaseFailureReason').click();
cy.get('[title="Missing Data"]').click();
cy.get('.toastui-editor-md-container > .toastui-editor > .ProseMirror')

View File

@ -12,7 +12,15 @@
*/
import { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, InputNumber, Select, Switch } from 'antd';
import {
Button,
Form,
FormItemProps,
Input,
InputNumber,
Select,
Switch,
} from 'antd';
import 'codemirror/addon/fold/foldgutter.css';
import { isUndefined } from 'lodash';
import React from 'react';
@ -32,6 +40,7 @@ const ParameterForm: React.FC<ParameterFormProps> = ({ definition, table }) => {
const { t } = useTranslation();
const prepareForm = (data: TestCaseParameterDefinition) => {
let internalFormItemProps: FormItemProps = {};
let Field = (
<Input
placeholder={`${t('message.enter-a-field', {
@ -122,6 +131,10 @@ const ParameterForm: React.FC<ParameterFormProps> = ({ definition, table }) => {
break;
case TestDataType.Boolean:
Field = <Switch />;
internalFormItemProps = {
...internalFormItemProps,
valuePropName: 'checked',
};
break;
case TestDataType.Array:
@ -204,7 +217,8 @@ const ParameterForm: React.FC<ParameterFormProps> = ({ definition, table }) => {
})}`,
},
]}
tooltip={data.description}>
tooltip={data.description}
{...internalFormItemProps}>
{Field}
</Form.Item>
);

View File

@ -103,6 +103,7 @@ export const TestCaseStatusModal = ({
...data.testCaseResolutionStatusDetails,
assignee: {
name: updatedAssignees[0].name,
displayName: updatedAssignees[0].displayName,
id: updatedAssignees[0].value,
type: EntityType.USER,
},

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Divider, Skeleton, Space, Typography } from 'antd';
import { Divider, Skeleton, Space, Tooltip, Typography } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { first, isUndefined, last } from 'lodash';
@ -274,9 +274,13 @@ const IncidentManagerPageHeader = ({
<Divider className="self-center m-x-sm" type="vertical" />
<Typography.Text className="self-center text-xs whitespace-nowrap">
<span className="text-grey-muted">{`${t('label.test-type')}: `}</span>
<span className="font-medium" data-testid="test-definition-name">
{getEntityName(testCaseData?.testDefinition)}
</span>
<Tooltip
placement="bottom"
title={testCaseData?.testDefinition.description}>
<span className="font-medium" data-testid="test-definition-name">
{getEntityName(testCaseData?.testDefinition)}
</span>
</Tooltip>
</Typography.Text>
</Space>
);

View File

@ -60,6 +60,7 @@ const SeverityModal = ({
onFinish={handleFormSubmit}>
<Form.Item label={t('label.severity')} name="severity">
<Select
allowClear
data-testid="severity-select"
placeholder={t('label.please-select-entity', {
entity: t('label.severity'),

View File

@ -134,7 +134,7 @@ const TestCaseResultTab = ({
<Typography.Text className="right-panel-label">
{t('label.parameter-plural')}
</Typography.Text>
{hasEditPermission && (
{hasEditPermission && Boolean(withoutSqlParams.length) && (
<Icon
component={EditIcon}
data-testid="edit-parameter-icon"

View File

@ -152,10 +152,10 @@ const TestSummary: React.FC<TestSummaryProps> = ({
failedRows: result.failedRows,
passedRowsPercentage: isUndefined(result.passedRowsPercentage)
? undefined
: `${result.passedRowsPercentage}%`,
: `${round(result.passedRowsPercentage, 2)}%`,
failedRowsPercentage: isUndefined(result.failedRowsPercentage)
? undefined
: `${result.failedRowsPercentage}%`,
: `${round(result.failedRowsPercentage, 2)}%`,
};
chartData.push({
@ -270,7 +270,7 @@ const TestSummary: React.FC<TestSummaryProps> = ({
const referenceArea = () => {
const params = data.parameterValues ?? [];
if (params.length < 2) {
if (params.length && params.length < 2) {
return (
<ReferenceLine
label={params[0].name}

View File

@ -171,15 +171,18 @@ const CustomMetricGraphs = ({
return (
<Row data-testid="custom-metric-graph-container" gutter={[16, 16]}>
{!isEmpty(customMetricsGraphData) && (
{!isEmpty(customMetrics) && (
<Col span={24}>
<PageHeader data={PAGE_HEADERS.CUSTOM_METRICS} />
</Col>
)}
{toPairs(customMetricsGraphData).map(([key, metric], i) => {
const metricDetails = customMetrics?.find(
(metric) => metric.name === key
);
const color = TOTAL_ENTITY_CHART_COLOR[i] ?? getRandomHexColor();
return (
return isUndefined(metricDetails) ? null : (
<Col key={key} span={24}>
<Card
className="shadow-none global-border-radius custom-metric-card"

View File

@ -161,4 +161,20 @@ describe('CustomMetricGraphs', () => {
expect(await screen.findByText('DeleteWidgetModal')).toBeInTheDocument();
});
it("CustomMetric should be visible based on 'customMetrics' prop", async () => {
render(
<CustomMetricGraphs
{...mockProps}
customMetrics={[mockProps.customMetrics[0]]}
/>
);
const name = mockProps.customMetrics[0].name;
const name2 = mockProps.customMetrics[1].name;
const metric = await screen.findByTestId(`${name}-custom-metrics`);
const metric2 = screen.queryByTestId(`${name2}-custom-metrics`);
expect(metric).toBeInTheDocument();
expect(metric2).not.toBeInTheDocument();
});
});

View File

@ -28,7 +28,14 @@ import Modal from 'antd/lib/modal/Modal';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { isEmpty, isEqual, isUndefined, startCase, unionBy } from 'lodash';
import {
isEmpty,
isEqual,
isUndefined,
last,
startCase,
unionBy,
} from 'lodash';
import { MenuInfo } from 'rc-menu/lib/interface';
import React, {
useCallback,
@ -136,9 +143,25 @@ export const TaskTab = ({
} = useActivityFeedProvider();
const isTaskTestCaseResult =
taskDetails?.type === TaskType.RequestTestCaseFailureResolution;
const [taskAction, setTaskAction] = useState<TaskAction>(
isTaskTestCaseResult ? INCIDENT_TASK_ACTION_LIST[0] : TASK_ACTION_LIST[0]
);
const latestAction = useMemo(() => {
const resolutionStatus = last(testCaseResolutionStatus);
if (isTaskTestCaseResult) {
switch (resolutionStatus?.testCaseResolutionStatusType) {
case TestCaseResolutionStatusTypes.Assigned:
return INCIDENT_TASK_ACTION_LIST[1];
default:
return INCIDENT_TASK_ACTION_LIST[0];
}
} else {
return TASK_ACTION_LIST[0];
}
}, [testCaseResolutionStatus, isTaskTestCaseResult]);
const [taskAction, setTaskAction] = useState<TaskAction>(latestAction);
const [isActionLoading, setIsActionLoading] = useState(false);
const isTaskClosed = isEqual(taskDetails?.status, ThreadTaskStatus.Closed);
const [showEditTaskModel, setShowEditTaskModel] = useState(false);
const [comment, setComment] = useState('');
@ -353,6 +376,7 @@ export const TaskTab = ({
};
const onTestCaseIncidentAssigneeUpdate = async () => {
setIsActionLoading(true);
const testCaseIncident: CreateTestCaseResolutionStatus = {
testCaseResolutionStatusType: TestCaseResolutionStatusTypes.Assigned,
testCaseReference: entityFQN,
@ -360,6 +384,7 @@ export const TaskTab = ({
assignee: {
id: updatedAssignees[0].value,
name: updatedAssignees[0].name,
displayName: updatedAssignees[0].displayName,
type: updatedAssignees[0].type,
},
},
@ -372,6 +397,8 @@ export const TaskTab = ({
});
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsActionLoading(false);
}
};
@ -382,6 +409,7 @@ export const TaskTab = ({
testCaseFailureReason: TestCaseFailureReasonType;
testCaseFailureComment: string;
}) => {
setIsActionLoading(true);
const testCaseIncident: CreateTestCaseResolutionStatus = {
testCaseResolutionStatusType: TestCaseResolutionStatusTypes.Resolved,
testCaseReference: entityFQN,
@ -402,6 +430,8 @@ export const TaskTab = ({
setShowEditTaskModel(false);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsActionLoading(false);
}
};
@ -484,6 +514,7 @@ export const TaskTab = ({
className="m-t-sm"
data-testid="task-cta-buttons"
icon={<DownOutlined />}
loading={isActionLoading}
menu={{
items: INCIDENT_TASK_ACTION_LIST,
selectable: true,
@ -618,6 +649,10 @@ export const TaskTab = ({
setOptions(assigneeOptions);
}, [initialAssignees, assigneeOptions]);
useEffect(() => {
setTaskAction(latestAction);
}, [latestAction]);
const taskHeader = isTaskTestCaseResult ? (
<TaskTabIncidentManagerHeader thread={taskThread} />
) : (
@ -763,6 +798,9 @@ export const TaskTab = ({
maskClosable
closable={false}
closeIcon={null}
okButtonProps={{
loading: isActionLoading,
}}
okText={t('label.submit')}
open={showEditTaskModel}
title={`${t('label.resolve')} ${t('label.task')} #${taskDetails?.id}`}
@ -885,6 +923,9 @@ export const TaskTab = ({
maskClosable
closable={false}
closeIcon={null}
okButtonProps={{
loading: isActionLoading,
}}
okText={t('label.submit')}
open={isEditAssignee}
title={`${t('label.re-assign')} ${t('label.task')} #${

View File

@ -12,13 +12,14 @@
*/
import { Col, Row, Tabs, TabsProps } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { compare, Operation as PatchOperation } from 'fast-json-patch';
import { isUndefined } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { ReactComponent as TestCaseIcon } from '../../../assets/svg/ic-checklist.svg';
import ActivityFeedProvider from '../../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
import ManageButton from '../../../components/common/EntityPageInfos/ManageButton/ManageButton';
import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
import TitleBreadcrumb from '../../../components/common/TitleBreadcrumb/TitleBreadcrumb.component';
import { TitleBreadcrumbProps } from '../../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface';
@ -27,6 +28,7 @@ import IncidentManagerPageHeader from '../../../components/IncidentManager/Incid
import TestCaseIncidentTab from '../../../components/IncidentManager/TestCaseIncidentTab/TestCaseIncidentTab.component';
import TestCaseResultTab from '../../../components/IncidentManager/TestCaseResultTab/TestCaseResultTab.component';
import Loader from '../../../components/Loader/Loader';
import { EntityName } from '../../../components/Modals/EntityNameModal/EntityNameModal.interface';
import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1';
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../components/PermissionProvider/PermissionProvider.interface';
@ -73,13 +75,26 @@ const IncidentManagerDetailPage = () => {
);
const { permissions } = usePermissionProvider();
const hasViewPermission = useMemo(() => {
return checkPermission(
Operation.ViewAll,
ResourceEntity.TEST_CASE,
permissions
);
}, [permissions]);
const { hasViewPermission, editDisplayNamePermission, hasDeletePermission } =
useMemo(() => {
return {
hasViewPermission: checkPermission(
Operation.ViewAll,
ResourceEntity.TEST_CASE,
permissions
),
editDisplayNamePermission: checkPermission(
Operation.EditDisplayName,
ResourceEntity.TEST_CASE,
permissions
),
hasDeletePermission: checkPermission(
Operation.Delete,
ResourceEntity.TEST_CASE,
permissions
),
};
}, [permissions]);
const onTestCaseUpdate = (data: TestCase) => {
setTestCaseData((prev) => ({ ...prev, data }));
@ -168,7 +183,14 @@ const IncidentManagerDetailPage = () => {
);
}
};
const updateTestCase = async (id: string, patch: PatchOperation[]) => {
try {
const res = await updateTestCaseById(id, patch);
onTestCaseUpdate(res);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const handleOwnerChange = async (owner?: EntityReference) => {
const data = testCaseData.data;
if (data) {
@ -178,13 +200,23 @@ const IncidentManagerDetailPage = () => {
};
const jsonPatch = compare(data, updatedTestCase);
if (jsonPatch.length) {
try {
const res = await updateTestCaseById(data.id ?? '', jsonPatch);
onTestCaseUpdate(res);
} catch (error) {
showErrorToast(error as AxiosError);
}
if (jsonPatch.length && data.id) {
updateTestCase(data.id, jsonPatch);
}
}
};
const handleDisplayNameChange = async (entityName?: EntityName) => {
const data = testCaseData.data;
if (data) {
const updatedTestCase = {
...data,
...entityName,
};
const jsonPatch = compare(data, updatedTestCase);
if (jsonPatch.length && data.id) {
updateTestCase(data.id, jsonPatch);
}
}
};
@ -228,13 +260,34 @@ const IncidentManagerDetailPage = () => {
<TitleBreadcrumb className="m-b-sm" titleLinks={breadcrumb} />
</Col>
<Col className="p-x-lg" data-testid="entity-page-header" span={24}>
<EntityHeaderTitle
className="w-max-full-45"
displayName={testCaseData.data?.displayName}
icon={<TestCaseIcon className="h-9" />}
name={testCaseData.data?.name ?? ''}
serviceName="testCase"
/>
<Row gutter={16}>
<Col span={23}>
<EntityHeaderTitle
className="w-max-full-45"
displayName={testCaseData.data?.displayName}
icon={<TestCaseIcon className="h-9" />}
name={testCaseData.data?.name ?? ''}
serviceName="testCase"
/>
</Col>
<Col className="d-flex justify-end" span={1}>
<ManageButton
isRecursiveDelete
afterDeleteAction={() =>
history.push(ROUTES.INCIDENT_MANAGER)
}
allowSoftDelete={false}
canDelete={hasDeletePermission}
displayName={testCaseData.data.displayName}
editDisplayNamePermission={editDisplayNamePermission}
entityFQN={testCaseData.data.fullyQualifiedName}
entityId={testCaseData.data.id}
entityName={testCaseData.data.name}
entityType={EntityType.TEST_CASE}
onEditDisplayName={handleDisplayNameChange}
/>
</Col>
</Row>
</Col>
<Col className="p-x-lg">
<IncidentManagerPageHeader

View File

@ -45,6 +45,7 @@ export interface Option {
value: string;
type: string;
name?: string;
displayName?: string;
children?: string;
'data-label'?: string;
'data-testid'?: string;

View File

@ -56,6 +56,7 @@ const Assignees: FC<Props> = ({
value: option.value,
type: option.type,
name: option.name,
displayName: option.displayName,
}));
onChange(newValues as Option[]);

View File

@ -30,7 +30,7 @@ export const deleteCustomMetric = async ({
: customMetricName;
return await APIClient.delete<Table>(
`${BASE_URL}/${tableId}/customMetric/${url}`
`${BASE_URL}/${tableId}/customMetric/${url}?recursive=true&hardDelete=true`
);
};

View File

@ -182,11 +182,13 @@ describe('Tests for fetchOptions', () => {
expect(mockSetOptions).toHaveBeenCalledWith([
{
label: 'Ashish Gupta',
displayName: 'Ashish Gupta',
name: 'ashish',
type: 'user',
value: '18ca6cd1-d696-4a22-813f-c7a42fc09dc4',
},
{
displayName: 'Ashley King',
label: 'Ashley King',
name: 'ashley_king5',
type: 'user',
@ -212,6 +214,7 @@ describe('Tests for fetchOptions', () => {
expect(mockSetOptions).toHaveBeenCalledWith([
{
displayName: 'Ashley King',
label: 'Ashley King',
name: 'ashley_king5',
type: 'user',

View File

@ -213,6 +213,7 @@ export const fetchOptions = ({
value: hit._id,
type: hit._source.entityType,
name: hit._source.name,
displayName: hit._source.displayName,
}));
setOptions(suggestOptions.filter((item) => item.value !== currentUserId));
@ -226,6 +227,7 @@ export const generateOptions = (assignees: EntityReference[]) => {
value: assignee.id || '',
type: assignee.type,
name: assignee.name,
displayName: assignee.displayName,
}));
};