Skip to content

Commit

Permalink
ep: switch policy steps to use urql (#3799)
Browse files Browse the repository at this point in the history
* switch to urql

* fix err reporting

* set additional typename

* handle deleted step
  • Loading branch information
mastercactapus committed Apr 9, 2024
1 parent 95ea8c5 commit 2ab8247
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 127 deletions.
19 changes: 11 additions & 8 deletions web/src/app/escalation-policies/PolicyStepCreateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,18 @@ function PolicyStepCreateDialog(props: {
maxWidth='sm'
onClose={props.onClose}
onSubmit={() =>
createStep({
input: {
escalationPolicyID: props.escalationPolicyID,
delayMinutes: parseInt(
(value && value.delayMinutes) || defaultValue.delayMinutes,
),
targets: (value && value.targets) || defaultValue.targets,
createStep(
{
input: {
escalationPolicyID: props.escalationPolicyID,
delayMinutes: parseInt(
(value && value.delayMinutes) || defaultValue.delayMinutes,
),
targets: (value && value.targets) || defaultValue.targets,
},
},
}).then((result) => {
{ additionalTypenames: ['EscalationPolicy'] },
).then((result) => {
if (!result.error) {
props.onClose()
}
Expand Down
156 changes: 57 additions & 99 deletions web/src/app/escalation-policies/PolicyStepsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
import React, { Suspense, useRef, useState } from 'react'
import React, { Suspense, useEffect, useState } from 'react'
import Button from '@mui/material/Button'
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import Dialog from '@mui/material/Dialog'
import Typography from '@mui/material/Typography'
import { Add } from '@mui/icons-material'
import { gql, useMutation } from '@apollo/client'
import { gql, useMutation } from 'urql'
import FlatList from '../lists/FlatList'
import CreateFAB from '../lists/CreateFAB'
import PolicyStepCreateDialog from './PolicyStepCreateDialog'
import PolicyStepCreateDialogDest from './PolicyStepCreateDialogDest'
import { useResetURLParams, useURLParam } from '../actions'
import DialogTitleWrapper from '../dialogs/components/DialogTitleWrapper'
import DialogContentError from '../dialogs/components/DialogContentError'
import { policyStepsQuery } from './PolicyStepsQuery'
import { useIsWidthDown } from '../util/useWidth'
import { reorderList } from '../rotations/util'
import PolicyStepEditDialog from './PolicyStepEditDialog'
import PolicyStepDeleteDialog from './PolicyStepDeleteDialog'
import PolicyStepEditDialogDest from './PolicyStepEditDialogDest'
import OtherActions from '../util/OtherActions'
import {
getStepNumber,
renderChips,
renderChipsDest,
renderDelayMessage,
} from './stepUtil'
import { renderChips, renderChipsDest, renderDelayMessage } from './stepUtil'
import { useExpFlag } from '../util/useExpFlag'
import { Destination, EscalationPolicy, Target } from '../../schema'
import { Destination, Target } from '../../schema'

const mutation = gql`
mutation UpdateEscalationPolicyMutation(
Expand All @@ -37,121 +31,82 @@ const mutation = gql`
}
`

type StepInfo = {
id: string
delayMinutes: number
stepNumber: number
actions?: Destination[]
targets: Target[]
}

export type PolicyStepsCardProps = {
escalationPolicyID: string
repeat: number
steps: Array<{
id: string
delayMinutes: number
stepNumber: number
actions?: Destination[]
targets: Target[]
}>
steps: Array<StepInfo>
}

export default function PolicyStepsCard(
props: PolicyStepsCardProps,
): React.ReactNode {
const hasDestTypesFlag = useExpFlag('dest-types')

const { escalationPolicyID, repeat, steps = [] } = props

const isMobile = useIsWidthDown('md')
const stepNumParam = 'createStep'
const [createStep, setCreateStep] = useURLParam<boolean>(stepNumParam, false)
const resetCreateStep = useResetURLParams(stepNumParam)

const oldID = useRef(null)
const oldIdx = useRef(null)
const newIdx = useRef(null)
const [stepIDs, setStepIDs] = useState<string[]>(props.steps.map((s) => s.id))

type Swap = { oldIndex: number; newIndex: number }
const [lastSwap, setLastSwap] = useState<Array<Swap>>([])
useEffect(() => {
setStepIDs(props.steps.map((s) => s.id))
}, [props.steps.map((s) => s.id).join(',')]) // update steps when order changes

const [error, setError] = useState<Error | null>(null)
const orderedSteps = stepIDs
.map((id) => props.steps.find((s) => s.id === id))
.filter((s) => s) as StepInfo[]

const [editStepID, setEditStepID] = useURLParam<string>('editStep', '')
const editStep = steps.find((step) => step.id === editStepID)
const editStep = props.steps.find((step) => step.id === editStepID)
const resetEditStep = useResetURLParams('editStep')
const [deleteStep, setDeleteStep] = useState('')

const [updateEscalationPolicy] = useMutation(mutation, {
onCompleted: () => {
oldID.current = null
oldIdx.current = null
newIdx.current = null
},
onError: (err) => setError(err),
})
const [updateError, setUpdateError] = useState<Error | null>(null)
const [status, commit] = useMutation(mutation)

useEffect(() => {
if (status.error) {
setUpdateError(status.error)
setStepIDs(props.steps.map((s) => s.id))
}
}, [status.error])

async function onReorder(
oldIndex: number,
newIndex: number,
): Promise<unknown> {
setLastSwap(lastSwap.concat({ oldIndex, newIndex }))

const updatedStepIDs = reorderList(
steps.map((step) => step.id),
oldIndex,
newIndex,
)
const newStepIDs = reorderList(stepIDs, oldIndex, newIndex)
setStepIDs(newStepIDs)

return updateEscalationPolicy({
variables: {
return commit(
{
input: {
id: escalationPolicyID,
stepIDs: updatedStepIDs,
id: props.escalationPolicyID,
stepIDs: newStepIDs,
},
},
update: (cache, { data }) => {
// mutation returns true on a success
if (!data.updateEscalationPolicy) {
return
}

// get the current state of the steps in the cache
const cacheData = cache.readQuery<{
escalationPolicy: EscalationPolicy
}>({
query: policyStepsQuery,
variables: { id: escalationPolicyID },
})
if (!cacheData) throw new Error('Cache data not found')
const escalationPolicy = cacheData.escalationPolicy
const steps = escalationPolicy.steps.slice()

if (steps.length > 0) {
const newSteps = reorderList(steps, oldIndex, newIndex)

// write new steps order to cache
cache.writeQuery({
query: policyStepsQuery,
variables: { id: escalationPolicyID },
data: {
escalationPolicy: {
...escalationPolicy,
steps: newSteps,
},
},
})
}
},
optimisticResponse: {
__typename: 'Mutation',
updateEscalationPolicy: true,
},
})
{ additionalTypenames: ['EscalationPolicy'] },
)
}

function renderRepeatText(): React.ReactNode {
if (!steps.length) {
if (!stepIDs.length) {
return null
}

let text = ''
if (repeat === 0) text = 'Do not repeat'
else if (repeat === 1) text = 'Repeat once'
else text = `Repeat ${repeat} times`
if (props.repeat === 0) text = 'Do not repeat'
else if (props.repeat === 1) text = 'Repeat once'
else text = `Repeat ${props.repeat} times`

return (
<Typography variant='subtitle1' component='p' sx={{ pl: 2, pb: 2 }}>
Expand All @@ -160,8 +115,6 @@ export default function PolicyStepsCard(
)
}

const { message: errMsg } = error || {}

return (
<React.Fragment>
{isMobile && (
Expand All @@ -171,12 +124,12 @@ export default function PolicyStepsCard(
<React.Fragment>
{hasDestTypesFlag ? (
<PolicyStepCreateDialogDest
escalationPolicyID={escalationPolicyID}
escalationPolicyID={props.escalationPolicyID}
onClose={resetCreateStep}
/>
) : (
<PolicyStepCreateDialog
escalationPolicyID={escalationPolicyID}
escalationPolicyID={props.escalationPolicyID}
onClose={resetCreateStep}
/>
)}
Expand All @@ -203,20 +156,25 @@ export default function PolicyStepsCard(
data-cy='steps-list'
emptyMessage='No steps currently on this Escalation Policy'
headerNote='Notify the following:'
items={steps.map((step) => ({
items={orderedSteps.map((step, idx) => ({
id: step.id,
disableTypography: true,
title: (
<Typography component='h4' variant='subtitle1' sx={{ pb: 1 }}>
<b>Step #{getStepNumber(step.id, steps)}:</b>
<b>Step #{idx + 1}:</b>
</Typography>
) as unknown as string, // needed to work around MUI incorrect types
subText: (
<React.Fragment>
{step.actions
? renderChipsDest(step.actions)
: renderChips(step)}
{renderDelayMessage(steps, step, repeat)}
{renderDelayMessage(
step,
idx,
props.repeat,
idx === orderedSteps.length - 1,
)}
</React.Fragment>
),
secondaryAction: (
Expand All @@ -238,25 +196,25 @@ export default function PolicyStepsCard(
/>
{renderRepeatText()}
</Card>
<Dialog open={Boolean(error)} onClose={() => setError(null)}>
<Dialog open={Boolean(updateError)} onClose={() => setUpdateError(null)}>
<DialogTitleWrapper
fullScreen={useIsWidthDown('md')}
title='An error occurred'
/>
<DialogContentError error={errMsg} />
<DialogContentError error={updateError?.message} />
</Dialog>
<Suspense>
{editStep && (
<React.Fragment>
{hasDestTypesFlag ? (
<PolicyStepEditDialogDest
escalationPolicyID={escalationPolicyID}
escalationPolicyID={props.escalationPolicyID}
onClose={resetEditStep}
stepID={editStep.id}
/>
) : (
<PolicyStepEditDialog
escalationPolicyID={escalationPolicyID}
escalationPolicyID={props.escalationPolicyID}
onClose={resetEditStep}
step={editStep}
/>
Expand All @@ -265,7 +223,7 @@ export default function PolicyStepsCard(
)}
{deleteStep && (
<PolicyStepDeleteDialog
escalationPolicyID={escalationPolicyID}
escalationPolicyID={props.escalationPolicyID}
onClose={() => setDeleteStep('')}
stepID={deleteStep}
/>
Expand Down
16 changes: 5 additions & 11 deletions web/src/app/escalation-policies/PolicyStepsQuery.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react'
import { gql, useQuery } from '@apollo/client'
import { gql, useQuery } from 'urql'
import PolicyStepsCard from './PolicyStepsCard'
import Spinner from '../loading/components/Spinner'
import { GenericError, ObjectNotFound } from '../error-pages'
import { useExpFlag } from '../util/useExpFlag'

Expand Down Expand Up @@ -60,16 +59,11 @@ export const policyStepsQueryDest = gql`
function PolicyStepsQuery(props: { escalationPolicyID: string }): JSX.Element {
const hasDestTypesFlag = useExpFlag('dest-types')

const { data, loading, error } = useQuery(
hasDestTypesFlag ? policyStepsQueryDest : policyStepsQuery,
{
variables: { id: props.escalationPolicyID },
},
)
const [{ data, error }] = useQuery({
query: hasDestTypesFlag ? policyStepsQueryDest : policyStepsQuery,
variables: { id: props.escalationPolicyID },
})

if (!data && loading) {
return <Spinner />
}
if (error) {
return <GenericError error={error.message} />
}
Expand Down
10 changes: 4 additions & 6 deletions web/src/app/escalation-policies/stepUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,11 @@ export function renderChips({ targets: _t }: Step): ReactElement {
* repeats, and if the message is rendering on the last step
*/
export function renderDelayMessage(
steps: Step[],
step: Step,
idx: number,
repeat: number,
isLastStep: boolean,
): ReactNode {
const len = steps.length
const isLastStep = getStepNumber(step.id, steps) === len

// if it's the last step and should not repeat, do not render end text
if (isLastStep && repeat === 0) {
return null
Expand All @@ -121,10 +119,10 @@ export function renderDelayMessage(
const pluralizer = (x: number): string => (x === 1 ? '' : 's')

let repeatText = `Move on to step #${
getStepNumber(step.id, steps) + 1
idx + 1
} after ${step.delayMinutes} minute${pluralizer(step.delayMinutes)}`

if (isLastStep && getStepNumber(step.id, steps) === 1) {
if (isLastStep && idx === 0) {
repeatText = `Repeat after ${step.delayMinutes} minutes`
}

Expand Down
6 changes: 3 additions & 3 deletions web/src/app/rotations/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export function calcNewActiveIndex(

// reorderList will move an item from the oldIndex to the newIndex, preserving order
// returning the result as a new array.
export function reorderList(
_items: unknown[],
export function reorderList<T>(
_items: T[],
oldIndex: number,
newIndex: number,
): unknown[] {
): T[] {
const items = _items.slice()
items.splice(oldIndex, 1) // remove 1 element from oldIndex position
items.splice(newIndex, 0, _items[oldIndex]) // add dest to newIndex position
Expand Down

0 comments on commit 2ab8247

Please sign in to comment.