Skip to content

Commit

Permalink
Merge pull request #3088 from target/hb-better-monitor-duration
Browse files Browse the repository at this point in the history
heartbeats: Increase max duration and improve UI for long-durations
  • Loading branch information
mastercactapus authored Jul 19, 2023
2 parents 2e8205a + 40f7b59 commit 42a54fa
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 14 deletions.
2 changes: 1 addition & 1 deletion heartbeat/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (m Monitor) Normalize() (*Monitor, error) {
err := validate.Many(
validate.UUID("ServiceID", m.ServiceID),
validate.IDName("Name", m.Name),
validate.Duration("Timeout", m.Timeout, 5*time.Minute, 9000*time.Minute),
validate.Duration("Timeout", m.Timeout, 5*time.Minute, 9000*time.Hour),
)
if err != nil {
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions test/integration/service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test('Service', async ({ page, isMobile }) => {
await page.getByTestId('create-monitor').click()
}
await page.getByLabel('Name').fill(invalidHMName)
await page.getByLabel('Timeout (minutes)').fill(timeoutMinutes)
await page.getByLabel('Timeout').fill(timeoutMinutes)
await page.getByRole('button', { name: 'Submit' }).click()

// Should see error message
Expand All @@ -88,7 +88,7 @@ test('Service', async ({ page, isMobile }) => {
await page.getByRole('button', { name: 'Other Actions' }).click()
await page.getByRole('menuitem', { name: 'Edit' }).click()
await page.getByLabel('Name').fill(hmName)
await page.getByLabel('Timeout (minutes)').fill(timeoutMinutes)
await page.getByLabel('Timeout').fill(timeoutMinutes)
await page.getByRole('button', { name: 'Submit' }).click()

// Should see the edited heartbeat monitor
Expand Down
22 changes: 12 additions & 10 deletions web/src/app/services/HeartbeatMonitorForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import React from 'react'
import Grid from '@mui/material/Grid'
import TextField from '@mui/material/TextField'
import { FormContainer, FormField } from '../forms'
import NumberField from '../util/NumberField'
import { FieldError } from '../util/errutil'
import { DurationField } from '../util/DurationField'
import { Duration } from 'luxon'

function clampTimeout(val: string): number | string {
if (!val) return ''
const num = parseInt(val, 10)
if (Number.isNaN(num)) return val

// need to have the min be 1 here so you can type `10`
return Math.min(Math.max(1, num), 9000)
const dur = Duration.fromISO(val)
return dur.as('minutes')
}
export interface Value {
name: string
Expand Down Expand Up @@ -47,13 +45,17 @@ export default function HeartbeatMonitorForm(
<Grid item xs={12}>
<FormField
fullWidth
component={NumberField}
component={DurationField}
required
type='number'
label='Timeout (minutes)'
label='Timeout'
name='timeoutMinutes'
min={5}
max={9000}
max={540000}
mapValue={(minutes) =>
Duration.fromObject({
minutes,
}).toISO()
}
mapOnChangeValue={clampTimeout}
/>
</Grid>
Expand Down
2 changes: 1 addition & 1 deletion web/src/app/services/HeartbeatMonitorList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default function HeartbeatMonitorList(props: {
prefix='Timeout: '
duration={{ minutes: monitor.timeoutMinutes }}
precise
units={['hours', 'minutes']}
units={['weeks', 'days', 'hours', 'minutes']}
/>
<br />
<CopyText title='Copy URL' value={monitor.href} asURL />
Expand Down
88 changes: 88 additions & 0 deletions web/src/app/util/DurationField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { useState } from 'react'
import NumberField from './NumberField'
import { Grid, MenuItem, Select } from '@mui/material'
import { Duration, DurationLikeObject } from 'luxon'

export type DurationFieldProps = {
value: string
name: string
label: string
onChange: (newValue: string) => void
}

type Unit = 'minute' | 'hour' | 'day' | 'week'

// getDefaultUnit returns largest unit that can be used for the given ISO duration
// as an integer. For example, if the value is 120, the default unit is hour, but
// if the value is 121, the default unit is minute.
//
// If the value is empty, the default unit is minute.
function getDefaultUnit(value: string): Unit {
if (!value) return 'minute'
const dur = Duration.fromISO(value)
if (dur.as('hours') / 24 / 7 === Math.floor(dur.as('hours') / 24 / 7))
return 'week'
if (dur.as('hours') / 24 === Math.floor(dur.as('hours') / 24)) return 'day'
if (dur.as('hours') === Math.floor(dur.as('hours'))) return 'hour'
return 'minute'
}

const mult = {
minute: 1,
hour: 60,
day: 60 * 24,
week: 60 * 24 * 7,
}

export const DurationField: React.FC<DurationFieldProps> = (props) => {
const [unit, setUnit] = useState(getDefaultUnit(props.value))
const val = Duration.fromISO(props.value).as('minute') / mult[unit]

const handleChange = (val: number, u: Unit = unit): void => {
const dur = Duration.fromObject({
minutes: val * mult[u],
} as DurationLikeObject)
props.onChange(dur.toISO())
}

return (
<Grid container sx={{ width: '100%' }}>
<Grid item xs={8}>
<NumberField
fullWidth
value={val.toString()}
name={props.name}
label={props.label}
onChange={(e) => handleChange(parseInt(e.target.value, 10))}
sx={{
'& fieldset': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
},
}}
/>
</Grid>
<Grid item xs={4}>
<Select
value={unit}
onChange={(e) => {
setUnit(e.target.value as Unit)
handleChange(val, e.target.value as Unit)
}}
sx={{
width: '100%',
'& fieldset': {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
},
}}
>
<MenuItem value='minute'>Minute(s)</MenuItem>
<MenuItem value='hour'>Hour(s)</MenuItem>
<MenuItem value='day'>Day(s) (24h)</MenuItem>
<MenuItem value='week'>Week(s)</MenuItem>
</Select>
</Grid>
</Grid>
)
}

0 comments on commit 42a54fa

Please sign in to comment.