Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ep: switch policy steps to use urql #3799

Merged
merged 4 commits into from
Apr 9, 2024
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
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
Loading