Skip to content
82 changes: 0 additions & 82 deletions components/DonationBanner.js

This file was deleted.

15 changes: 8 additions & 7 deletions components/MATChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,13 @@ const fetcher = (query) => {
}
})
.catch((e) => {
// throw new Error(e?.response?.data?.error ?? e.message)
const error = new Error('An error occurred while fetching the data.')
// Attach extra info to the error object.
error.info = e.response.data.error
error.status = e.response.status
throw error
if (!axios.isAxiosError(e)) throw e

const status = e.response?.status
const message = e.response?.data?.message ?? e.message
const data = e.response?.data

throw new Error(`${status}: ${message}\n${JSON.stringify(data)}`)
})
}

Expand Down Expand Up @@ -160,7 +161,7 @@ const MATChart = ({ query, showFilters = true }) => {

return (
<>
{error && <NoCharts message={error?.info ?? JSON.stringify(error)} />}
{error && <NoCharts message={error?.message ?? JSON.stringify(error)} />}

{isValidating ? (
<ChartSpinLoader height="500px" />
Expand Down
2 changes: 1 addition & 1 deletion components/aggregation/mat/CustomTooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const CustomToolTip = memo(({ data, onClose, title, link = true }) => {
: data.blocked_max_outcome}
</span>
</div>
{!!data?.likely_blocked_protocols.length && (
{!!data?.likely_blocked_protocols?.length && (
<div>
Likely blocked protocols:{' '}
<ul>
Expand Down
6 changes: 4 additions & 2 deletions components/aggregation/mat/NoCharts.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ export const NoCharts = ({ message }) => {
<FormattedMessage id="MAT.Charts.NoData.Title" />
</div>
<div className="p-8 text-center bg-gray-200 font-bold">
<FormattedMessage id="MAT.Charts.NoData.Description" />
<div className="mb-2">
<FormattedMessage id="MAT.Charts.NoData.Description" />
</div>
{message && (
<div className="p-2 m-2">
<div className="p-2 m-2 text-sm">
<FormattedMessage id="MAT.Charts.NoData.Details" />
<pre className="my-2">{message}</pre>
</div>
Expand Down
202 changes: 202 additions & 0 deletions components/alerts/Form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { format } from 'date-fns'
import { Input, Select } from 'ooni-components'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Controller, useForm } from 'react-hook-form'
import { useIntl } from 'react-intl'
import { useRouter } from 'next/router'
import DateRangePicker from 'components/DateRangePicker'
import countries from 'data/countries.json'
import dayjs from 'services/dayjs'
import { getLocalisedRegionName } from 'utils/i18nCountries'

const tomorrowUTC = dayjs.utc().add(1, 'day').format('YYYY-MM-DD')
const monthAgoUTC = dayjs.utc().subtract(1, 'month').format('YYYY-MM-DD')

const asnRegEx = /^(AS)?([1-9][0-9]*)$/

const AlertsForm = () => {
const intl = useIntl()
const [showDatePicker, setShowDatePicker] = useState(false)
const router = useRouter()
const { since, until, probe_cc, probe_asn, domain } = router.query

// Set default since/until in the URL if not present
// biome-ignore lint/correctness/useExhaustiveDependencies: only run once when router is ready
useEffect(() => {
if (router.isReady && (!since || !until)) {
router.replace(
{
pathname: router.pathname,
query: {
...router.query,
since: since || monthAgoUTC,
until: until || tomorrowUTC,
},
},
undefined,
{ shallow: true },
)
}
}, [router.isReady])

const onSubmit = (data) => {
const query = Object.fromEntries(
Object.entries(data).filter(([, v]) => v && v !== 'XX'),
)
router.push({
pathname: router.pathname,
query,
})
}

const {
control,
handleSubmit,
setValue,
getValues,
formState: { errors },
} = useForm({
defaultValues: {
since: since || monthAgoUTC,
until: until || tomorrowUTC,
probe_cc: probe_cc || 'XX',
probe_asn: probe_asn || '',
domain: domain || '',
},
})

const handleRangeSelect = useCallback(
(range) => {
if (range?.from) {
setValue('since', format(range.from, 'y-MM-dd'))
} else {
setValue('since', '')
}
if (range?.to) {
setValue('until', format(range.to, 'y-MM-dd'))
} else {
setValue('until', '')
}
setShowDatePicker(false)
},
[setValue],
)

// biome-ignore lint/correctness/useExhaustiveDependencies: locale change triggers re-sort
const countryOptions = useMemo(() => {
const options = countries.map((c) => ({
...c,
name: getLocalisedRegionName(c.alpha_2, intl.locale),
}))
options.sort((a, b) => a.name.localeCompare(b.name))
options.unshift({
name: intl.formatMessage({ id: 'Search.Sidebar.Country.AllCountries' }),
alpha_2: 'XX',
})
return options
}, [intl.locale])

return (
<form
className="flex flex-col md:flex-row flex-wrap gap-4 mb-6"
onSubmit={handleSubmit(onSubmit)}
>
<div className="relative">
<div className="flex gap-4">
<Controller
control={control}
name="since"
render={({ field }) => (
<Input
{...field}
label={intl.formatMessage({ id: 'Search.Sidebar.From' })}
onFocus={() => setShowDatePicker(true)}
onKeyDown={() => setShowDatePicker(false)}
/>
)}
/>
<Controller
control={control}
name="until"
render={({ field }) => (
<Input
{...field}
label={intl.formatMessage({ id: 'Search.Sidebar.Until' })}
onFocus={() => setShowDatePicker(true)}
onKeyDown={() => setShowDatePicker(false)}
/>
)}
/>
</div>
{showDatePicker && (
<DateRangePicker
handleRangeSelect={handleRangeSelect}
initialRange={{
from: getValues('since'),
to: getValues('until'),
}}
close={() => setShowDatePicker(false)}
/>
)}
</div>

<Controller
control={control}
name="probe_cc"
render={({ field }) => (
<Select
{...field}
label={intl.formatMessage({ id: 'Search.Sidebar.Country' })}
>
{countryOptions.map((v) => (
<option key={v.alpha_2} value={v.alpha_2}>
{v.name}
</option>
))}
</Select>
)}
/>

<Controller
control={control}
name="probe_asn"
rules={{
pattern: {
value: asnRegEx,
message: intl.formatMessage({ id: 'Search.Sidebar.ASN.Error' }),
},
}}
render={({ field }) => (
<Input
{...field}
label={intl.formatMessage({ id: 'Search.Sidebar.ASN' })}
placeholder={intl.formatMessage({
id: 'Search.Sidebar.ASN.example',
})}
error={errors?.probe_asn?.message}
/>
)}
/>

<Controller
control={control}
name="domain"
render={({ field }) => (
<Input
{...field}
label={intl.formatMessage({ id: 'Search.Sidebar.Domain' })}
placeholder="e.g. example.com"
/>
)}
/>

<div className="flex items-end">
<button className="btn btn-primary" type="submit">
{intl.formatMessage({ id: 'General.Apply' })}
</button>
</div>
</form>
)
}

export default AlertsForm
Loading
Loading