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

feat: on call notification channel separation #3046

Merged
merged 16 commits into from
Jun 5, 2023
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ ensure-yarn: # Yarn ensures the correct version of yarn is installed

yarn:
corepack enable
corepack prepare yarn@stable --activate
corepack prepare yarn@$(YARN_VERSION) --activate

check-js: generate $(NODE_DEPS)
$(MAKE) ensure-yarn
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import React, { useState } from 'react'
import { mapOnCallErrors, NO_DAY, Value } from './util'
import FormDialog from '../../dialogs/FormDialog'
import ScheduleOnCallNotificationsForm from './ScheduleOnCallNotificationsForm'
import { useOnCallRulesData, useSetOnCallRulesSubmit } from './hooks'
import { NO_DAY, Value, mapOnCallErrors } from './util'

interface ScheduleOnCallNotificationsCreateDialogProps {
onClose: () => void
scheduleID: string
}

const defaultValue: Value = {
time: null,
weekdayFilter: NO_DAY,
type: 'SLACK_CHANNEL',
channelFields: {
slackChannelID: null,
},
}

export default function ScheduleOnCallNotificationsCreateDialog(
props: ScheduleOnCallNotificationsCreateDialogProps,
): JSX.Element {
const { onClose, scheduleID } = props
const [value, setValue] = useState<Value | null>(null)
const [slackType, setSlackType] = useState('channel')
const [value, setValue] = useState<Value>(defaultValue)

const { q, zone, rules } = useOnCallRulesData(scheduleID)

const newValue: Value = value || {
time: null,
weekdayFilter: NO_DAY,
slackChannelID: null,
slackUserGroup: null,
}
if (!newValue.slackChannelID) delete newValue.slackChannelID
if (!newValue.slackUserGroup) delete newValue.slackUserGroup
const { m, submit } = useSetOnCallRulesSubmit(
scheduleID,
zone,
newValue,
value,
...rules,
)

Expand All @@ -47,10 +47,8 @@ export default function ScheduleOnCallNotificationsCreateDialog(
<ScheduleOnCallNotificationsForm
scheduleID={scheduleID}
errors={fieldErrors}
value={newValue}
onChange={(value) => setValue(value)}
slackType={slackType}
setSlackType={setSlackType}
value={value}
onChange={setValue}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import React, { useState } from 'react'

import { EVERY_DAY, mapOnCallErrors, NO_DAY, Value } from './util'
import {
channelFieldsFromTarget,
channelTypeFromTarget,
EVERY_DAY,
mapOnCallErrors,
NO_DAY,
Value,
} from './util'
import { useOnCallRulesData, useSetOnCallRulesSubmit } from './hooks'
import FormDialog from '../../dialogs/FormDialog'
import ScheduleOnCallNotificationsForm from './ScheduleOnCallNotificationsForm'
Expand All @@ -17,7 +24,6 @@ export default function ScheduleOnCallNotificationsEditDialog(
p: ScheduleOnCallNotificationsEditDialogProps,
): JSX.Element {
const [value, setValue] = useState<Value | null>(null)
const [slackType, setSlackType] = useState('channel')

const { q, zone, rules } = useOnCallRulesData(p.scheduleID)

Expand All @@ -27,14 +33,8 @@ export default function ScheduleOnCallNotificationsEditDialog(
? DateTime.fromFormat(rule.time, 'HH:mm', { zone }).toISO()
: null,
weekdayFilter: rule?.time ? rule.weekdayFilter || EVERY_DAY : NO_DAY,
slackChannelID:
rule?.target.type === 'slackChannel'
? rule?.target.id
: rule?.target.id.split(':')[1],
slackUserGroup:
rule?.target.type === 'slackUserGroup'
? rule?.target.id.split(':')[0]
: null,
type: channelTypeFromTarget(rule?.target),
channelFields: channelFieldsFromTarget(rule?.target),
}
const { m, submit } = useSetOnCallRulesSubmit(
p.scheduleID,
Expand All @@ -57,9 +57,7 @@ export default function ScheduleOnCallNotificationsEditDialog(
scheduleID={p.scheduleID}
errors={fieldErrors}
value={newValue}
onChange={(value) => setValue(value)}
slackType={slackType}
setSlackType={setSlackType}
onChange={setValue}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
import React, { useState } from 'react'
import FormControlLabel from '@mui/material/FormControlLabel'
import Grid from '@mui/material/Grid'
import RadioGroup from '@mui/material/RadioGroup'
import Radio from '@mui/material/Radio'
import { DateTime } from 'luxon'
import { Checkbox, Tooltip, Typography } from '@mui/material'
import {
Checkbox,
FormControlLabel,
Grid,
MenuItem,
Radio,
RadioGroup,
Select,
SelectChangeEvent,
Typography,
} from '@mui/material'
import InfoIcon from '@mui/icons-material/Info'
import makeStyles from '@mui/styles/makeStyles'
import { DateTime } from 'luxon'
import React from 'react'

import { FormContainer, FormField } from '../../forms'
import { SlackChannelSelect, SlackUserGroupSelect } from '../../selection'
import { ISOTimePicker } from '../../util/ISOPickers'
import { Value, NO_DAY, EVERY_DAY, RuleFieldError } from './util'
import { useConfigValue } from '../../util/RequireConfig'
import { Time } from '../../util/Time'
import { useScheduleTZ } from '../useScheduleTZ'
import { useExpFlag } from '../../util/useExpFlag'
import { useScheduleTZ } from '../useScheduleTZ'
import {
EVERY_DAY,
NO_DAY,
NotificationChannelType,
RuleFieldError,
Value,
getEmptyChannelFields,
} from './util'

const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']

Expand All @@ -23,41 +37,128 @@ interface ScheduleOnCallNotificationsFormProps {
value: Value
errors: RuleFieldError[]
onChange: (val: Value) => void
slackType: string
setSlackType: (slackType: string) => void
}

const useStyles = makeStyles({
margin0: { margin: 0 },
tzNote: { fontStyle: 'italic' },
})

export default function ScheduleOnCallNotificationsForm(
props: ScheduleOnCallNotificationsFormProps,
): JSX.Element {
const { scheduleID, slackType, setSlackType, ...formProps } = props
const { scheduleID, ...formProps } = props
const classes = useStyles()
const [slackEnabled] = useConfigValue('Slack.Enable')
const slackUGEnabled = useExpFlag('slack-ug')
const { zone } = useScheduleTZ(scheduleID)
const [slackUGChecked, setSlackUGChecked] = useState(
!!formProps.value.slackUserGroup,
)

const handleRuleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (e.target.value === 'on-change') {
props.onChange({ ...formProps.value, time: null, weekdayFilter: NO_DAY })
return
}

props.onChange({
...props.value,
weekdayFilter: EVERY_DAY,
time: DateTime.fromObject({ hour: 9 }, { zone }).toISO(),
})
}

const handleTypeChange = (
e: SelectChangeEvent<NotificationChannelType>,
): void => {
const newType = e.target.value as NotificationChannelType
if (props.value.type !== newType) {
const channelFields = getEmptyChannelFields(newType)
if (
'slackChannelID' in props.value.channelFields &&
'slackChannelID' in channelFields
) {
channelFields.slackChannelID = props.value.channelFields
.slackChannelID as string | null
}
props.onChange({ ...props.value, type: newType, channelFields })
}
}

function renderTypeFields(type: NotificationChannelType): JSX.Element {
switch (type) {
case 'SLACK_UG':
return (
<React.Fragment>
<Grid item container>
<Grid
item
xs={1}
display='flex'
justifyContent='center'
alignItems='center'
>
<InfoIcon color='primary' />
</Grid>
<Grid item xs={11}>
<Typography>
This will edit your user group in Slack to ensure that only
the members in the selected group are also on-call
</Typography>
</Grid>
</Grid>
<Grid item>
<FormField
component={SlackUserGroupSelect}
fullWidth
label='Slack User Group'
name='channelFields.slackUserGroup'
/>
</Grid>
<Grid item>
<FormField
component={SlackChannelSelect}
fullWidth
required
label='Slack Channel (fallback)'
name='channelFields.slackChannelID'
/>
</Grid>
</React.Fragment>
)
case 'SLACK_CHANNEL':
default:
return (
<Grid item>
<FormField
component={SlackChannelSelect}
fullWidth
required
label='Slack Channel'
name='channelFields.slackChannelID'
/>
</Grid>
)
}
}

return (
<FormContainer {...formProps}>
<Grid container spacing={2} direction='column'>
<Grid item xs={12}>
<Select
fullWidth
value={props.value.type}
required
onChange={handleTypeChange}
>
<MenuItem value='SLACK_CHANNEL' disabled={!slackEnabled}>
SLACK CHANNEL
</MenuItem>
{slackUGEnabled && (
<MenuItem value='SLACK_UG' disabled={!slackEnabled}>
SLACK USER GROUP
</MenuItem>
)}
</Select>
</Grid>
<Grid item>
<RadioGroup
name='ruleType'
Expand Down Expand Up @@ -125,66 +226,7 @@ export default function ScheduleOnCallNotificationsForm(
</Grid>
</Grid>
</Grid>
<Grid item>
<FormField
component={SlackChannelSelect}
fullWidth
required
label='Slack Channel'
name='slackChannelID'
mapOnChangeValue={(v) => {
setSlackType('channel')
return v
}}
/>
</Grid>
{slackUGEnabled && (
<Grid item>
<FormControlLabel
sx={{ pb: 2 }}
control={
<Checkbox
checked={slackUGChecked}
onChange={() => {
const newVal = !slackUGChecked
setSlackUGChecked(newVal)
setSlackType(newVal ? 'usergroup' : 'channel')
if (!newVal) {
props.onChange({
...props.value,
slackUserGroup: null,
})
}
}}
/>
}
label={
<Typography sx={{ display: 'flex' }}>
Also set the members of a Slack user group?
<Tooltip
data-cy='fts-tooltip'
disableFocusListener
placement='right'
title='This will edit your user group in Slack to ensure that only the members in the selected group are also on-call'
>
<InfoIcon color='primary' sx={{ pl: 0.5 }} />
</Tooltip>
</Typography>
}
/>
<FormField
component={SlackUserGroupSelect}
disabled={!slackUGChecked}
fullWidth
label='Slack User Group'
name='slackUserGroup'
mapOnChangeValue={(v) => {
setSlackType('usergroup')
return v
}}
/>
</Grid>
)}
{renderTypeFields(formProps.value.type)}
</Grid>
</FormContainer>
)
Expand Down
8 changes: 5 additions & 3 deletions web/src/app/schedules/on-call-notifications/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
EVERY_DAY,
formatLocalClockTime,
onCallRuleToInput,
channelTypeFromTarget,
channelFieldsFromTarget,
} from './util'

const schedTZQuery = gql`
Expand Down Expand Up @@ -101,7 +103,7 @@ export function useSetOnCallRulesSubmit(
rules: rules
.map((r) => {
if (r === null) return null
if ('slackChannelID' in r || 'slackUserGroup' in r) {
if ('channelFields' in r) {
return onCallValueToRuleInput(zone, r)
}

Expand Down Expand Up @@ -144,8 +146,8 @@ export function useEditOnCallRule(
? DateTime.fromFormat(rule.time, 'HH:mm', { zone }).toISO()
: null,
weekdayFilter: rule?.time ? rule.weekdayFilter || EVERY_DAY : NO_DAY,
slackChannelID: rule?.target.id || null,
slackUserGroup: rule?.target.id || null,
type: channelTypeFromTarget(rule?.target),
channelFields: channelFieldsFromTarget(rule?.target),
}
const { m, submit } = useSetOnCallRulesSubmit(
scheduleID,
Expand Down
Loading