diff --git a/package.json b/package.json index d7dad1900..389ca6051 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "react-popper": "^2.2.5", "react-router-dom": "^5.2.0", "rxjs": "7.8.1", + "usehooks-ts": "^3.1.0", "valid-url": "^1.0.9", "yaml": "^2.2.2", "zod": "3.23.6" diff --git a/src/components/CheckEditor/CheckProbes.tsx b/src/components/CheckEditor/CheckProbes.tsx deleted file mode 100644 index 0b39984bd..000000000 --- a/src/components/CheckEditor/CheckProbes.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { forwardRef, useCallback, useState } from 'react'; -import { AppEvents, SelectableValue } from '@grafana/data'; -import { Button, Field, MultiSelect, Stack, ThemeContext } from '@grafana/ui'; -import appEvents from 'grafana/app/core/app_events'; -import { css } from '@emotion/css'; - -import { Probe } from 'types'; - -interface CheckProbesProps { - probes: number[]; - availableProbes: Probe[]; - disabled?: boolean; - onChange: (probes: number[]) => void; - onBlur?: () => void; - invalid?: boolean; - error?: string; -} - -export const PROBES_SELECT_ID = 'check-probes-select'; - -export const CheckProbes = forwardRef( - ({ probes, availableProbes, disabled, onChange, onBlur, invalid, error }: CheckProbesProps, ref) => { - const [currentProbes, setCurrentProbes] = useState(probes); - - const onChangeSelect = useCallback( - (items: Array>) => { - // On adding a new probe, check deprecation status - if (currentProbes.length < items.length) { - const newItem = items.find((item) => !currentProbes.includes(item.value as number)); - // Prevent adding to list if probe is deprecated - if (newItem && newItem.deprecated) { - appEvents.emit(AppEvents.alertWarning, [`Deprecated probes cannot be added to checks`]); - return; - } - } - const probes = items.map((p) => p.value && p.value) as number[]; - setCurrentProbes(probes); - onChange(probes); - }, - [onChange, currentProbes] - ); - - const onClearLocations = () => { - setCurrentProbes([]); - onChange([]); - }; - - const onAllLocations = () => { - const probes = availableProbes.filter((p) => !p.deprecated).map((p) => p.id) as number[]; - setCurrentProbes(probes); - onChange(probes); - }; - - const options = availableProbes.map((p) => { - return { - label: p.deprecated ? `${p.name} (deprecated)` : p.name, - value: p.id, - description: p.online ? 'Online' : 'Offline', - deprecated: p.deprecated, - }; - }); - - const selectedProbes = options.filter((p) => currentProbes.includes(p.value as number)); - const id = 'check-probes'; - - return ( - - {(theme) => ( - <> - - - -
- - - - -
- - )} -
- ); - } -); - -CheckProbes.displayName = 'CheckProbes'; diff --git a/src/components/CheckEditor/CheckProbes/CheckProbes.tsx b/src/components/CheckEditor/CheckProbes/CheckProbes.tsx new file mode 100644 index 000000000..b4072639d --- /dev/null +++ b/src/components/CheckEditor/CheckProbes/CheckProbes.tsx @@ -0,0 +1,78 @@ +import React, { forwardRef, useMemo, useState } from 'react'; +import { Field, Stack } from '@grafana/ui'; + +import { Probe } from 'types'; + +import { PrivateProbesAlert } from './PrivateProbesAlert'; +import { PROBES_FILTER_ID, ProbesFilter } from './ProbesFilter'; +import { ProbesList } from './ProbesList'; + +interface CheckProbesProps { + probes: number[]; + availableProbes: Probe[]; + disabled?: boolean; + onChange: (probes: number[]) => void; + onBlur?: () => void; + invalid?: boolean; + error?: string; +} +export const CheckProbes = forwardRef(({ probes, availableProbes, onChange, error }: CheckProbesProps) => { + const [filteredProbes, setFilteredProbes] = useState(availableProbes); + + const publicProbes = useMemo(() => filteredProbes.filter((probe) => probe.public), [filteredProbes]); + const privateProbes = useMemo(() => filteredProbes.filter((probe) => !probe.public), [filteredProbes]); + + const groupedByRegion = useMemo( + () => + publicProbes.reduce((acc: Record, curr: Probe) => { + const region = curr.region; + if (!acc[region]) { + acc[region] = []; + } + acc[region].push(curr); + return acc; + }, {}), + [publicProbes] + ); + + const showPrivateProbesDiscovery = privateProbes.length === 0 && filteredProbes.length === availableProbes.length; + + return ( +
+ +
+ + + {privateProbes.length > 0 && ( + + )} + + {Object.entries(groupedByRegion).map(([region, allProbes]) => ( + + ))} + +
+
+ {showPrivateProbesDiscovery && } +
+ ); +}); + +CheckProbes.displayName = 'CheckProbes'; diff --git a/src/components/CheckEditor/CheckProbes/PrivateProbesAlert.tsx b/src/components/CheckEditor/CheckProbes/PrivateProbesAlert.tsx new file mode 100644 index 000000000..e4d50f85e --- /dev/null +++ b/src/components/CheckEditor/CheckProbes/PrivateProbesAlert.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Alert, LinkButton, Stack, TextLink } from '@grafana/ui'; +import { useLocalStorage } from 'usehooks-ts'; + +import { ROUTES } from 'types'; +import { getRoute } from 'components/Routing.utils'; + +export const PrivateProbesAlert = () => { + const [dismissed, setDismissed] = useLocalStorage('dismissedPrivateProbesAlert', false); + + if (dismissed) { + return null; + } + + return ( + { + setDismissed(true); + }} + > + +

+ Private probes are instances of the open source Grafana{' '} + + Synthetic Monitoring Agent + {' '} + and are only accessible to you. +

+ + Set up a Private Probe + +
+
+ ); +}; diff --git a/src/components/CheckEditor/CheckProbes/ProbesFilter.tsx b/src/components/CheckEditor/CheckProbes/ProbesFilter.tsx new file mode 100644 index 000000000..ea9b0ebb1 --- /dev/null +++ b/src/components/CheckEditor/CheckProbes/ProbesFilter.tsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; + +import { Probe } from 'types'; +import { SearchFilter } from 'components/SearchFilter'; + +export const PROBES_FILTER_ID = 'check-probes-filter'; + +export const ProbesFilter = ({ probes, onSearch }: { probes: Probe[]; onSearch: (probes: Probe[]) => void }) => { + const [showEmptyState, setShowEmptyState] = useState(false); + const [filterText, setFilterText] = useState(''); + + const handleSearch = (searchValue: string) => { + setFilterText(searchValue); + const filteredProbes = probes.filter( + (probe) => + probe.region.toLowerCase().includes(searchValue) || + probe.name.toLowerCase().includes(searchValue) || + probe.longRegion?.toLowerCase().includes(searchValue) || + probe.city?.toLowerCase().includes(searchValue) || + probe.provider?.toLowerCase().includes(searchValue) || + probe.country?.toLowerCase().includes(searchValue) || + probe.countryCode?.toLowerCase().includes(searchValue) + ); + + onSearch(filteredProbes); + setShowEmptyState(filteredProbes.length === 0); + }; + + return ( + <> + + + ); +}; diff --git a/src/components/CheckEditor/CheckProbes/ProbesList.tsx b/src/components/CheckEditor/CheckProbes/ProbesList.tsx new file mode 100644 index 000000000..abf2abeb7 --- /dev/null +++ b/src/components/CheckEditor/CheckProbes/ProbesList.tsx @@ -0,0 +1,152 @@ +import React, { useMemo } from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Checkbox, Label, Stack, Text, useStyles2 } from '@grafana/ui'; +import { css } from '@emotion/css'; + +import { Probe } from 'types'; +import { ProbeStatus } from 'components/ProbeCard/ProbeStatus'; + +export const ProbesList = ({ + title, + probes, + selectedProbes, + onSelectionChange, +}: { + title: string; + probes: Probe[]; + selectedProbes: number[]; + onSelectionChange: (probes: number[]) => void; +}) => { + const styles = useStyles2(getStyles); + + const handleToggleAll = () => { + if (allProbesSelected) { + onSelectionChange(selectedProbes.filter((id) => !probes.some((probe) => probe.id === id))); + return; + } + const selected = new Set([ + ...selectedProbes, + ...probes.filter((probe) => !probe.deprecated).map((probe) => probe.id!), + ]); + onSelectionChange([...selected]); + }; + + const handleToggleProbe = (probe: Probe) => { + if (!probe.id || probe.deprecated) { + return; + } + if (selectedProbes.includes(probe.id)) { + onSelectionChange(selectedProbes.filter((p) => p !== probe.id)); + return; + } + onSelectionChange([...selectedProbes, probe.id]); + }; + + const probeIds = useMemo(() => probes.map((probe) => probe.id!), [probes]); + const regionSelectedProbes = useMemo( + () => selectedProbes.filter((probe) => probeIds.includes(probe)), + [selectedProbes, probeIds] + ); + + const allProbesSelected = useMemo( + () => probes.every((probe) => selectedProbes.includes(probe.id!)), + [probes, selectedProbes] + ); + + const someProbesSelected = useMemo( + () => probes.some((probe) => selectedProbes.includes(probe.id!)) && !allProbesSelected, + [probes, selectedProbes, allProbesSelected] + ); + + return ( +
+
+ + +
+
+ {probes.map((probe: Probe) => ( +
+ handleToggleProbe(probe)} + checked={selectedProbes.includes(probe.id!)} + /> + +
+ ))} +
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + item: css({ + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + display: `flex`, + gap: theme.spacing(1), + marginLeft: theme.spacing(1), + alignItems: 'center', + }), + + probesColumn: css({ + fontSize: theme.typography.h6.fontSize, + fontWeight: theme.typography.fontWeightLight, + }), + + probeRegionDescription: css({ + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + paddingTop: '3px', + }), + + probesList: css({ + display: 'flex', + flexDirection: 'column', + minWidth: '250px', + maxWidth: '350px', + maxHeight: '230px', + overflowY: 'auto', + }), + + sectionHeader: css({ + display: 'flex', + border: `1px solid ${theme.colors.border.weak}`, + backgroundColor: `${theme.colors.background.secondary}`, + padding: theme.spacing(1), + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + gap: theme.spacing(1), + verticalAlign: 'middle', + alignItems: 'center', + }), + + headerLabel: css({ + fontWeight: theme.typography.fontWeightLight, + fontSize: theme.typography.h5.fontSize, + color: 'white', + }), + + columnLabel: css({ + fontWeight: theme.typography.fontWeightLight, + fontSize: theme.typography.h6.fontSize, + lineHeight: theme.typography.body.lineHeight, + marginBottom: '0', + }), +}); diff --git a/src/components/CheckEditor/ProbeOptions.tsx b/src/components/CheckEditor/ProbeOptions.tsx index 0223b808a..fe6c73ec4 100644 --- a/src/components/CheckEditor/ProbeOptions.tsx +++ b/src/components/CheckEditor/ProbeOptions.tsx @@ -3,10 +3,10 @@ import { Controller, useFormContext } from 'react-hook-form'; import { Field } from '@grafana/ui'; import { CheckFormValues, CheckType, Probe } from 'types'; -import { useProbes } from 'data/useProbes'; +import { useProbesWithMetadata } from 'data/useProbes'; import { SliderInput } from 'components/SliderInput'; -import { CheckProbes } from './CheckProbes'; +import { CheckProbes } from './CheckProbes/CheckProbes'; interface ProbeOptionsProps { checkType: CheckType; @@ -14,7 +14,7 @@ interface ProbeOptionsProps { } export const ProbeOptions = ({ checkType, disabled }: ProbeOptionsProps) => { - const { data: probes = [] } = useProbes(); + const { data: probes = [] } = useProbesWithMetadata(); const { control, formState: { errors }, diff --git a/src/components/CheckEditor/ProbesMetadata.ts b/src/components/CheckEditor/ProbesMetadata.ts new file mode 100644 index 000000000..2e711a4c7 --- /dev/null +++ b/src/components/CheckEditor/ProbesMetadata.ts @@ -0,0 +1,200 @@ +import { ProbeProvider } from 'types'; + +const REGION_APAC = { code: 'APAC', long: 'Asia-Pacific' }; +const REGION_AMER = { code: 'AMER', long: 'The Americas' }; +const REGION_EMEA = { code: 'EMEA', long: 'Europe, M. East & Africa' }; + +const COUNTRY_IN = { code: 'IN', long: 'India' }; +const COUNTRY_KR = { code: 'KR', long: 'South Korea' }; +const COUNTRY_SG = { code: 'SG', long: 'Singapore' }; +const COUNTRY_AU = { code: 'AU', long: 'Australia' }; +const COUNTRY_JP = { code: 'JP', long: 'Japan' }; +const COUNTRY_US = { code: 'US', long: 'United States' }; +const COUNTRY_CA = { code: 'CA', long: 'Canada' }; +const COUNTRY_BR = { code: 'BR', long: 'Brazil' }; +const COUNTRY_NL = { code: 'NL', long: 'Netherlands' }; +const COUNTRY_ZA = { code: 'ZA', long: 'South Africa' }; +const COUNTRY_DE = { code: 'DE', long: 'Germany' }; +const COUNTRY_UK = { code: 'UK', long: 'United Kingdom' }; +const COUNTRY_FR = { code: 'FR', long: 'France' }; + +export const PROBES_METADATA = [ + { + name: 'Bangalore', + region: REGION_APAC.code, + longRegion: REGION_APAC.long, + provider: ProbeProvider.DIGITAL_OCEAN, + countryCode: COUNTRY_IN.code, + country: COUNTRY_IN.long, + }, + { + name: 'Mumbai', + region: REGION_APAC.code, + longRegion: REGION_APAC.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_IN.code, + country: COUNTRY_IN.long, + }, + { + name: 'Seoul', + region: REGION_APAC.code, + longRegion: REGION_APAC.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_KR.code, + country: COUNTRY_KR.long, + }, + { + name: 'Singapore', + region: REGION_APAC.code, + longRegion: REGION_APAC.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_SG.code, + country: COUNTRY_SG.long, + }, + { + name: 'Sydney', + region: REGION_APAC.code, + longRegion: REGION_APAC.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_AU.code, + country: COUNTRY_AU.long, + }, + { + name: 'Tokyo', + region: REGION_APAC.code, + longRegion: REGION_APAC.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_JP.code, + country: COUNTRY_JP.long, + }, + + { + name: 'Atlanta', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.LINODE, + countryCode: COUNTRY_US.code, + country: COUNTRY_US.long, + }, + { + name: 'Dallas', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.LINODE, + countryCode: COUNTRY_US.code, + country: COUNTRY_US.long, + }, + { + name: 'Newark', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.LINODE, + countryCode: COUNTRY_US.code, + country: COUNTRY_US.long, + }, + { + name: 'Toronto', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.LINODE, + countryCode: COUNTRY_CA.code, + country: COUNTRY_CA.long, + }, + { + name: 'NewYork', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.DIGITAL_OCEAN, + countryCode: COUNTRY_US.code, + country: COUNTRY_US.long, + }, + { + name: 'SanFrancisco', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.DIGITAL_OCEAN, + countryCode: COUNTRY_US.code, + country: COUNTRY_US.long, + }, + { + name: 'NorthCalifornia', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_US.code, + country: COUNTRY_US.long, + }, + { + name: 'NorthVirginia', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_US.code, + country: COUNTRY_US.long, + }, + { + name: 'Ohio', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_US.code, + country: COUNTRY_US.long, + }, + { + name: 'Oregon', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_US.code, + country: COUNTRY_US.long, + }, + { + name: 'SaoPaulo', + region: REGION_AMER.code, + longRegion: REGION_AMER.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_BR.code, + country: COUNTRY_BR.long, + }, + + { + name: 'Amsterdam', + region: REGION_EMEA.code, + longRegion: REGION_EMEA.long, + provider: ProbeProvider.DIGITAL_OCEAN, + countryCode: COUNTRY_NL.code, + country: COUNTRY_NL.long, + }, + { + name: 'CapeTown', + region: REGION_EMEA.code, + longRegion: REGION_EMEA.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_ZA.code, + country: COUNTRY_ZA.long, + }, + { + name: 'Frankfurt', + region: REGION_EMEA.code, + longRegion: REGION_EMEA.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_DE.code, + country: COUNTRY_DE.long, + }, + { + name: 'London', + region: REGION_EMEA.code, + longRegion: REGION_EMEA.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_UK.code, + country: COUNTRY_UK.long, + }, + { + name: 'Paris', + region: REGION_EMEA.code, + longRegion: REGION_EMEA.long, + provider: ProbeProvider.AWS, + countryCode: COUNTRY_FR.code, + country: COUNTRY_FR.long, + }, +]; diff --git a/src/components/CheckForm/checkForm.utils.ts b/src/components/CheckForm/checkForm.utils.ts index c055846c3..c517620c6 100644 --- a/src/components/CheckForm/checkForm.utils.ts +++ b/src/components/CheckForm/checkForm.utils.ts @@ -1,7 +1,7 @@ import { FieldErrors } from 'react-hook-form'; import { CheckFormValues } from 'types'; -import { PROBES_SELECT_ID } from 'components/CheckEditor/CheckProbes'; +import { PROBES_FILTER_ID } from 'components/CheckEditor/CheckProbes/ProbesFilter'; import { SCRIPT_TEXTAREA_ID } from 'components/CheckEditor/FormComponents/ScriptedCheckScript'; import { CHECK_FORM_ERROR_EVENT } from 'components/constants'; @@ -57,7 +57,7 @@ function getFirstInput(errs: FieldErrors) { } function searchForSpecialInputs(errKeys: string[] = []) { - const probes = errKeys.includes(`probes`) && document.querySelector(`#${PROBES_SELECT_ID} input`); + const probes = errKeys.includes(`probes`) && document.querySelector(`#${PROBES_FILTER_ID}`); const script = errKeys.includes(`settings.scripted.script`) && document.querySelector(`#${SCRIPT_TEXTAREA_ID} textarea`); diff --git a/src/components/SearchFilter.tsx b/src/components/SearchFilter.tsx new file mode 100644 index 000000000..7bef2564f --- /dev/null +++ b/src/components/SearchFilter.tsx @@ -0,0 +1,77 @@ +import React, { useCallback, useRef } from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Button, EmptySearchResult, Icon, Input, useStyles2 } from '@grafana/ui'; +import { css } from '@emotion/css'; + +export interface SearchFilterProps { + onSearch: (value: string) => void; + showEmptyState: boolean; + emptyText?: string; + placeholder?: string; + id: string; + value: string; +} + +export const SearchFilter = ({ + onSearch, + id, + value, + showEmptyState, + emptyText = '', + placeholder = '', +}: SearchFilterProps) => { + const styles = useStyles2(getStyles); + + const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + } + }, []); + + const onClearFilterClick = () => { + onSearch(''); + inputRef.current?.focus(); + }; + + const inputRef = useRef(null); + + return ( + <> + {' '} +
+ } + suffix={ + value.length && ( + + ) + } + value={value} + placeholder={placeholder} + onChange={(event: React.ChangeEvent) => onSearch(event.target.value.toLowerCase())} + id={id} + onKeyDown={handleKeyDown} + /> +
+ {showEmptyState && ( +
+ {emptyText} +
+ )} + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + searchInput: css({ + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + }), + + emptyState: css({ + marginTop: theme.spacing(2), + }), +}); diff --git a/src/data/useProbes.ts b/src/data/useProbes.ts index 6a21330a7..f970811af 100644 --- a/src/data/useProbes.ts +++ b/src/data/useProbes.ts @@ -10,11 +10,13 @@ import type { AddProbeResult, DeleteProbeError, DeleteProbeResult, + ListProbeResult, ResetProbeTokenResult, UpdateProbeResult, } from 'datasource/responses.types'; import { queryClient } from 'data/queryClient'; import { useSMDS } from 'hooks/useSMDS'; +import { PROBES_METADATA } from 'components/CheckEditor/ProbesMetadata'; import { useChecks } from './useChecks'; @@ -35,6 +37,25 @@ export function useProbes() { return useQuery(probesQuery(smDS)); } +export function useProbesWithMetadata() { + const { data: probes = [], isLoading } = useProbes(); + + const probesWithMetadata = useMemo(() => { + if (isLoading) { + return []; + } + + return probes.map((probe) => { + const metadata = PROBES_METADATA.find( + (info) => info.name === probe.name && info.region === probe.region + ); + return metadata ? { ...probe, ...metadata } : probe; + }); + }, [probes, isLoading]); + + return { data: probesWithMetadata, isLoading }; +} + export function useExtendedProbes(): [ExtendedProbe[], boolean] { const { data: probes = [], isLoading: isLoadingProbes } = useProbes(); const { data: checks = [], isLoading: isLoadingChecks } = useChecks(); diff --git a/src/page/NewCheck/__tests__/NewCheck.test.tsx b/src/page/NewCheck/__tests__/NewCheck.test.tsx index 0f56316dc..6bf267fcc 100644 --- a/src/page/NewCheck/__tests__/NewCheck.test.tsx +++ b/src/page/NewCheck/__tests__/NewCheck.test.tsx @@ -99,14 +99,14 @@ describe(``, () => { expect(screen.getByRole(`button`, { name: /Submit/ })).toBeDisabled(); }); - it(`should focus the probes select correctly when appropriate`, async () => { + it(`should focus the probes filter component when appropriate`, async () => { const { user } = await renderNewForm(CheckType.HTTP); await fillMandatoryFields({ user, checkType: CheckType.HTTP, fieldsToOmit: ['probes'] }); await submitForm(user); - const probesSelect = await screen.findByLabelText(/Probe locations/); - await waitFor(() => expect(probesSelect).toHaveFocus()); + const probesFilter = await screen.findByLabelText(/Probe locations/); + await waitFor(() => expect(probesFilter).toHaveFocus()); }); it(`should display an error message when the job name contains commas`, async () => { diff --git a/src/page/__testHelpers__/apiEndPoint.ts b/src/page/__testHelpers__/apiEndPoint.ts index 278d4e318..687b3c104 100644 --- a/src/page/__testHelpers__/apiEndPoint.ts +++ b/src/page/__testHelpers__/apiEndPoint.ts @@ -1,7 +1,6 @@ import { screen } from '@testing-library/react'; import { UserEvent } from '@testing-library/user-event'; import { PRIVATE_PROBE } from 'test/fixtures/probes'; -import { selectOption } from 'test/utils'; import { CheckType } from 'types'; @@ -29,6 +28,7 @@ export async function fillMandatoryFields({ user, fieldsToOmit = [], checkType } await goToSection(user, 5); if (!fieldsToOmit.includes('probes')) { - await selectOption(user, { label: 'Probe locations', option: PRIVATE_PROBE.name }); + const probeCheckbox = await screen.findByLabelText(PRIVATE_PROBE.name); + await user.click(probeCheckbox); } } diff --git a/src/page/__testHelpers__/multiStep.ts b/src/page/__testHelpers__/multiStep.ts index 78293b3b6..f2f2a5efd 100644 --- a/src/page/__testHelpers__/multiStep.ts +++ b/src/page/__testHelpers__/multiStep.ts @@ -1,7 +1,6 @@ import { screen } from '@testing-library/react'; import { UserEvent } from '@testing-library/user-event'; import { PRIVATE_PROBE } from 'test/fixtures/probes'; -import { selectOption } from 'test/utils'; import { CheckType } from 'types'; @@ -29,6 +28,7 @@ export async function fillMandatoryFields({ user, fieldsToOmit = [], checkType } await goToSection(user, 5); if (!fieldsToOmit.includes('probes')) { - await selectOption(user, { label: 'Probe locations', option: PRIVATE_PROBE.name }); + const probeCheckbox = await screen.findByLabelText(PRIVATE_PROBE.name); + await user.click(probeCheckbox); } } diff --git a/src/page/__testHelpers__/scripted.ts b/src/page/__testHelpers__/scripted.ts index 3ed6b846b..ae0203e70 100644 --- a/src/page/__testHelpers__/scripted.ts +++ b/src/page/__testHelpers__/scripted.ts @@ -1,7 +1,6 @@ import { screen } from '@testing-library/react'; import { UserEvent } from '@testing-library/user-event'; import { PRIVATE_PROBE } from 'test/fixtures/probes'; -import { selectOption } from 'test/utils'; import { CheckType } from 'types'; @@ -29,6 +28,7 @@ export async function fillMandatoryFields({ user, fieldsToOmit = [], checkType } await goToSection(user, 5); if (!fieldsToOmit.includes('probes')) { - await selectOption(user, { label: 'Probe locations', option: PRIVATE_PROBE.name }); + const probeCheckbox = await screen.findByLabelText(PRIVATE_PROBE.name); + await user.click(probeCheckbox); } } diff --git a/src/types.ts b/src/types.ts index 782fffe86..eafc31aa4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,6 +62,12 @@ export enum DnsProtocol { UDP = 'UDP', } +export enum ProbeProvider { + AWS = 'AWS', + LINODE = 'Linode', + DIGITAL_OCEAN = 'Digital Ocean', +} + export interface HeaderMatch { header: string; regexp: string; @@ -117,6 +123,12 @@ export interface Probe extends ExistingObject { version: string; deprecated: boolean; capabilities: ProbeCapabilities; + + provider?: ProbeProvider; + city?: string; + country?: string; + countryCode?: string; + longRegion?: string; } // Used to extend the Probe object with additional properties (see Probes.tsx component) diff --git a/yarn.lock b/yarn.lock index c77e2570e..5844ea377 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7755,6 +7755,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -10450,16 +10455,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10542,14 +10538,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11191,6 +11180,13 @@ use-memo-one@^1.1.1: resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== +usehooks-ts@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-3.1.0.tgz#156119f36efc85f1b1952616c02580f140950eca" + integrity sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw== + dependencies: + lodash.debounce "^4.0.8" + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -11537,7 +11533,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11555,15 +11551,6 @@ wrap-ansi@^6.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"