Skip to content
This repository was archived by the owner on Apr 2, 2024. It is now read-only.

cim#18 Use transition instead of process_state as much as possible #199

Merged
merged 3 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/custom-surf/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
OpenServiceTicketPayload,
ServiceTicket,
ServiceTicketBackgroundJobCount,
ServiceTicketProcessState,
ServiceTicketTransition,
ServiceTicketWithDetails,
} from "custom/types";
import { intl } from "locale/i18n";
Expand Down Expand Up @@ -230,6 +230,14 @@ export class CustomApiClient extends CustomApiClientInterface {
return this.postPutJson(prefix_cim_dev_uri("surf/cim/tickets"), ticket, "post", false, true);
};

cimAcceptTicket = (ticket_id: string): Promise<ServiceTicketWithDetails> => {
return this.postPutJson(prefix_cim_dev_uri(`surf/cim/tickets/${ticket_id}/accept`), {}, "post", false);
};

cimAbortTicket = (ticket_id: string): Promise<ServiceTicketWithDetails> => {
return this.postPutJson(prefix_cim_dev_uri(`surf/cim/tickets/${ticket_id}/abort`), {}, "post", false);
};

cimOpenTicket = (payload: OpenServiceTicketPayload): Promise<{ id: string }> => {
return this.postPutJson(
prefix_cim_dev_uri(`surf/cim/tickets/${payload.cim_ticket_id}/open`),
Expand Down Expand Up @@ -277,13 +285,9 @@ export class CustomApiClient extends CustomApiClientInterface {
return this.fetchJson<ServiceTicketWithDetails>(prefix_cim_dev_uri(`surf/cim/tickets/${ticket_id}`));
};

// todo cim#18
cimChangeTicketStateById = (
ticket_id: string,
state: ServiceTicketProcessState
): Promise<ServiceTicketWithDetails> => {
return this.fetchJson<ServiceTicketWithDetails>(
prefix_cim_dev_uri(`surf/cim/tickets/${ticket_id}/state/${state}`)
cimGetAllowedTransitions = (ticket_id: string): Promise<ServiceTicketTransition[]> => {
return this.fetchJson<ServiceTicketTransition[]>(
prefix_cim_dev_uri(`surf/cim/tickets/${ticket_id}/transitions`)
);
};

Expand Down
4 changes: 1 addition & 3 deletions src/custom-surf/components/cim/OpenForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
*/

import UserInputFormWizard from "components/inputForms/UserInputFormWizard";
import { ServiceTicketProcessState } from "custom/types";
import React, { useCallback, useContext, useEffect, useState } from "react";
import { useIntl } from "react-intl";
import ApplicationContext from "utils/ApplicationContext";
Expand All @@ -36,8 +35,7 @@ export default function OpenForm({ formKey, ticketId, handleSubmit, handleCancel

const submit = useCallback(
(userInputs: {}[]) => {
// Pass the ticket id and next process state. Backend validates the state transition.
userInputs = [{ ticket_id: ticketId, next_process_state: ServiceTicketProcessState.OPEN }, ...userInputs];
userInputs = [{ ticket_id: ticketId }, ...userInputs];
let promise = customApiClient.cimStartForm(formKey, userInputs).then(
(form) => {
handleSubmit(form);
Expand Down
145 changes: 79 additions & 66 deletions src/custom-surf/pages/ServiceTicketDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ import {
EuiTitle,
} from "@elastic/eui";
import { TabbedSection } from "components/subscriptionDetail/TabbedSection";
import ConfirmationDialogContext from "contextProviders/ConfirmationDialogProvider";
import BackgroundJobLogs from "custom/components/cim/BackgroundJobLogs";
import ImpactedObjects from "custom/components/cim/ImpactedObjects";
import { ticketDetail } from "custom/pages/ServiceTicketDetailStyling";
import {
ServiceTicketLog,
ServiceTicketLogType,
ServiceTicketProcessState,
ServiceTicketTransition,
ServiceTicketType,
ServiceTicketWithDetails,
} from "custom/types";
Expand All @@ -59,18 +61,34 @@ interface IProps {
interface Action {
translation: string;
onClick: () => void;
requiredState: ServiceTicketProcessState[];
transition?: ServiceTicketTransition;
requiredStates?: ServiceTicketProcessState[];
}

const renderLogItemActions = (ticket: ServiceTicketWithDetails, actions: Action[]) => {
const renderLogItemActions = (
ticket: ServiceTicketWithDetails,
allowedTransitions: ServiceTicketTransition[],
actions: Action[]
) => {
const enabled = (action: Action) => {
if (action.transition) {
if (ticket.transition_action) {
// There is an ongoing transition -> hide actions that would try (but fail) to start a transition
return false;
}
return allowedTransitions.includes(action.transition);
}
if (action.requiredStates) {
return action.requiredStates.includes(ticket.process_state);
}
return true;
};

return (
<EuiFlexGroup gutterSize="s" className="buttons">
{actions.map((action: Action, index: number) => (
<EuiFlexItem key={index}>
<EuiButton
onClick={() => action.onClick()}
isDisabled={!action.requiredState.includes(ticket.process_state)}
>
<EuiButton onClick={() => action.onClick()} isDisabled={!enabled(action)}>
<FormattedMessage id={action.translation} />
</EuiButton>
</EuiFlexItem>
Expand All @@ -82,18 +100,18 @@ const renderLogItemActions = (ticket: ServiceTicketWithDetails, actions: Action[
const ServiceTicketDetail = () => {
const { id } = useParams<IProps>();
const [ticket, setTicket] = useState<ServiceTicketWithDetails>();
const [allowedTransitions, setAllowedTransitions] = useState<ServiceTicketTransition[]>([]);
const [notFound, setNotFound] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const { theme, customApiClient, redirect } = useContext(ApplicationContext);
const { showConfirmDialog } = useContext(ConfirmationDialogContext);
const closeModal = () => setIsModalVisible(false);
const showModal = () => setIsModalVisible(true);

useInterval(async () => {
if (ticket?.transition_action) {
console.log("Refreshing");
const ticket = await customApiClient.cimTicketById(id);
setTicket(ticket);
}
console.log("Refreshing");
const ticket = await customApiClient.cimTicketById(id);
setTicket(ticket);
}, 5000);

useEffect(() => {
Expand All @@ -112,6 +130,12 @@ const ServiceTicketDetail = () => {
});
}, [id, customApiClient]);

useEffect(() => {
customApiClient.cimGetAllowedTransitions(id).then((res) => {
setAllowedTransitions(res);
});
}, [id, customApiClient, ticket]);

if (notFound) {
return (
<h2>
Expand Down Expand Up @@ -166,25 +190,11 @@ const ServiceTicketDetail = () => {
}));
}

const changeTicketState = (state: ServiceTicketProcessState) => {
customApiClient
.cimChangeTicketStateById(id, state)
.then((res) => {
setTicket(res);
})
.catch((err) => {
if (err.response && err.response.status === 404) {
setNotFound(true);
} else {
throw err;
}
});
};
const acceptImpactedObjects = () => {
changeTicketState(ServiceTicketProcessState.OPEN_ACCEPTED);
customApiClient.cimAcceptTicket(id).then((res) => setTicket(res));
};
const abortTicket = () => {
changeTicketState(ServiceTicketProcessState.ABORTED);
customApiClient.cimAbortTicket(id).then((res) => setTicket(res));
};
const openTicket = () => {
redirect(`/tickets/${ticket._id}/open`);
Expand All @@ -202,31 +212,40 @@ const ServiceTicketDetail = () => {
redirect(`/tickets/${ticket._id}/mails`);
};

const onButtonClick = (question: string, confirm: (e: React.MouseEvent<HTMLButtonElement>) => void) => {
showConfirmDialog({
question: question,
confirmAction: confirm,
cancelAction: () => {},
leavePage: false,
});
};

let actions: Action[] = [
{
translation: "tickets.action.opening",
onClick: openTicket,
requiredState: [ServiceTicketProcessState.OPEN_ACCEPTED],
transition: ServiceTicketTransition.OPENING,
},
{
translation: "tickets.action.updating",
onClick: updateTicket,
requiredState: [ServiceTicketProcessState.OPEN, ServiceTicketProcessState.UPDATED],
transition: ServiceTicketTransition.UPDATING,
},
{
translation: "tickets.action.closing",
onClick: closeTicket,
requiredState: [ServiceTicketProcessState.OPEN, ServiceTicketProcessState.UPDATED],
transition: ServiceTicketTransition.CLOSING,
},
{
translation: "tickets.action.aborting",
onClick: abortTicket,
requiredState: [ServiceTicketProcessState.OPEN_ACCEPTED, ServiceTicketProcessState.OPEN_RELATED],
onClick: () => onButtonClick("Aborting the ticket is irreversible. Are you sure?", abortTicket),
transition: ServiceTicketTransition.ABORTING,
},
{
translation: "tickets.action.show",
onClick: viewLastSentMails,
requiredState: [
requiredStates: [
ServiceTicketProcessState.OPEN,
ServiceTicketProcessState.UPDATED,
ServiceTicketProcessState.CLOSED,
Expand All @@ -238,18 +257,11 @@ const ServiceTicketDetail = () => {
actions.splice(2, 0, {
translation: "tickets.action.open_and_close",
onClick: openAndCloseTicket,
requiredState: [
ServiceTicketProcessState.OPEN_ACCEPTED,
ServiceTicketProcessState.OPEN,
ServiceTicketProcessState.UPDATED,
],
transition: ServiceTicketTransition.OPEN_AND_CLOSE,
});
}

const isUpdateImpactActive = [
ServiceTicketProcessState.OPEN_ACCEPTED,
ServiceTicketProcessState.OPEN_RELATED,
].includes(ticket.process_state);
const isUpdateImpactActive = allowedTransitions.includes(ServiceTicketTransition.ACCEPTING);

console.log("Rendering the ServiceTicketDetail page with ticket;", ticket);

Expand Down Expand Up @@ -299,29 +311,30 @@ const ServiceTicketDetail = () => {
</EuiFacetButton>
</EuiFlexItem>

{ticket?.transition_action === null && ticket?.process_state === "initial" && (
<EuiFlexItem grow={false} style={{ minWidth: 200 }}>
<EuiButton
color={"danger"}
iconType="refresh"
isDisabled={false}
size="m"
fill
onClick={() =>
customApiClient.cimRestartOpenRelate(ticket._id).then(() => {
setFlash(
intl.formatMessage({
id: "tickets.action.background_job_restarted",
})
);
redirect("/tickets");
})
}
>
{intl.formatMessage({ id: "tickets.action.restart_open_relate" })}
</EuiButton>
</EuiFlexItem>
)}
{ticket?.transition_action === null &&
ticket?.process_state === ServiceTicketProcessState.NEW && (
<EuiFlexItem grow={false} style={{ minWidth: 200 }}>
<EuiButton
color={"danger"}
iconType="refresh"
isDisabled={false}
size="m"
fill
onClick={() =>
customApiClient.cimRestartOpenRelate(ticket._id).then(() => {
setFlash(
intl.formatMessage({
id: "tickets.action.background_job_restarted",
})
);
redirect("/tickets");
})
}
>
{intl.formatMessage({ id: "tickets.action.restart_open_relate" })}
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
<div className="mod-ticket-detail">
<table className={`detail-block`}>
Expand Down Expand Up @@ -419,7 +432,7 @@ const ServiceTicketDetail = () => {
></TabbedSection>
</div>
<EuiSpacer />
{renderLogItemActions(ticket, actions)}
{renderLogItemActions(ticket, allowedTransitions, actions)}
<EuiSpacer />
<EuiSpacer size="s" />
<ImpactedObjects ticket={ticket} withSubscriptions={true} updateable={isUpdateImpactActive} />
Expand Down
29 changes: 15 additions & 14 deletions src/custom-surf/pages/ServiceTickets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "@elastic/eui";
import ServiceTicketFilter from "custom/components/ServiceTicketFilter";
import { tableTickets } from "custom/pages/ServiceTicketsStyling";
import { ServiceTicket, ServiceTicketProcessState } from "custom/types";
import { ServiceTicket, ServiceTicketProcessState, ServiceTicketTransition } from "custom/types";
import { renderStringAsDateTime } from "custom/Utils";
import { intl } from "locale/i18n";
import debounce from "lodash/debounce";
Expand Down Expand Up @@ -311,19 +311,20 @@ class ServiceTickets extends React.PureComponent<IProps, IState> {
data-label={intl.formatMessage({ id: "tickets.table.jira_ticket_id" })}
className="jira_ticket_id"
>
{ticket.process_state === "initial" && !ticket.transition_action && (
<EuiIconTip
aria-label="Warning"
size="l"
type="alert"
color="warning"
content={intl.formatMessage({
id: "tickets.table.background_job_warning",
})}
/>
)}
{ticket.process_state === "initial" &&
ticket.transition_action === "relating" && (
{ticket.process_state === ServiceTicketProcessState.NEW &&
!ticket.transition_action && (
<EuiIconTip
aria-label="Warning"
size="l"
type="alert"
color="warning"
content={intl.formatMessage({
id: "tickets.table.background_job_warning",
})}
/>
)}
{ticket.process_state === ServiceTicketProcessState.NEW &&
ticket.transition_action === ServiceTicketTransition.RELATING && (
<EuiLoadingSpinner size="s" style={{ marginRight: "9px" }} />
)}
<Link to={`/tickets/${ticket._id}`}>{ticket.jira_ticket_id}</Link>
Expand Down
20 changes: 12 additions & 8 deletions src/custom-surf/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,8 @@ export interface CloseServiceTicketPayload extends OpenServiceTicketPayload {
end_date?: string;
}

export enum ServiceTicketState {
INITIAL = "initial",
ACTIVE = "active",
ABORTED = "aborted",
CLOSED = "closed",
}

export enum ServiceTicketProcessState {
INITIAL = "initial",
NEW = "new",
OPEN = "open",
OPEN_RELATED = "open_related",
OPEN_ACCEPTED = "open_accepted",
Expand All @@ -50,6 +43,17 @@ export enum ServiceTicketProcessState {
CLOSED = "closed",
}

export enum ServiceTicketTransition {
RELATING = "relating",
ACCEPTING = "accepting",
ABORTING = "aborting",
OPENING = "opening",
OPEN_AND_CLOSE = "open_and_close",
UPDATING = "updating",
CLOSING = "closing",
CLEANING = "cleaning",
}

export interface ServiceTicket {
_id: string;
jira_ticket_id: string;
Expand Down