Fix(ui/incident): Refactor code for updating incidents (#13172)

This commit is contained in:
amit-apptware 2025-04-18 07:45:15 +05:30 committed by GitHub
parent 36cce7d1be
commit 9bea6341c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 106 additions and 147 deletions

View File

@ -404,7 +404,11 @@ const EntityDropdown = (props: Props) => {
{hasBeenDeleted && !onDelete && deleteRedirectPath && <Redirect to={deleteRedirectPath} />}
{isRaiseIncidentModalVisible && (
<IncidentDetailDrawer
urn={urn}
entity={{
urn,
entityType,
platform: entityData?.platform ?? entityData?.dataPlatformInstance?.platform,
}}
mode={IncidentAction.CREATE}
onSubmit={() => {
setIsRaiseIncidentModalVisible(false);
@ -423,11 +427,6 @@ const EntityDropdown = (props: Props) => {
}, 3000);
}}
onCancel={() => setIsRaiseIncidentModalVisible(false)}
entity={{
urn,
entityType,
platform: entityData?.platform ?? entityData?.dataPlatformInstance?.platform,
}}
/>
)}
{isLinkAssetVersionModalVisible && (

View File

@ -12,22 +12,20 @@ import { EntityPrivileges, Incident } from '@src/types.generated';
const modalBodyStyle = { padding: 0, fontFamily: 'Mulish, sans-serif' };
type IncidentDetailDrawerProps = {
urn: string;
entity: EntityStagedForIncident;
mode: IncidentAction;
incident?: IncidentTableRow;
onCancel?: () => void;
onSubmit?: (incident?: Incident) => void;
entity?: EntityStagedForIncident;
privileges?: EntityPrivileges;
};
export const IncidentDetailDrawer = ({
urn,
entity,
mode,
onCancel,
onSubmit,
incident,
entity,
privileges,
}: IncidentDetailDrawerProps) => {
const [isEditView, setIsEditView] = useState<boolean>(false);
@ -83,7 +81,6 @@ export const IncidentDetailDrawer = ({
incidentUrn={incident?.urn}
entity={entity}
onSubmit={handleSubmit}
urn={urn}
/>
) : (
<IncidentView incident={incident as IncidentTableRow} />

View File

@ -36,12 +36,11 @@ const HalfWidthInput = styled(Input)`
`;
export const IncidentEditor = ({
entity,
incidentUrn,
onSubmit,
data,
mode = IncidentAction.CREATE,
entity,
urn,
}: IncidentEditorProps) => {
const assigneeValues = data?.assignees && getAssigneeWithURN(data.assignees);
const isFormValid = Boolean(
@ -53,8 +52,8 @@ export const IncidentEditor = ({
const { user } = useUserContext();
const userHasChangedState = useRef(false);
const isFirstRender = useRef(true);
const [cachedAssignees, setCachedAssignees] = useState<any>([]);
const [cachedLinkedAssets, setCachedLinkedAssets] = useState<any>([]);
const [cachedAssignees, setCachedAssignees] = useState<any[]>([]);
const [cachedLinkedAssets, setCachedLinkedAssets] = useState<any[]>([]);
const [isLoadingAssigneeOrAssets, setIsLoadingAssigneeOrAssets] = useState(true);
const [isRequiredFieldsFilled, setIsRequiredFieldsFilled] = useState<boolean>(
@ -69,8 +68,6 @@ export const IncidentEditor = ({
assignees: cachedAssignees,
linkedAssets: cachedLinkedAssets,
entity,
currentIncident: data,
urn,
});
const formValues = Form.useWatch([], form);
@ -88,7 +85,6 @@ export const IncidentEditor = ({
isFirstRender.current = false;
return;
}
// Ensure we don't override user's choice if they manually change the state
if (
mode === IncidentAction.EDIT &&
@ -118,16 +114,38 @@ export const IncidentEditor = ({
}
};
const actionButtonLabel = mode === IncidentAction.CREATE ? 'Create' : 'Update';
const showCustomCategory = form.getFieldValue('type') === IncidentType.Custom;
const isLinkedAssetPresent = !formValues?.resourceUrns?.length;
const isLinkedAssetMissing = !formValues?.resourceUrns?.length;
const isSubmitButtonDisabled =
!validateForm(form) ||
!isRequiredFieldsFilled ||
isLoadingAssigneeOrAssets ||
isLinkedAssetPresent ||
isLinkedAssetMissing ||
isLoading;
const actionButtonLabel = mode === IncidentAction.CREATE ? 'Create' : 'Update';
const actionButton = isLoading ? (
<>
<StyledSpinner />
{actionButtonLabel === 'Create' ? 'Creating...' : 'Updating...'}
</>
) : (
actionButtonLabel
);
const resolutionInput = form.getFieldValue('state') === IncidentState.Resolved && (
<SelectFormItem
label="Resolution Note"
name="message"
rules={[{ required: false }]}
customStyle={{
color: colors.gray[600],
}}
>
<HalfWidthInput label="" placeholder="Add a resolution note......" id="incident-message" />
</SelectFormItem>
);
return (
<StyledForm
form={form}
@ -209,12 +227,12 @@ export const IncidentEditor = ({
initialValue={getLinkedAssetsData(data?.linkedAssets) || []}
>
<IncidentLinkedAssetsList
initialUrn={entity?.urn}
form={form}
data={data}
mode={mode}
setCachedLinkedAssets={setCachedLinkedAssets}
setIsLinkedAssetsLoading={setIsLoadingAssigneeOrAssets}
urn={urn}
/>
</SelectFormItem>
{mode === IncidentAction.EDIT && (
@ -227,30 +245,11 @@ export const IncidentEditor = ({
value={formValues?.[INCIDENT_OPTION_LABEL_MAPPING.state.fieldName]}
/>
)}
{form.getFieldValue('state') === IncidentState.Resolved && (
<SelectFormItem
label="Resolution Note"
name="message"
rules={[{ required: false }]}
customStyle={{
color: colors.gray[600],
}}
>
<HalfWidthInput label="" placeholder="Add a resolution note......" id="incident-message" />
</SelectFormItem>
)}
{resolutionInput}
</StyledFormElements>
<IncidentFooter>
<SaveButton data-testid="incident-create-button" type="submit" disabled={isSubmitButtonDisabled}>
{/* {actionButtonLabel} */}
{isLoading ? (
<>
<StyledSpinner />
{actionButtonLabel === 'Create' ? 'Creating...' : 'Updating...'}
</>
) : (
actionButtonLabel
)}
{actionButton}
</SaveButton>
</IncidentFooter>
</StyledForm>

View File

@ -25,12 +25,12 @@ const StyledButton = styled(Button)`
`;
export const IncidentLinkedAssetsList = ({
initialUrn,
form,
data,
mode,
setCachedLinkedAssets,
setIsLinkedAssetsLoading,
urn,
}: IncidentLinkedAssetsListProps) => {
const [getEntities, { data: resolvedLinkedAssets, loading: entitiesLoading }] = useGetEntitiesLazyQuery();
const entityRegistry = useEntityRegistryV2();
@ -67,11 +67,11 @@ export const IncidentLinkedAssetsList = ({
};
useEffect(() => {
if (mode === IncidentAction.CREATE) {
if (urn) {
if (mode === IncidentAction.CREATE && initialUrn) {
if (initialUrn) {
getEntities({
variables: {
urns: [urn],
urns: [initialUrn],
},
});
}
@ -82,7 +82,7 @@ export const IncidentLinkedAssetsList = ({
useEffect(() => {
setLinkedAssets(resolvedLinkedAssets?.entities as any);
if (mode === IncidentAction.CREATE) {
form.setFieldValue(RESOURCE_URN_FIELD_NAME, [urn]);
form.setFieldValue(RESOURCE_URN_FIELD_NAME, [initialUrn]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resolvedLinkedAssets]);

View File

@ -3,6 +3,7 @@ import { Form, message } from 'antd';
import _ from 'lodash';
import { useState } from 'react';
import { useEntityData } from '@app/entity/shared/EntityContext';
import { IncidentAction } from '@app/entityV2/shared/tabs/Incident/constant';
import { PAGE_SIZE, updateActiveIncidentInCache } from '@app/entityV2/shared/tabs/Incident/incidentUtils';
import analytics, { EntityActionType, EventType } from '@src/app/analytics';
@ -22,6 +23,7 @@ export const getCacheIncident = ({
incidentUrn?: string;
}) => {
const newIncident = {
__typename: 'Incident',
urn: incidentUrn ?? responseData?.data?.raiseIncident,
type: EntityType.Incident,
incidentType: values.type,
@ -31,6 +33,7 @@ export const getCacheIncident = ({
startedAt: null,
tags: null,
status: {
__typename: 'IncidentStatus',
state: values?.state,
stage: values?.stage,
message: values?.message || null,
@ -41,18 +44,12 @@ export const getCacheIncident = ({
},
},
source: {
__typename: 'IncidentSource',
type: IncidentSourceType.Manual,
source: {
urn: '',
type: 'Assertion',
platform: {
urn: '',
name: '',
properties: { displayName: '', logoUrl: '' },
},
},
source: null,
},
linkedAssets: {
__typename: 'EntityRelationshipsResult',
relationships: values.linkedAssets?.map((linkedAsset) => ({
entity: {
...linkedAsset,
@ -62,6 +59,7 @@ export const getCacheIncident = ({
priority: values.priority,
created: {
__typename: 'AuditStamp',
time: values.created || Date.now(),
actor: user?.urn,
},
@ -70,17 +68,10 @@ export const getCacheIncident = ({
return newIncident;
};
export const useIncidentHandler = ({
mode,
onSubmit,
incidentUrn,
user,
assignees,
linkedAssets,
entity,
currentIncident,
urn,
}) => {
export const useIncidentHandler = ({ mode, onSubmit, incidentUrn, user, assignees, linkedAssets, entity }) => {
// Important: Here we are trying to fetch the URN of the sibling whose "profile" we are currently viewing.
// We then insert any new incidents into this cache as well so that it immediately updates the page for the asset.
const { urn: maybeCacheEntityUrn } = useEntityData();
const [raiseIncidentMutation] = useRaiseIncidentMutation();
const [updateIncidentMutation] = useUpdateIncidentMutation();
const [form] = Form.useForm();
@ -146,7 +137,6 @@ export const useIncidentHandler = ({
const values = form.getFieldsValue();
const baseInput = {
...values,
resourceUrn: entity?.urn || urn,
status: {
stage: values.status,
state: values.state || IncidentState.Active,
@ -154,7 +144,7 @@ export const useIncidentHandler = ({
},
};
const newInput = _.omit(baseInput, ['state', 'message']);
const newUpdateInput = _.omit(newInput, ['resourceUrn', 'type', 'customType']);
const newUpdateInput = _.omit(newInput, ['resourceUrn', 'type', 'resourceUrns', 'customType']);
const input = !isAddIncidentMode ? newUpdateInput : newInput;
if (isAddIncidentMode) {
@ -175,32 +165,22 @@ export const useIncidentHandler = ({
incidentUrn: responseData?.data?.raiseIncident,
user,
});
updateActiveIncidentInCache(client, urn, newIncident, PAGE_SIZE);
// Add new incident to core entity's cache.
if (!entity) return;
updateActiveIncidentInCache(client, entity.urn, newIncident, PAGE_SIZE);
if (maybeCacheEntityUrn) {
// Optional: Also add into the cache of the sibling whose page we are viewing.
updateActiveIncidentInCache(client, maybeCacheEntityUrn, newIncident, PAGE_SIZE);
}
analytics.event({
type: EventType.EntityActionEvent,
entityType: entity?.entityType,
entityUrn: urn,
entityUrn: entity.urn,
actionType: EntityActionType.AddIncident,
});
} else if (incidentUrn) {
const updatedIncidentResponse: any = await handleUpdateIncident(input, incidentUrn);
if (updatedIncidentResponse?.data?.updateIncident) {
const updatedIncident = getCacheIncident({
values: {
...values,
state: baseInput.status.state,
stage: baseInput.status.stage || '',
message: baseInput.status.message,
priority: values.priority || null,
assignees,
linkedAssets,
created: currentIncident.created,
},
user,
incidentUrn,
});
updateActiveIncidentInCache(client, urn, updatedIncident, PAGE_SIZE);
}
await handleUpdateIncident(input, incidentUrn);
showMessage('Incident Updated');
}

View File

@ -19,10 +19,13 @@ import { useGetEntityIncidentsQuery } from '@graphql/incident.generated';
import { EntityPrivileges, Incident } from '@types';
export const IncidentList = () => {
const { urn } = useEntityData();
const { urn, entityType } = useEntityData();
const refetchEntity = useRefetch();
const [showIncidentBuilder, setShowIncidentBuilder] = useState(false);
const [entity, setEntity] = useState<EntityStagedForIncident>();
const [entity, setEntity] = useState<EntityStagedForIncident>({
urn,
entityType,
});
const [visibleIncidents, setVisibleIncidents] = useState<IncidentTable>({
incidents: [],
groupBy: { category: [], priority: [], stage: [], state: [] },
@ -124,7 +127,7 @@ export const IncidentList = () => {
{renderListTable()}
{showIncidentBuilder && (
<IncidentDetailDrawer
urn={urn}
entity={entity}
mode={IncidentAction.CREATE}
onSubmit={() => {
setShowIncidentBuilder(false);
@ -133,7 +136,6 @@ export const IncidentList = () => {
}, 3000);
}}
onCancel={() => setShowIncidentBuilder(false)}
entity={entity}
/>
)}
</>

View File

@ -21,7 +21,7 @@ type Props = {
};
export const IncidentListTable = ({ incidentData, filter, refetch, privileges }: Props) => {
const { entityData } = useEntityData();
const { urn, entityData } = useEntityData();
const { groupBy } = filter;
const { expandedGroupIds, setExpandedGroupIds } = useGetExpandedTableGroupsFromEntityUrnInUrl(
@ -125,15 +125,15 @@ export const IncidentListTable = ({ incidentData, filter, refetch, privileges }:
</StyledTableContainer>
{focusIncidentUrn && focusedIncidentEntity && (
<IncidentDetailDrawer
urn={focusIncidentUrn}
entity={{
urn,
}}
mode={IncidentAction.EDIT}
incident={focusedIncident}
privileges={privileges}
onCancel={() => setFocusIncidentUrn(null)}
onSubmit={() => {
setTimeout(() => {
refetch();
}, 3000);
refetch();
}}
/>
)}

View File

@ -1,16 +1,12 @@
import { useApolloClient } from '@apollo/client';
import { Form, Input, Modal, message } from 'antd';
import React from 'react';
import { IncidentSelectField } from '@app/entityV2/shared/tabs/Incident/AcrylComponents/IncidentSelectedField';
import { getCacheIncident } from '@app/entityV2/shared/tabs/Incident/AcrylComponents/hooks/useIncidentHandler';
import { INCIDENT_OPTION_LABEL_MAPPING, INCIDENT_RESOLUTION_STAGES } from '@app/entityV2/shared/tabs/Incident/constant';
import { PAGE_SIZE, updateActiveIncidentInCache } from '@app/entityV2/shared/tabs/Incident/incidentUtils';
import { FormItem, ModalHeading, ModalTitleContainer } from '@app/entityV2/shared/tabs/Incident/styledComponents';
import { IncidentTableRow } from '@app/entityV2/shared/tabs/Incident/types';
import { Button, colors } from '@src/alchemy-components';
import analytics, { EntityActionType, EventType } from '@src/app/analytics';
import { useUserContext } from '@src/app/context/useUserContext';
import { useEntityData } from '@src/app/entity/shared/EntityContext';
import { ModalButtonContainer } from '@src/app/shared/button/styledComponents';
import handleGraphQLError from '@src/app/shared/handleGraphQLError';
@ -34,8 +30,6 @@ const ModalTitle = () => (
);
export const IncidentResolutionPopup = ({ incident, refetch, handleClose }: IncidentResolutionPopupProps) => {
const client = useApolloClient();
const { user } = useUserContext();
const { urn, entityType } = useEntityData();
const [updateIncidentStatusMutation] = useUpdateIncidentStatusMutation();
const [form] = Form.useForm();
@ -59,35 +53,15 @@ export const IncidentResolutionPopup = ({ incident, refetch, handleClose }: Inci
analytics.event({
type: EventType.EntityActionEvent,
entityType,
entityUrn: incident.urn,
entityUrn: urn,
actionType: EntityActionType.ResolvedIncident,
});
const values = {
title: incident.title,
description: incident.description,
type: incident.type,
priority: incident.priority,
state: IncidentState.Resolved,
customType: incident.customType,
stage: formData?.status || IncidentStage.Fixed,
message: formData?.note,
linkedAssets: incident.linkedAssets,
assignees: incident.assignees,
created: incident.created,
};
const updatedIncident = getCacheIncident({
values,
incidentUrn: incident.urn,
user,
});
updateActiveIncidentInCache(client, urn, updatedIncident, PAGE_SIZE);
message.success({ content: 'Incident updated!', duration: 2 });
refetch();
handleClose?.();
setTimeout(() => refetch(), 3000);
})
.catch((error) => {
console.log(error);
handleGraphQLError({
error,
defaultMessage: 'Failed to update incident! An unexpected error occurred',

View File

@ -36,7 +36,7 @@ export const IncidentTitleContainer = ({
}: {
privileges: EntityPrivileges;
setShowIncidentBuilder: Dispatch<SetStateAction<boolean>>;
setEntity: Dispatch<SetStateAction<EntityStagedForIncident | undefined>>;
setEntity: Dispatch<SetStateAction<EntityStagedForIncident>>;
}) => {
return (
<TitleContainer>

View File

@ -120,7 +120,7 @@ describe('Utility Functions', () => {
siblingsSearch: { searchResults: [{ entity: { incidents: { incidents: [{ id: 2 }] } } }] },
},
};
expect(getExistingIncidents(currData)).toEqual([{ id: 1 }, { id: 2 }]);
expect(getExistingIncidents(currData)).toEqual([{ id: 1 }]);
});
test('should return main entity data in options', () => {

View File

@ -109,7 +109,7 @@ export const updateListIncidentsCache = (client, urn, incident, pageSize) => {
},
// Add the missing 'siblings' field with the appropriate data
siblings: currData?.entity?.siblings || null,
siblingsSearch: null,
siblingsSearch: currData?.entity.siblingsSearch || null,
},
},
});

View File

@ -95,17 +95,17 @@ export type IncidentTableRow = {
};
export type IncidentEditorProps = {
entity: EntityStagedForIncident;
incidentUrn?: string;
refetch?: () => void;
onSubmit?: (incident?: Incident) => void;
onClose?: () => void;
data?: IncidentTableRow;
mode?: IncidentAction;
entity?: EntityStagedForIncident;
urn?: string;
};
export type IncidentLinkedAssetsListProps = {
initialUrn?: string;
form: any;
data?: IncidentTableRow;
mode: IncidentAction;
@ -131,7 +131,7 @@ export enum IncidentConstant {
export type EntityStagedForIncident = {
urn: string;
platform?: DataPlatform;
entityType: EntityType;
entityType?: EntityType; // TODO remove this.
};
export type IncidentBuilderSiblingOptions = {
@ -139,8 +139,18 @@ export type IncidentBuilderSiblingOptions = {
disabled?: boolean;
} & Partial<EntityStagedForIncident>;
export type IncidentHandlerProps = {
mode: IncidentAction;
onSubmit?: () => void;
incidentUrn: string | undefined;
user: CorpUser | null | undefined;
entity: EntityStagedForIncident | undefined;
assignees: CorpUser[];
linkedAssets: string[];
};
export type CreateIncidentButtonProps = {
privileges: EntityPrivileges;
setShowIncidentBuilder: Dispatch<SetStateAction<boolean>>;
setEntity: Dispatch<SetStateAction<EntityStagedForIncident | undefined>>;
setEntity: Dispatch<SetStateAction<EntityStagedForIncident>>;
};

View File

@ -126,14 +126,15 @@ const orderedIncidents = (priorityIncidentGroups) => {
return newOrderedIncidents;
};
export const getIncidentType = (incident: Incident) =>
incident.incidentType === IncidentType.Custom ? incident.customType : incident.incidentType;
export const createIncidentGroups = (incidents: Array<Incident>): IncidentGroupBy => {
// Pre-sort the list of incidents based on which has been most recently created.
incidents?.sort((a, b) => a?.created?.time - b?.created?.time);
// Group incidents by type, stage, and priority
const typeToIncidents = groupIncidentsBy(incidents, (incident) =>
incident?.incidentType === IncidentType.Custom ? incident?.customType : incident?.incidentType,
);
const typeToIncidents = groupIncidentsBy(incidents, (incident) => getIncidentType(incident));
const stageToIncidents = groupIncidentsBy(incidents, (incident) => incident?.status?.stage);
const stateToIncidents = groupIncidentsBy(incidents, (incident) => incident?.status?.state);
const priorityToIncidents = groupIncidentsBy(incidents, (incident) => incident.priority);
@ -308,9 +309,10 @@ const extractFilterOptionListFromIncidents = (incidents: Incident[]) => {
if (index > -1) {
remainingIncidentTypes.splice(index, 1);
}
const categoryName =
category === IncidentType.Custom && incident.customType ? incident.customType : incident.incidentType;
filterGroupCounts.category[categoryName] = (filterGroupCounts.category[categoryName] || 0) + 1;
const categoryName = getIncidentType(incident);
if (categoryName) {
filterGroupCounts.category[categoryName] = (filterGroupCounts.category[categoryName] || 0) + 1;
}
}
// filter out tracked stages
@ -390,8 +392,7 @@ const getFilteredIncidents = (incidents: Incident[], filter: IncidentListFilter)
// Apply cateory, priority, and stage
return incidents.filter((incident: Incident) => {
const categoryName =
incident.incidentType === IncidentType.Custom ? incident.customType : incident.incidentType;
const categoryName = getIncidentType(incident);
const matchesCategory = category.length === 0 || (categoryName ? category.includes(categoryName) : false);
const matchesPriority = priority.length === 0 || priority.includes(incident.priority || 'None');
const matchesStage = stage.length === 0 || stage.includes(incident.status.stage || 'None');
@ -509,10 +510,7 @@ export const getSortedIncidents = (record: any, sortedOptions: { sortColumn: str
};
export const getExistingIncidents = (currData) => {
return [
...(currData?.entity?.incidents?.incidents || []),
...(currData?.entity?.siblingsSearch?.searchResults[0]?.entity?.incidents?.incidents || []),
];
return [...(currData?.entity?.incidents?.incidents || [])];
};
/**