-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(aci): Add uptime detector form #94617
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
Merged
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
14 changes: 0 additions & 14 deletions
14
static/app/views/detectors/components/forms/uptime/detect.tsx
This file was deleted.
Oops, something went wrong.
153 changes: 153 additions & 0 deletions
153
static/app/views/detectors/components/forms/uptime/detect/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import styled from '@emotion/styled'; | ||
|
|
||
| import {Alert} from 'sentry/components/core/alert'; | ||
| import {FieldWrapper} from 'sentry/components/forms/fieldGroup/fieldWrapper'; | ||
| import BooleanField from 'sentry/components/forms/fields/booleanField'; | ||
| import RangeField from 'sentry/components/forms/fields/rangeField'; | ||
| import SelectField from 'sentry/components/forms/fields/selectField'; | ||
| import TextareaField from 'sentry/components/forms/fields/textareaField'; | ||
| import TextField from 'sentry/components/forms/fields/textField'; | ||
| import type FormModel from 'sentry/components/forms/model'; | ||
| import ExternalLink from 'sentry/components/links/externalLink'; | ||
| import {Container} from 'sentry/components/workflowEngine/ui/container'; | ||
| import Section from 'sentry/components/workflowEngine/ui/section'; | ||
| import {t, tct} from 'sentry/locale'; | ||
| import getDuration from 'sentry/utils/duration/getDuration'; | ||
| import {UptimeHeadersField} from 'sentry/views/detectors/components/forms/uptime/detect/uptimeHeadersField'; | ||
|
|
||
| const HTTP_METHOD_OPTIONS = ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']; | ||
| const HTTP_METHODS_NO_BODY = ['GET', 'HEAD', 'OPTIONS']; | ||
| const MINUTE = 60; | ||
| const VALID_INTERVALS_SEC = [ | ||
| MINUTE * 1, | ||
| MINUTE * 5, | ||
| MINUTE * 10, | ||
| MINUTE * 20, | ||
| MINUTE * 30, | ||
| MINUTE * 60, | ||
| ]; | ||
|
|
||
| function methodHasBody(model: FormModel) { | ||
| return !HTTP_METHODS_NO_BODY.includes(model.getValue('method')); | ||
| } | ||
|
|
||
| export function UptimeDetectorFormDetectSection() { | ||
| return ( | ||
| <Container> | ||
| <Section title={t('Detect')}> | ||
| <DetectFieldsContainer> | ||
| <div> | ||
| <SelectField | ||
| options={VALID_INTERVALS_SEC.map(value => ({ | ||
| value, | ||
| label: t('Every %s', getDuration(value)), | ||
| }))} | ||
| name="intervalSeconds" | ||
| label={t('Interval')} | ||
| defaultValue={60} | ||
| flexibleControlStateSize | ||
| showHelpInTooltip={{isHoverable: true}} | ||
| help={({model}) => | ||
| tct( | ||
| 'The amount of time between each uptime check request. Selecting a period of [interval] means it will take at least [expectedFailureInterval] until you are notified of a failure. [link:Learn more].', | ||
| { | ||
| link: ( | ||
| <ExternalLink href="https://docs.sentry.io/product/alerts/uptime-monitoring/#uptime-check-failures" /> | ||
| ), | ||
| interval: ( | ||
| <strong>{getDuration(model.getValue('intervalSeconds'))}</strong> | ||
| ), | ||
| expectedFailureInterval: ( | ||
| <strong> | ||
| {getDuration(Number(model.getValue('intervalSeconds')) * 3)} | ||
| </strong> | ||
| ), | ||
| } | ||
| ) | ||
| } | ||
| required | ||
| /> | ||
| <RangeField | ||
| name="timeoutMs" | ||
| label={t('Timeout')} | ||
| min={1000} | ||
| max={60_000} | ||
| step={250} | ||
| tickValues={[1_000, 10_000, 20_000, 30_000, 40_000, 50_000, 60_000]} | ||
| defaultValue={5_000} | ||
| showTickLabels | ||
| formatLabel={value => getDuration((value || 0) / 1000, 2, true)} | ||
| flexibleControlStateSize | ||
| required | ||
| /> | ||
| <TextField | ||
| name="url" | ||
| label={t('URL')} | ||
| placeholder={t('The URL to monitor')} | ||
| flexibleControlStateSize | ||
| monospace | ||
| required | ||
| /> | ||
| <SelectField | ||
| name="method" | ||
| label={t('Method')} | ||
| defaultValue="GET" | ||
| options={HTTP_METHOD_OPTIONS.map(option => ({ | ||
| value: option, | ||
| label: option, | ||
| }))} | ||
| flexibleControlStateSize | ||
| required | ||
| /> | ||
| <UptimeHeadersField | ||
| name="headers" | ||
| label={t('Headers')} | ||
| flexibleControlStateSize | ||
| /> | ||
| <TextareaField | ||
| name="body" | ||
| label={t('Body')} | ||
| visible={({model}: any) => methodHasBody(model)} | ||
| rows={4} | ||
| maxRows={15} | ||
| autosize | ||
| monospace | ||
| placeholder='{"key": "value"}' | ||
| flexibleControlStateSize | ||
| /> | ||
| <BooleanField | ||
| name="traceSampling" | ||
| label={t('Allow Sampling')} | ||
| showHelpInTooltip={{isHoverable: true}} | ||
| help={tct( | ||
| 'Defer the sampling decision to a Sentry SDK configured in your application. Disable to prevent all span sampling. [link:Learn more].', | ||
| { | ||
| link: ( | ||
| <ExternalLink href="https://docs.sentry.io/product/alerts/uptime-monitoring/uptime-tracing/" /> | ||
| ), | ||
| } | ||
| )} | ||
| flexibleControlStateSize | ||
| /> | ||
| </div> | ||
| <Alert type="muted" showIcon> | ||
| {tct( | ||
| 'By enabling uptime monitoring, you acknowledge that uptime check data may be stored outside your selected data region. [link:Learn more].', | ||
| { | ||
| link: ( | ||
| <ExternalLink href="https://docs.sentry.io/organization/data-storage-location/#data-stored-in-us" /> | ||
| ), | ||
| } | ||
| )} | ||
| </Alert> | ||
| </DetectFieldsContainer> | ||
| </Section> | ||
| </Container> | ||
| ); | ||
| } | ||
|
|
||
| const DetectFieldsContainer = styled('div')` | ||
| ${FieldWrapper} { | ||
| padding-left: 0; | ||
| } | ||
| `; |
171 changes: 171 additions & 0 deletions
171
static/app/views/detectors/components/forms/uptime/detect/uptimeHeadersField.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| import {useEffect, useState} from 'react'; | ||
| import styled from '@emotion/styled'; | ||
|
|
||
| import {Button} from 'sentry/components/core/button'; | ||
| import {Input} from 'sentry/components/core/input'; | ||
| import type {FormFieldProps} from 'sentry/components/forms/formField'; | ||
| import FormField from 'sentry/components/forms/formField'; | ||
| import FormFieldControlState from 'sentry/components/forms/formField/controlState'; | ||
| import {IconAdd, IconDelete} from 'sentry/icons'; | ||
| import {t} from 'sentry/locale'; | ||
| import {space} from 'sentry/styles/space'; | ||
| import {uniqueId} from 'sentry/utils/guid'; | ||
|
|
||
| /** | ||
| * Matches characters that are not valid in a header name. | ||
| */ | ||
| const INVALID_NAME_HEADER_REGEX = new RegExp(/[^a-zA-Z0-9_-]+/g); | ||
|
|
||
| type HeaderEntry = [id: string, name: string, value: string]; | ||
|
|
||
| // XXX(epurkhiser): The types of the FormField render props are absolutely | ||
| // abysmal, so we're leaving this untyped for now. | ||
|
|
||
| function UptimHeadersControl(props: any) { | ||
| const {onChange, onBlur, disabled, model, name, value} = props; | ||
|
|
||
| // Store itmes in local state so we can add empty values without persisting | ||
| // those into the form model. | ||
| const [items, setItems] = useState<HeaderEntry[]>( | ||
| Object.keys(value).length > 0 | ||
| ? value.map((v: any) => [uniqueId(), ...v] as HeaderEntry) | ||
| : [[uniqueId(), '', '']] | ||
| ); | ||
|
|
||
| // Persist the field value back to the form model on changes to the items | ||
| // list. Empty items are discarded and not persisted. | ||
| useEffect(() => { | ||
| const newValue = items.filter(item => item[1] !== '').map(item => [item[1], item[2]]); | ||
|
|
||
| onChange(newValue, {}); | ||
| onBlur(newValue, {}); | ||
| }, [items, onChange, onBlur]); | ||
|
|
||
| function addItem() { | ||
| setItems(currentItems => [...currentItems, [uniqueId(), '', '']]); | ||
| } | ||
|
|
||
| function removeItem(index: number) { | ||
| setItems(currentItems => currentItems.toSpliced(index, 1)); | ||
| } | ||
|
|
||
| function handleNameChange(index: number, newName: string) { | ||
| setItems(currentItems => | ||
| currentItems.toSpliced(index, 1, [ | ||
| items[index]![0], | ||
| newName.replaceAll(INVALID_NAME_HEADER_REGEX, ''), | ||
| items[index]![2], | ||
| ]) | ||
| ); | ||
| } | ||
|
|
||
| function handleValueChange(index: number, newHeaderValue: string) { | ||
| setItems(currentItems => | ||
| currentItems.toSpliced(index, 1, [ | ||
| items[index]![0], | ||
| items[index]![1], | ||
| newHeaderValue, | ||
| ]) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Disambiguates headers that are named the same by adding a `(x)` number to | ||
| * the end of the name in the order they were added. | ||
| */ | ||
| function disambiguateHeaderName(index: number) { | ||
| const headerName = items[index]![1]; | ||
| const matchingIndexes = items | ||
| .map((item, idx) => [idx, item[1]]) | ||
| .filter(([_, itemName]) => itemName === headerName) | ||
| .map(([idx]) => idx); | ||
|
|
||
| const duplicateIndex = matchingIndexes.indexOf(index) + 1; | ||
|
|
||
| return duplicateIndex === 1 ? headerName : `${headerName} (${duplicateIndex})`; | ||
| } | ||
|
|
||
| return ( | ||
| <HeadersContainer> | ||
| {items.length > 0 && ( | ||
| <HeaderItems> | ||
| {items.map(([id, headerName, headerValue], index) => ( | ||
| <HeaderRow key={id}> | ||
| <Input | ||
| monospace | ||
| disabled={disabled} | ||
| value={headerName ?? ''} | ||
| placeholder="X-Header-Value" | ||
| onChange={e => handleNameChange(index, e.target.value)} | ||
| aria-label={t('Name of header %s', index + 1)} | ||
| /> | ||
| <Input | ||
| monospace | ||
| disabled={disabled} | ||
| value={headerValue ?? ''} | ||
| placeholder={t('Header Value')} | ||
| onChange={e => handleValueChange(index, e.target.value)} | ||
| aria-label={ | ||
| headerName | ||
| ? t('Value of %s', disambiguateHeaderName(index)) | ||
| : t('Value of header %s', index + 1) | ||
| } | ||
| /> | ||
| <Button | ||
| disabled={disabled} | ||
| icon={<IconDelete />} | ||
| size="sm" | ||
| borderless | ||
| aria-label={ | ||
| headerName | ||
| ? t('Remove %s', disambiguateHeaderName(index)) | ||
| : t('Remove header %s', index + 1) | ||
| } | ||
| onClick={() => removeItem(index)} | ||
| /> | ||
| </HeaderRow> | ||
| ))} | ||
| </HeaderItems> | ||
| )} | ||
| <HeaderActions> | ||
| <Button disabled={disabled} icon={<IconAdd />} size="sm" onClick={addItem}> | ||
| {t('Add Header')} | ||
| </Button> | ||
| <FormFieldControlState model={model} name={name} /> | ||
| </HeaderActions> | ||
| </HeadersContainer> | ||
| ); | ||
| } | ||
|
|
||
| export function UptimeHeadersField(props: Omit<FormFieldProps, 'children'>) { | ||
| return ( | ||
| <FormField defaultValue={[]} {...props} hideControlState flexibleControlStateSize> | ||
| {({ref: _ref, ...fieldProps}) => <UptimHeadersControl {...fieldProps} />} | ||
| </FormField> | ||
| ); | ||
| } | ||
|
|
||
| const HeadersContainer = styled('div')` | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: ${space(1)}; | ||
| `; | ||
|
|
||
| const HeaderActions = styled('div')` | ||
| display: flex; | ||
| gap: ${space(1.5)}; | ||
| `; | ||
|
|
||
| const HeaderItems = styled('fieldset')` | ||
| display: grid; | ||
| grid-template-columns: minmax(200px, 1fr) 2fr max-content; | ||
| gap: ${space(1)}; | ||
| width: 100%; | ||
| `; | ||
|
|
||
| const HeaderRow = styled('div')` | ||
| display: grid; | ||
| grid-template-columns: subgrid; | ||
| grid-column: 1 / -1; | ||
| align-items: center; | ||
| `; | ||
42 changes: 42 additions & 0 deletions
42
static/app/views/detectors/components/forms/uptime/fields.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import {useFormField} from 'sentry/components/workflowEngine/form/useFormField'; | ||
|
|
||
| interface UptimeDetectorFormData { | ||
| environment: string; | ||
| intervalSeconds: number; | ||
| method: string; | ||
| name: string; | ||
| owner: string; | ||
| projectId: string; | ||
| timeoutMs: number; | ||
| traceSampling: boolean; | ||
| url: string; | ||
| } | ||
|
|
||
| type UptimeDetectorFormFieldName = keyof UptimeDetectorFormData; | ||
|
|
||
| /** | ||
| * Small helper to automatically get the type of the form field. | ||
| */ | ||
| export function useUptimeDetectorFormField<T extends UptimeDetectorFormFieldName>( | ||
| name: T | ||
| ): UptimeDetectorFormData[T] { | ||
| const value = useFormField(name); | ||
| return value; | ||
| } | ||
|
|
||
| /** | ||
| * Enables type-safe form field names. | ||
| * Helps you find areas setting specific fields. | ||
| */ | ||
| export const UPTIME_DETECTOR_FORM_FIELDS = { | ||
| // Core detector fields | ||
| name: 'name', | ||
| environment: 'environment', | ||
| projectId: 'projectId', | ||
| owner: 'owner', | ||
| intervalSeconds: 'intervalSeconds', | ||
| timeoutMs: 'timeoutMs', | ||
| url: 'url', | ||
| method: 'method', | ||
| traceSampling: 'traceSampling', | ||
| } satisfies Record<UptimeDetectorFormFieldName, UptimeDetectorFormFieldName>; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.