diff --git a/components/Badge.js b/components/Badge.js index 0c01893cf..925ad7817 100644 --- a/components/Badge.js +++ b/components/Badge.js @@ -1,35 +1,31 @@ -import { Box, Flex, Text, theme } from 'ooni-components' +import * as icons from 'ooni-components/icons' import PropTypes from 'prop-types' import { cloneElement } from 'react' import { FormattedMessage } from 'react-intl' -import styled from 'styled-components' +import { twMerge } from 'tailwind-merge' import { getTestMetadata } from './utils' -import * as icons from 'ooni-components/icons' -// XXX replace what is inside of search/results-list.StyledResultTag -export const Badge = styled(Box)` - display: inline-block; - border-radius: 4px; - padding: 4px 8px; - line-height: 16px; - font-size: 12px; - text-transform: uppercase; - background-color: ${(props) => props.bg || props.theme.colors.gray8}; - border: ${(props) => (props.borderColor ? `1px solid ${props.borderColor}` : 'none')}; - color: ${(props) => props.color || props.theme.colors.white}; - letter-spacing: 1.25px; - font-weight: 600; -` +export const Badge = ({ className, ...props }) => { + return ( +
+ ) +} const TestGroupBadge = ({ testName, ...props }) => { const { icon, groupName, color } = getTestMetadata(testName) return ( - - - {groupName} + +
+
{groupName}
{cloneElement(icon, { size: 12 })} - +
) } @@ -43,13 +39,13 @@ export const CategoryBadge = ({ categoryCode }) => { } return ( - - - + +
+
- +
{IconComponent && } - +
) } diff --git a/components/BlockText.js b/components/BlockText.js index a38426390..3e0321be4 100644 --- a/components/BlockText.js +++ b/components/BlockText.js @@ -1,14 +1,8 @@ -import styled from 'styled-components' -import { Box } from 'ooni-components' +const BlockText = ({ className, ...props }) => ( +
+) -const BlockText = styled(Box)` - background-color: ${props => props.theme.colors.gray0}; - border-left: 10px solid ${props => props.theme.colors.blue5}; -` - -BlockText.defaultProps = { - p: 3, - fontSize: 1, -} - -export default BlockText \ No newline at end of file +export default BlockText diff --git a/components/ButtonSpinner.js b/components/ButtonSpinner.js index f8ff7aca7..8725730ea 100644 --- a/components/ButtonSpinner.js +++ b/components/ButtonSpinner.js @@ -1,16 +1,5 @@ import { ImSpinner8 } from 'react-icons/im' -import { keyframes, styled } from 'styled-components' -const spin = keyframes` -to { - transform: rotate(360deg); -} -` - -const StyledSpinner = styled(ImSpinner8)` - animation: ${spin} 1s linear infinite; -` - -const ButtonSpinner = () => +const ButtonSpinner = () => export default ButtonSpinner diff --git a/components/CallToActionBox.js b/components/CallToActionBox.js index 42099aaaf..28751062c 100644 --- a/components/CallToActionBox.js +++ b/components/CallToActionBox.js @@ -1,25 +1,20 @@ -import NLink from 'next/link' -import { Box, Button, Flex, Heading, Text } from 'ooni-components' +import Link from 'next/link' import { FormattedMessage } from 'react-intl' -const CallToActionBox = ({title, text}) => { +const CallToActionBox = ({ title, text }) => { return ( - - - - {title} - - - {text} - - - - - - +
+
+

{title}

+
{text}
+
+ + + +
) } -export default CallToActionBox \ No newline at end of file +export default CallToActionBox diff --git a/components/Chart.js b/components/Chart.js index 9be643ec3..df7d1dd4e 100644 --- a/components/Chart.js +++ b/components/Chart.js @@ -1,14 +1,14 @@ -import GridChart, { prepareDataForGridChart } from 'components/aggregation/mat/GridChart' +import GridChart, { + prepareDataForGridChart, +} from 'components/aggregation/mat/GridChart' import { MATContextProvider } from 'components/aggregation/mat/MATContext' import { DetailsBox } from 'components/measurement/DetailsBox' -import NLink from 'next/link' -import { Box, Flex } from 'ooni-components' -import React, { useEffect, useMemo } from 'react' +import Link from 'next/link' +import { memo, useEffect, useMemo } from 'react' import { MdBarChart, MdOutlineFileDownload } from 'react-icons/md' import { FormattedMessage, useIntl } from 'react-intl' import { MATFetcher } from 'services/fetchers' import useSWR from 'swr' -import { StyledHollowButton } from './SharedStyledComponents' const swrOptions = { revalidateOnFocus: false, @@ -23,51 +23,55 @@ export const MATLink = ({ query }) => { const showMATButton = !Array.isArray(query.test_name) return ( - - - {showMATButton && - - - {intl.formatMessage({id: 'MAT.Charts.SeeOnMAT'})} - - - } - - - - {intl.formatMessage({id: 'MAT.Charts.DownloadJSONData'})} - - - {intl.formatMessage({id: 'MAT.Charts.DownloadCSVData'})} - - - +
+ {showMATButton && ( + + + + )} +
+ + {intl.formatMessage({ id: 'MAT.Charts.DownloadJSONData' })}{' '} + + + + {intl.formatMessage({ id: 'MAT.Charts.DownloadCSVData' })}{' '} + + +
+
) } -const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, setState}) { +const Chart = memo(function Chart({ + testGroup = null, + queryParams = {}, + setState, +}) { const apiQuery = useMemo(() => { const qs = new URLSearchParams(queryParams).toString() return qs }, [queryParams]) - const { data, error } = useSWR( - apiQuery, - MATFetcher, - swrOptions - ) + const { data, error } = useSWR(apiQuery, MATFetcher, swrOptions) const [chartData, rowKeys, rowLabels] = useMemo(() => { if (!data) { return [null, 0] } - let chartData = data.data + const chartData = data.data const graphQuery = queryParams - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, graphQuery) + const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart( + chartData, + graphQuery, + ) return [reshapedData, rowKeys, rowLabels] }, [data, queryParams]) - useEffect(()=> { + useEffect(() => { if (setState && data?.data) setState(data.data) }, [data, setState]) @@ -76,32 +80,35 @@ const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, set return ( // - - - {(!chartData && !error) ? ( - - ) : ( - <> - - {!!chartData?.size && } - - )} - - {error && - -
- Error: {error.message} - - {JSON.stringify(error, null, 2)} - -
- }/> - } -
+
+ {!chartData && !error ? ( + + ) : ( + <> + + {!!chartData?.size && } + + )} + {error && ( + +
+ + Error: {error.message} + +
{JSON.stringify(error, null, 2)}
+
+ + } + /> + )} +
) }) diff --git a/components/CollapseTrigger.js b/components/CollapseTrigger.js deleted file mode 100644 index bf63d31f8..000000000 --- a/components/CollapseTrigger.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -import styled from 'styled-components' -import { MdExpandLess } from 'react-icons/md' - -export const CollapseTrigger = styled(MdExpandLess)` - cursor: pointer; - background-color: ${props => props.$bg || '#ffffff'}; - border-radius: 50%; - transform: ${props => props.$open ? 'rotate(0deg)': 'rotate(180deg)'}; - transition: transform 0.1s linear; -` diff --git a/components/CountryBox.js b/components/CountryBox.js index d4f16fafa..88b716497 100644 --- a/components/CountryBox.js +++ b/components/CountryBox.js @@ -1,32 +1,29 @@ import Flag from 'components/Flag' import { GridBox } from 'components/VirtualizedGrid' -import { Box, Flex, Text } from 'ooni-components' - -const CountryList = ({ countries, itemsPerRow = 6, gridGap = 3 }) => { - const gridTemplateColumns = ['1fr 1fr', '1fr 1fr', '1fr 1fr 1fr 1fr', [...Array(itemsPerRow)].map((i) => ('1fr')).join(' ')] +const CountryList = ({ countries, itemsPerRow = 6 }) => { return ( - + // lg:grid-cols-${itemsPerRow} is added to the safelist in tailwindConfig.config.js +
{countries.map((c) => ( - - {c.localisedName} - +
+
+ +
+
{c.localisedName}
+
} count={c.count} /> - )) - } - + ))} +
) } -export default CountryList \ No newline at end of file +export default CountryList diff --git a/components/DateRangePicker.js b/components/DateRangePicker.js index 45dba90dd..758202558 100644 --- a/components/DateRangePicker.js +++ b/components/DateRangePicker.js @@ -1,11 +1,8 @@ import { addDays, parse, sub } from 'date-fns' -import { Button } from 'ooni-components' -import React, { useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { DayPicker } from 'react-day-picker' -import 'react-day-picker/dist/style.css' import { useIntl } from 'react-intl' import OutsideClickHandler from 'react-outside-click-handler' -import styled from 'styled-components' import ar from 'date-fns/locale/ar' import de from 'date-fns/locale/de' @@ -24,58 +21,42 @@ import zhHant from 'date-fns/locale/zh-HK' import { getDirection } from 'components/withIntl' -const StyledDatetime = styled.div` -z-index: 99999; -position: absolute; -max-width: 300px; -background-color: #ffffff; -border: 1px solid ${props => props.theme.colors.gray2}; - -.rdp-cell { - padding: 2px 0; -} - -.rdp-day_selected:not([disabled]), -.rdp-day_selected:focus:not([disabled]), -.rdp-day_selected:active:not([disabled]), -.rdp-day_selected:hover:not([disabled]) { - background-color: ${props => props.theme.colors.blue5}; -} -` - -const StyledRangeButtons = styled.div` -margin: 1em 1em 0; -display: flex; -gap: 6px; -flex-wrap: wrap; -` - -const StyledFooter = styled.div` -display: flex; -justify-content: right; -gap: 6px; -` - const Footer = ({ handleRangeSelect, range, close }) => { const intl = useIntl() return ( - - - + - + close() + }} + > + {intl.formatMessage({ id: 'DateRange.Cancel' })} + +
) } -const DateRangePicker = ({handleRangeSelect, initialRange, close, ...props}) => { +const DateRangePicker = ({ + handleRangeSelect, + initialRange, + close, + ...props +}) => { const intl = useIntl() const tomorrow = addDays(new Date(), 1) const ranges = ['Today', 'LastWeek', 'LastMonth', 'LastYear'] @@ -112,46 +93,54 @@ const DateRangePicker = ({handleRangeSelect, initialRange, close, ...props}) => return en } }, [intl.locale]) - + const selectRange = (range) => { switch (range) { case 'Today': - handleRangeSelect({from: new Date(), to: tomorrow}) + handleRangeSelect({ from: new Date(), to: tomorrow }) break case 'LastWeek': - handleRangeSelect({from: sub(new Date(), {weeks: 1}) , to: tomorrow}) + handleRangeSelect({ from: sub(new Date(), { weeks: 1 }), to: tomorrow }) break case 'LastMonth': - handleRangeSelect({from: sub(new Date(), {months: 1}) , to: tomorrow}) + handleRangeSelect({ + from: sub(new Date(), { months: 1 }), + to: tomorrow, + }) break case 'LastYear': - handleRangeSelect({from: sub(new Date(), {years: 1}) , to: tomorrow}) + handleRangeSelect({ from: sub(new Date(), { years: 1 }), to: tomorrow }) break } } - const rangesList = ranges.map((range) => - - ) - const [range, setRange] = useState({from: parse(initialRange.from, 'yyyy-MM-dd', new Date()), to: parse(initialRange.to, 'yyyy-MM-dd', new Date())}) - + }} + > + {intl.formatMessage({ id: `DateRange.${range}` })} + + )) + const [range, setRange] = useState({ + from: parse(initialRange.from, 'yyyy-MM-dd', new Date()), + to: parse(initialRange.to, 'yyyy-MM-dd', new Date()), + }) + const onSelect = (range) => { setRange(range) } return ( - +
close()}> - {rangesList} - {rangesList}
+ toDate={tomorrow} selected={range} onSelect={onSelect} - footer={
) } -export default DateRangePicker \ No newline at end of file +export default DateRangePicker diff --git a/components/DomainChart.js b/components/DomainChart.js index 2a6bd2b43..44c7a0674 100644 --- a/components/DomainChart.js +++ b/components/DomainChart.js @@ -1,11 +1,11 @@ -// TODO: refactor to use the same chart for circumvention tools and domains (no request on probe_cc change) -import GridChart, { prepareDataForGridChart } from 'components/aggregation/mat/GridChart' +import GridChart, { + prepareDataForGridChart, +} from 'components/aggregation/mat/GridChart' import { MATContextProvider } from 'components/aggregation/mat/MATContext' import { MATLink } from 'components/Chart' import { DetailsBox } from 'components/measurement/DetailsBox' import { useRouter } from 'next/router' -import { Box, Flex, Heading } from 'ooni-components' -import React, { useEffect, useMemo } from 'react' +import { memo, useEffect, useMemo } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import { MATFetcher } from 'services/fetchers' import useSWR from 'swr' @@ -15,49 +15,52 @@ const swrOptions = { dedupingInterval: 10 * 60 * 1000, } -const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, setState}) { - const {query: { probe_cc }} = useRouter() - const {locale} = useIntl() +const Chart = memo(function Chart({ queryParams = {}, setState }) { + const { + query: { probe_cc }, + } = useRouter() + const { locale } = useIntl() const apiQuery = useMemo(() => { const qs = new URLSearchParams(queryParams).toString() return qs }, [queryParams]) - const { data, error } = useSWR( - apiQuery, - MATFetcher, - swrOptions - ) + const { data, error } = useSWR(apiQuery, MATFetcher, swrOptions) const [chartData, rowKeys, rowLabels] = useMemo(() => { if (!data) { return [null, 0] } - let chartData = data.data.sort((a, b) => (new Intl.Collator(locale).compare(a.probe_cc, b.probe_cc))) + let chartData = data.data.sort((a, b) => + new Intl.Collator(locale).compare(a.probe_cc, b.probe_cc), + ) - if (probe_cc) chartData = chartData.filter(d => probe_cc === d.probe_cc) + if (probe_cc) chartData = chartData.filter((d) => probe_cc === d.probe_cc) - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, queryParams, locale) + const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart( + chartData, + queryParams, + locale, + ) return [reshapedData, rowKeys, rowLabels] - }, [data, queryParams, probe_cc, locale]) - useEffect(()=> { + useEffect(() => { if (setState && data?.data) setState(data.data) }, [data, setState]) const headerOptions = { probe_cc: false, subtitle: false } - const linkParams = {...queryParams, ... probe_cc && {probe_cc}} + const linkParams = { ...queryParams, ...(probe_cc && { probe_cc }) } return ( // - - - {(!chartData && !error) ? ( +
+
+ {!chartData && !error ? ( ) : ( <> @@ -69,18 +72,23 @@ const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, set {!!chartData?.size && } )} - - {error && - -
- Error: {error.message} - - {JSON.stringify(error, null, 2)} - -
- }/> - } - +
+ {error && ( + +
+ + Error: {error.message} + +
{JSON.stringify(error, null, 2)}
+
+ + } + /> + )} +
) }) diff --git a/components/Flag.js b/components/Flag.js index b6e18136d..b123929d7 100644 --- a/components/Flag.js +++ b/components/Flag.js @@ -1,45 +1,261 @@ -import React from 'react' +import Image from 'next/image' import PropTypes from 'prop-types' -import styled from 'styled-components' -var supportedCountryCodes = [ - 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', - 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bl', - 'bm', 'bn', 'bo', 'bq', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc', - 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cu', 'cv', - 'cw', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', - 'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', - 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', - 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', - 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', - 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', - 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mf', 'mg', - 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'mv', - 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl', 'no', 'np', - 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', - 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', - 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', - 'ss', 'st', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th', 'tj', 'tk', - 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'um', 'un', - 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', - 'yt', 'za', 'zm', 'zw'] +const supportedCountryCodes = [ + 'ad', + 'ae', + 'af', + 'ag', + 'ai', + 'al', + 'am', + 'ao', + 'aq', + 'ar', + 'as', + 'at', + 'au', + 'aw', + 'ax', + 'az', + 'ba', + 'bb', + 'bd', + 'be', + 'bf', + 'bg', + 'bh', + 'bi', + 'bj', + 'bl', + 'bm', + 'bn', + 'bo', + 'bq', + 'br', + 'bs', + 'bt', + 'bv', + 'bw', + 'by', + 'bz', + 'ca', + 'cc', + 'cd', + 'cf', + 'cg', + 'ch', + 'ci', + 'ck', + 'cl', + 'cm', + 'cn', + 'co', + 'cr', + 'cu', + 'cv', + 'cw', + 'cx', + 'cy', + 'cz', + 'de', + 'dj', + 'dk', + 'dm', + 'do', + 'dz', + 'ec', + 'ee', + 'eg', + 'eh', + 'er', + 'es', + 'et', + 'eu', + 'fi', + 'fj', + 'fk', + 'fm', + 'fo', + 'fr', + 'ga', + 'gb', + 'gd', + 'ge', + 'gf', + 'gg', + 'gh', + 'gi', + 'gl', + 'gm', + 'gn', + 'gp', + 'gq', + 'gr', + 'gs', + 'gt', + 'gu', + 'gw', + 'gy', + 'hk', + 'hm', + 'hn', + 'hr', + 'ht', + 'hu', + 'id', + 'ie', + 'il', + 'im', + 'in', + 'io', + 'iq', + 'ir', + 'is', + 'it', + 'je', + 'jm', + 'jo', + 'jp', + 'ke', + 'kg', + 'kh', + 'ki', + 'km', + 'kn', + 'kp', + 'kr', + 'kw', + 'ky', + 'kz', + 'la', + 'lb', + 'lc', + 'li', + 'lk', + 'lr', + 'ls', + 'lt', + 'lu', + 'lv', + 'ly', + 'ma', + 'mc', + 'md', + 'me', + 'mf', + 'mg', + 'mh', + 'mk', + 'ml', + 'mm', + 'mn', + 'mo', + 'mp', + 'mq', + 'mr', + 'ms', + 'mt', + 'mu', + 'mv', + 'mw', + 'mx', + 'my', + 'mz', + 'na', + 'nc', + 'ne', + 'nf', + 'ng', + 'ni', + 'nl', + 'no', + 'np', + 'nr', + 'nu', + 'nz', + 'om', + 'pa', + 'pe', + 'pf', + 'pg', + 'ph', + 'pk', + 'pl', + 'pm', + 'pn', + 'pr', + 'ps', + 'pt', + 'pw', + 'py', + 'qa', + 're', + 'ro', + 'rs', + 'ru', + 'rw', + 'sa', + 'sb', + 'sc', + 'sd', + 'se', + 'sg', + 'sh', + 'si', + 'sj', + 'sk', + 'sl', + 'sm', + 'sn', + 'so', + 'sr', + 'ss', + 'st', + 'sv', + 'sx', + 'sy', + 'sz', + 'tc', + 'td', + 'tf', + 'tg', + 'th', + 'tj', + 'tk', + 'tl', + 'tm', + 'tn', + 'to', + 'tr', + 'tt', + 'tv', + 'tw', + 'tz', + 'ua', + 'ug', + 'um', + 'un', + 'us', + 'uy', + 'uz', + 'va', + 'vc', + 've', + 'vg', + 'vi', + 'vn', + 'vu', + 'wf', + 'ws', + 'ye', + 'yt', + 'za', + 'zm', + 'zw', +] -const FlagImg = styled.img` - width: ${props => props.size}px; - height: ${props => props.size}px; - clip-path: circle(50% at 50% 50%); -` - -const FlagContainer = styled.div` - border-radius: 50%; - /* padding-left: 3px; */ - /* padding-top: 3px; */ - width: ${props => props.size + 2}px; - height: ${props => props.size + 2}px; - border: ${props => props.$border ? '1px solid white' : 'none'}; -` - -export const Flag = ({countryCode, size, border}) => { +export const Flag = ({ countryCode = 'zz', size = 60, border }) => { countryCode = countryCode.toLowerCase() if (supportedCountryCodes.indexOf(countryCode) === -1) { // Map unsupported country codes to ZZ @@ -47,20 +263,24 @@ export const Flag = ({countryCode, size, border}) => { } const src = `/static/flags/1x1/${countryCode}.svg` return ( - - - +
+ +
) } Flag.propTypes = { countryCode: PropTypes.string.isRequired, - size: PropTypes.number.isRequired -} - -Flag.defaultProps = { - countryCode: 'zz', - size: 60 + size: PropTypes.number.isRequired, } export default Flag diff --git a/components/Footer.js b/components/Footer.js index f5fdc93c0..e9afe728a 100755 --- a/components/Footer.js +++ b/components/Footer.js @@ -1,139 +1,137 @@ -import { Box, Container, Flex, Link } from 'ooni-components' import ExplorerLogo from 'ooni-components/svgs/logos/OONI-HorizontalMonochromeInverted.svg' import PropTypes from 'prop-types' -import React from 'react' import { useIntl } from 'react-intl' -import styled from 'styled-components' -const StyledFooter = styled.footer` - background-color: ${(props) => props.theme.colors.blue9}; - color: #ffffff; - font-size: 16px; - margin-top: 32px; -` - -const FooterBox = styled(Box)` - padding-top: 25px; - padding-bottom: 25px; -` - -const FooterHead = styled.div` - font-weight: bolder; - margin-bottom: 10px; -` - -const StyledFooterItem = styled(Link)` - && { - text-decoration: none; - color: #ffffff; - cursor: pointer; - opacity: 0.5; - display: ${(props) => (props.$horizontal === 'true' ? 'inline' : 'block')}; - margin-left: ${(props) => (props.$horizontal === 'true' ? '1rem' : 0)}; - &:hover { - opacity: 1; - } - } -` +const FooterHead = ({ ...props }) => ( +
+) -const FooterLink = ({ label, href, horizontal = false }) => ( - // Use non-boolean value for props sent to non-DOM styled components - // https://www.styled-components.com/docs/faqs#why-am-i-getting-html-attribute-warnings - +const FooterLink = ({ label, ...props }) => ( + {label} - + ) FooterLink.propTypes = { label: PropTypes.node, href: PropTypes.string.isRequired, - horizontal: PropTypes.bool, } -const FooterText = styled.div` - margin-top: 0px; -` - const Footer = () => { const intl = useIntl() const currentYear = new Intl.DateTimeFormat(intl.locale, { year: 'numeric', }).format(new Date()) return ( - - - - - - - - - +
+
+
+
+
+
+ +
+
{' '} - {intl.formatMessage({ id: 'Footer.Text.Slogan' })}{' '} - - - - - {intl.formatMessage({ id: 'Footer.Heading.About' })} - +
+ {intl.formatMessage({ id: 'Footer.Text.Slogan' })} +
{' '} +
+
+
+
+ + {intl.formatMessage({ id: 'Footer.Heading.About' })} + + - - - {intl.formatMessage({ id: 'Footer.Heading.OONIProbe' })} - - - - - - - {intl.formatMessage({ id: 'Footer.Heading.Updates' })} - +
+
+ + {intl.formatMessage({ id: 'Footer.Heading.OONIProbe' })} + + + + + +
+
+ + {intl.formatMessage({ id: 'Footer.Heading.Updates' })} + + - - - - +
+
+
+
- {intl.formatMessage({ id: 'Footer.Text.Copyright' }, { currentYear })} +
+ {intl.formatMessage( + { id: 'Footer.Text.Copyright' }, + { currentYear }, + )} +
- - {intl.formatMessage({ id: 'Footer.Text.Version' })}: {process.env.GIT_COMMIT_SHA_SHORT} - +
+ {intl.formatMessage({ id: 'Footer.Text.Version' })}:{' '} + {process.env.GIT_COMMIT_SHA_SHORT} +
- - - - +
+
+
+
) } diff --git a/components/FormattedMarkdown.js b/components/FormattedMarkdown.js index 7c1a814dc..cfffc3fbd 100644 --- a/components/FormattedMarkdown.js +++ b/components/FormattedMarkdown.js @@ -1,23 +1,37 @@ // Documentation for markdown-to-jsx // https://github.com/probablyup/markdown-to-jsx -import React from 'react' +import Markdown from 'markdown-to-jsx' import PropTypes from 'prop-types' import { useIntl } from 'react-intl' -import Markdown from 'markdown-to-jsx' -import { Link, theme } from 'ooni-components' +import { twMerge } from 'tailwind-merge' + +const MdH1 = ({ children, className, ...props }) => ( +

+ {children} +

+) + +const MdUL = ({ children, className, ...props }) => ( +
    + {children} +
+) + +const MdP = ({ children, className, ...props }) => ( +

+ {children} +

+) export const FormattedMarkdownBase = ({ children }) => { return ( {children} @@ -30,7 +44,7 @@ const FormattedMarkdown = ({ id, defaultMessage, values }) => { return ( - {intl.formatMessage({id, defaultMessage}, values )} + {intl.formatMessage({ id, defaultMessage }, values)} ) } @@ -38,7 +52,7 @@ const FormattedMarkdown = ({ id, defaultMessage, values }) => { FormattedMarkdown.propTypes = { id: PropTypes.string.isRequired, defaultMessage: PropTypes.string, - values: PropTypes.object + values: PropTypes.object, } export default FormattedMarkdown diff --git a/components/GridLoader.js b/components/GridLoader.js index 8213a48bb..ce63862f0 100644 --- a/components/GridLoader.js +++ b/components/GridLoader.js @@ -1,8 +1,7 @@ -import React from 'react' import ContentLoader from 'react-content-loader' const Loader = (props) => ( - ( foregroundColor="#ecebeb" {...props} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) diff --git a/components/Header.js b/components/Header.js index 89c550653..0c643dc44 100644 --- a/components/Header.js +++ b/components/Header.js @@ -1,4 +1,3 @@ -import React from 'react' import Head from 'next/head' import { useRouter } from 'next/router' import { useIntl } from 'react-intl' @@ -7,7 +6,7 @@ const Header = () => { const { asPath, locale, defaultLocale } = useRouter() const lang = locale === defaultLocale ? '' : `/${locale}` - const canonical = 'https://explorer.ooni.org' + lang + asPath.split('?')[0] + const canonical = `https://explorer.ooni.org${lang}${asPath.split('?')[0]}` const intl = useIntl() const description = intl.formatMessage({ id: 'Home.Meta.Description' }) @@ -15,15 +14,44 @@ const Header = () => { return ( - - - - - - - - - + + + + + + + + + @@ -34,10 +62,14 @@ const Header = () => { - + ) } -export default Header \ No newline at end of file +export default Header diff --git a/components/Layout.js b/components/Layout.js index 2ff31f116..1b497263f 100644 --- a/components/Layout.js +++ b/components/Layout.js @@ -1,94 +1,60 @@ -import { Box, theme } from 'ooni-components' import PropTypes from 'prop-types' -import React, { useMemo } from 'react' -import { ThemeProvider, createGlobalStyle } from 'styled-components' +import { useMemo } from 'react' -import { StyledStickyNavBar, StyledStickySubMenu } from 'components/SharedStyledComponents' -import { getDirection } from 'components/withIntl' +import { StyledStickyNavBar } from 'components/SharedStyledComponents' import { UserProvider } from 'hooks/useUser' import { useRouter } from 'next/router' -import { useIntl } from 'react-intl' import Footer from './Footer' import Header from './Header' import NavBar from './NavBar' -theme.maxWidth = 1024 - -const GlobalStyle = createGlobalStyle` - * { - text-rendering: geometricPrecision; - box-sizing: border-box; - } - body, html { - // direction: ${props => props.direction}; - margin: 0; - padding: 0; - font-size: 14px; - height: 100%; - background-color: #ffffff; - } - a { - text-decoration: none; - color: ${(props) => props.theme.colors.blue6}; - &:hover { - color: ${(props) => props.theme.colors.blue9}; - } - } - /* - Sticky Footer fix - Based on: https://philipwalton.github.io/solved-by-flexbox/demos/sticky-footer/ - */ - .site { - display: flex; - flex-direction: column; - min-height: 100vh; - } - .content { - flex: 1 0 auto; - } -` - const Layout = ({ children }) => { - const { locale } = useIntl() const { pathname } = useRouter() const navbarColor = useMemo(() => { - return pathname === '/' || pathname.match(/^\/m\/\S+/) || pathname.match(/^\/measurement\/\S+/) ? - 'transparent' - : null + return pathname === '/' || + pathname.match(/^\/m\/\S+/) || + pathname.match(/^\/measurement\/\S+/) + ? null + : 'bg-blue-500' }, [pathname]) + const navbarSticky = useMemo(() => { - return pathname === '/countries' || - pathname === '/domains' || - pathname === '/networks' || - pathname === '/findings' || - pathname.match(/^\/country\/\S+/) + return ( + pathname === '/countries' || + pathname === '/domains' || + pathname === '/networks' || + pathname === '/findings' || + pathname.match(/^\/country\/\S+/) + ) }, [pathname]) return ( - - - -
+ +
+
- {navbarSticky ? + {navbarSticky ? ( - : + + ) : ( - } - - { children } - + )} +
+ {children} +
+
+
- - +
+
) } Layout.propTypes = { - children: PropTypes.object.isRequired + children: PropTypes.object.isRequired, } export default Layout diff --git a/components/MATChart.js b/components/MATChart.js index e2e25269d..1482c5af7 100644 --- a/components/MATChart.js +++ b/components/MATChart.js @@ -1,15 +1,14 @@ -import { Box, Text } from 'ooni-components' -import { StackedBarChart } from 'components/aggregation/mat/StackedBarChart' +import axios from 'axios' import { FunnelChart } from 'components/aggregation/mat/FunnelChart' +import { MATContextProvider } from 'components/aggregation/mat/MATContext' import { NoCharts } from 'components/aggregation/mat/NoCharts' +import { StackedBarChart } from 'components/aggregation/mat/StackedBarChart' import TableView from 'components/aggregation/mat/TableView' -import { useMemo } from 'react' -import useSWR from 'swr' -import dayjs from 'services/dayjs' import { axiosResponseTime } from 'components/axios-plugins' -import axios from 'axios' -import { MATContextProvider } from 'components/aggregation/mat/MATContext' +import { useMemo } from 'react' import { useIntl } from 'react-intl' +import dayjs from 'services/dayjs' +import useSWR from 'swr' import { FormattedMarkdownBase } from './FormattedMarkdown' axiosResponseTime(axios) @@ -65,47 +64,62 @@ export const MATChartWrapper = ({ link, caption }) => { ...searchParams, } - return !!searchParams && - - - {caption && ( - - {captionText} - - )} - + return ( + !!searchParams && ( +
+ + {caption && ( +
+ {captionText} +
+ )} +
+ ) + ) } const MATChart = ({ query, showFilters = true }) => { const intl = useIntl() - const { data, error, isValidating } = useSWR(query ? query : null, fetcher, swrOptions) + const { data, error, isValidating } = useSWR( + query ? query : null, + fetcher, + swrOptions, + ) const showLoadingIndicator = useMemo(() => isValidating, [isValidating]) return ( - + <> {error && } - + <> {showLoadingIndicator ? ( - -

{intl.formatMessage({ id: 'General.Loading' })}

-
+

{intl.formatMessage({ id: 'General.Loading' })}

) : ( <> - {data?.data?.result?.length > 0 ? - - {data && data.data.dimension_count == 0 && } - {data && data.data.dimension_count == 1 && } + {data?.data?.result?.length > 0 ? ( + <> + {data && data.data.dimension_count === 0 && ( + + )} + {data && data.data.dimension_count === 1 && ( + + )} {data && data.data.dimension_count > 1 && ( - + )} - : - } + + ) : ( + + )} )} -
+
-
+ ) } diff --git a/components/NavBar.js b/components/NavBar.js index d3e4e20b5..ea40a0da1 100644 --- a/components/NavBar.js +++ b/components/NavBar.js @@ -1,129 +1,63 @@ import useUser from 'hooks/useUser' -import NLink from 'next/link' +import Link from 'next/link' import { useRouter } from 'next/router' -import { Box, Container, Flex } from 'ooni-components' import ExplorerLogo from 'ooni-components/svgs/logos/Explorer-HorizontalMonochromeInverted.svg' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { MdClose, MdMenu } from 'react-icons/md' import { FormattedMessage, useIntl } from 'react-intl' -import styled from 'styled-components' import { getLocalisedLanguageName } from 'utils/i18nCountries' import { getDirection } from './withIntl' -const StyledNavItem = styled(NLink)` - position: relative; - color: ${(props) => props.theme.colors.white}; - cursor: pointer; - padding-bottom: ${(props) => (props.$active ? '4px' : '6px')}; - opacity: ${(props) => (props.$active ? '1' : '0.6')}; - border-bottom: ${(props) => (props.$active ? `2px solid ${props.theme.colors.white}` : 'none')}; - - &:hover { - padding-bottom: 4px; - color: ${(props) => props.theme.colors.white}; - opacity: 1; - border-bottom: 2px solid ${(props) => props.theme.colors.white}; - } -` - -const LanguageSelect = styled.select` -color: ${(props) => props.theme.colors.white}; -background: none; -opacity: 0.6; -border: none; -text-transform: capitalize; -cursor: pointer; -font-family: inherit; -font-size: inherit; -padding: 0; -padding-bottom: 6px; -outline: none; -appearance: none; --webkit-appearance: none; --moz-appearance: none; --ms-appearance: none; --o-appearance: none; -&:hover { - opacity: 1; -} -// reset option styling for browsers that apply it to its native styling (Brave) -> option { - color: initial; - opacity: initial; -} -` - -const NavItem = ({ label, href, ...rest }) => { +const LanguageSelect = (props) => ( + - - ) - } -) + return ( + <> + + + ) +}) IndeterminateCheckbox.displayName = 'IndeterminateCheckbox' const SearchFilter = ({ @@ -81,23 +47,17 @@ const SearchFilter = ({ return ( { + onChange={(e) => { setFilter(e.target.value || undefined) // Set undefined to remove the filter entirely }} - placeholder={intl.formatMessage({id: 'MAT.Table.FilterPlaceholder'}, {count})} + placeholder={intl.formatMessage( + { id: 'MAT.Table.FilterPlaceholder' }, + { count }, + )} /> ) } -const StyledGlobalFilter = styled(Box)` - margin: 16px; - margin-top: 10px; - input { - border: 0; - outline: 0; - } -` - function GlobalFilter({ preGlobalFilteredRows, globalFilter, @@ -106,7 +66,7 @@ function GlobalFilter({ const intl = useIntl() const count = preGlobalFilteredRows.length const [value, setValue] = useState(globalFilter) - const onChange = useAsyncDebounce(value => { + const onChange = useAsyncDebounce((value) => { setGlobalFilter(value || '') }, 200) @@ -117,28 +77,27 @@ function GlobalFilter({ }, [globalFilter]) return ( - - {intl.formatMessage({id: 'MAT.Table.Search'})}{' '} +
+ {intl.formatMessage({ id: 'MAT.Table.Search' })}{' '} { + onChange={(e) => { setValue(e.target.value) onChange(e.target.value) }} - placeholder={intl.formatMessage({id: 'MAT.Table.FilterPlaceholder'}, {count})} + placeholder={intl.formatMessage( + { id: 'MAT.Table.FilterPlaceholder' }, + { count }, + )} /> - +
) } const SortHandle = ({ isSorted, isSortedDesc }) => { return ( - - {isSorted ? ( - isSortedDesc ? '▼' : '▲' - ) : ( -   - )} + <>{isSorted ? isSortedDesc ? : : } ) } @@ -158,81 +117,87 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => { Filter: SearchFilter, Cell: ({ value }) => { const intl = useIntl() - return typeof value === 'number' ? intl.formatNumber(value, {}) : String(value) - } + return typeof value === 'number' + ? intl.formatNumber(value, {}) + : String(value) + }, }), - [] + [], ) // Aggregate by the first column - const initialState = useMemo(() => ({ - hiddenColumns: ['yAxisCode'], - sortBy: [{ id: 'yAxisLabel', desc: false }] - }),[]) + const initialState = useMemo( + () => ({ + hiddenColumns: ['yAxisCode'], + sortBy: [{ id: 'yAxisLabel', desc: false }], + }), + [], + ) - const getRowId = useCallback(row => row[query.axis_y], [query.axis_y]) + const getRowId = useCallback((row) => row[query.axis_y], [query.axis_y]) - const columns = useMemo(() => [ - { - Header: intl.formatMessage({ id: `MAT.Table.Header.${yAxis}`}), - Cell: ({ value, row }) => ( - - {value} - - ), - id: 'yAxisLabel', - accessor: 'rowLabel', - filter: 'text', - style: { - width: '35%' - } - }, - { - id: 'yAxisCode', - accessor: yAxis, - disableFilters: true, - }, - { - Header: , - accessor: 'anomaly_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'confirmed_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'failure_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - }, - { - Header: , - accessor: 'measurement_count', - width: 150, - sortDescFirst: true, - disableFilters: true, - style: { - textAlign: 'end' - } - } - ], [intl, yAxis]) + const columns = useMemo( + () => [ + { + Header: intl.formatMessage({ id: `MAT.Table.Header.${yAxis}` }), + Cell: ({ value, row }) => ( +
{value}
+ ), + id: 'yAxisLabel', + accessor: 'rowLabel', + filter: 'text', + style: { + width: '35%', + }, + }, + { + id: 'yAxisCode', + accessor: yAxis, + disableFilters: true, + }, + { + Header: , + accessor: 'anomaly_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end', + }, + }, + { + Header: , + accessor: 'confirmed_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end', + }, + }, + { + Header: , + accessor: 'failure_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end', + }, + }, + { + Header: , + accessor: 'measurement_count', + width: 150, + sortDescFirst: true, + disableFilters: true, + style: { + textAlign: 'end', + }, + }, + ], + [intl, yAxis], + ) const { getTableProps, @@ -279,22 +244,33 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => {
- ) + ), }, - ...columns + ...columns, ]) - } + }, ) - + const updateCharts = useCallback(() => { - const selectedRows = Object.keys(state.selectedRowIds).sort((a,b) => sortRows(a, b, query.axis_y, intl.locale)) + const selectedRows = Object.keys(state.selectedRowIds).sort((a, b) => + sortRows(a, b, query.axis_y, intl.locale), + ) - if (selectedRows.length > 0 && selectedRows.length !== preGlobalFilteredRows.length) { + if ( + selectedRows.length > 0 && + selectedRows.length !== preGlobalFilteredRows.length + ) { setDataForCharts(selectedRows) } else { setDataForCharts(noRowsSelected) } - }, [preGlobalFilteredRows.length, query.axis_y, state.selectedRowIds, setDataForCharts, intl.locale]) + }, [ + preGlobalFilteredRows.length, + query.axis_y, + state.selectedRowIds, + setDataForCharts, + intl.locale, + ]) /** * Reset the table filter @@ -312,10 +288,15 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => { setGlobalFilter('') } setDataForCharts(noRowsSelected) - }, [setGlobalFilter, state.globalFilter, toggleAllRowsSelected, setDataForCharts]) + }, [ + setGlobalFilter, + state.globalFilter, + toggleAllRowsSelected, + setDataForCharts, + ]) useEffect(() => { - if (state.globalFilter == undefined && resetTableRef.current === true) { + if (state.globalFilter === undefined && resetTableRef.current === true) { resetTableRef.current = false toggleAllRowsSelected(false) } @@ -331,64 +312,91 @@ const Filters = ({ data = [], tableData, setDataForCharts, query }) => { }) return ( - - - - - - - - - + +
+
+ + +
+
+
+
{headerGroups.map((headerGroup, i) => ( - +
{headerGroup.headers.map((column, i) => { return ( - - +
+ {column.render('Header')} - {column.canSort && - - } + {column.canSort && ( + + )} - - )} - )} - +
+ ) + })} +
))} - +
- - +
+
- - {virtualRows.map(virtualRow => { + {virtualRows.map((virtualRow) => { const row = rows[virtualRow.index] prepareRow(row) return ( - { left: 0, width: '100%', height: `${virtualRow.size}px`, - transform: `translateY(${virtualRow.start}px)` - } - })}> + transform: `translateY(${virtualRow.start}px)`, + }, + })} + > {row.cells.map((cell, i) => { return ( - + style: cell.column.style, + }, + ])} + > {cell.render('Cell')} - +
) })} - +
) })} - +
-
-
-
+
+
+ ) } -export default Filters \ No newline at end of file +export default Filters diff --git a/components/aggregation/mat/Form.js b/components/aggregation/mat/Form.js index 7bd5e4c9a..93c7954c9 100644 --- a/components/aggregation/mat/Form.js +++ b/components/aggregation/mat/Form.js @@ -1,13 +1,12 @@ import { format } from 'date-fns' -import { Box, Button, Flex, Input, Select } from 'ooni-components' +import { useRouter } from 'next/router' +import { Input, Select } from 'ooni-components' import PropTypes from 'prop-types' import { useCallback, useEffect, useMemo, useState } from 'react' import { Controller, useForm } from 'react-hook-form' import { FormattedMessage, defineMessages, useIntl } from 'react-intl' import dayjs from 'services/dayjs' import { localisedCountries } from 'utils/i18nCountries' - -import { useRouter } from 'next/router' import DateRangePicker from '../../DateRangePicker' import { TestNameOptions } from '../../TestNameOptions' import { categoryCodes } from '../../utils/categoryCodes' @@ -73,13 +72,24 @@ const yAxisOptions = [ ['', [], false], ] -const testsWithValidDomainFilter = ['web_connectivity', 'http_requests', 'dns_consistency', 'tcp_connect'] +const testsWithValidDomainFilter = [ + 'web_connectivity', + 'http_requests', + 'dns_consistency', + 'tcp_connect', +] -const filterAxisOptions = (options, countryValue = '', testNameValue = 'web_connectivity') => { +const filterAxisOptions = ( + options, + countryValue = '', + testNameValue = 'web_connectivity', +) => { return options .filter(([option, validTestNames, hideForSingleCountry]) => { if (hideForSingleCountry && countryValue !== '') return false - return validTestNames.length === 0 || validTestNames.includes(testNameValue) + return ( + validTestNames.length === 0 || validTestNames.includes(testNameValue) + ) }) .map(([option]) => option) } @@ -111,7 +121,10 @@ export const Form = ({ onSubmit, query }) => { const router = useRouter() const [showConfirmation, setShowConfirmation] = useState(false) - const defaultValues = useMemo(() => Object.assign({}, defaultDefaultValues, query), [query]) + const defaultValues = useMemo( + () => Object.assign({}, defaultDefaultValues, query), + [query], + ) const { handleSubmit, control, getValues, watch, reset, setValue } = useForm({ defaultValues, @@ -124,31 +137,35 @@ export const Form = ({ onSubmit, query }) => { } }, [defaultValues, reset, router.isReady]) - const [since, setSince] = useState(defaultValues['since']) - const [until, setUntil] = useState(defaultValues['until']) - const [countryValue, setCountryValue] = useState(defaultValues['probe_cc']) - const [testNameValue, setTestNameValue] = useState(defaultValues['test_name']) + const [since, setSince] = useState(defaultValues.since) + const [until, setUntil] = useState(defaultValues.until) + const [countryValue, setCountryValue] = useState(defaultValues.probe_cc) + const [testNameValue, setTestNameValue] = useState(defaultValues.test_name) useEffect(() => { - const subscription = watch((value, { name, type }) => { - if (name === 'since') setSince(value['since']) - if (name === 'until') setUntil(value['until']) - if (name === 'probe_cc') setCountryValue(value['probe_cc']) - if (name === 'test_name') setTestNameValue(value['test_name']) + const subscription = watch((value, { name }) => { + if (name === 'since') setSince(value.since) + if (name === 'until') setUntil(value.until) + if (name === 'probe_cc') setCountryValue(value.probe_cc) + if (name === 'test_name') setTestNameValue(value.test_name) }) return () => subscription.unsubscribe() }, [watch]) - const sortedCountries = useMemo(() => ( + const sortedCountries = useMemo( + () => localisedCountries(intl.locale).sort((a, b) => - new Intl.Collator(intl.locale).compare(a.localisedCountryName, b.localisedCountryName)) - ), - [intl.locale] + new Intl.Collator(intl.locale).compare( + a.localisedCountryName, + b.localisedCountryName, + ), + ), + [intl.locale], ) const showWebConnectivityFilters = useMemo( () => isValidFilterForTestname(testNameValue, testsWithValidDomainFilter), - [testNameValue] + [testNameValue], ) // reset domain and input when web_connectivity is deselected useEffect(() => { @@ -178,7 +195,7 @@ export const Form = ({ onSubmit, query }) => { setShowConfirmation(false) handleSubmit(onSubmit)(e) }, - [handleSubmit, onSubmit] + [handleSubmit, onSubmit], ) const onCancel = useCallback((e) => { @@ -190,7 +207,11 @@ export const Form = ({ onSubmit, query }) => { (e) => { e.preventDefault() - const [since, until, timeGrain] = getValues(['since', 'until', 'time_grain']) + const [since, until, timeGrain] = getValues([ + 'since', + 'until', + 'time_grain', + ]) const shouldShowConfirmationModal = () => { if (timeGrain === 'month') return false const diff = dayjs(until).diff(dayjs(since), 'month') @@ -205,7 +226,7 @@ export const Form = ({ onSubmit, query }) => { onConfirm(e) } }, - [getValues, onConfirm] + [getValues, onConfirm], ) const xAxisOptionsFiltered = useMemo(() => { @@ -213,7 +234,8 @@ export const Form = ({ onSubmit, query }) => { }, [testNameValue, countryValue]) useEffect(() => { - if (!xAxisOptionsFiltered.includes(getValues('axis_x'))) setValue('axis_x', 'measurement_start_day') + if (!xAxisOptionsFiltered.includes(getValues('axis_x'))) + setValue('axis_x', 'measurement_start_day') }, [setValue, getValues, xAxisOptionsFiltered]) const yAxisOptionsFiltered = useMemo(() => { @@ -221,37 +243,54 @@ export const Form = ({ onSubmit, query }) => { }, [testNameValue, countryValue]) useEffect(() => { - if (!yAxisOptionsFiltered.includes(getValues('axis_y'))) setValue('axis_y', '') + if (!yAxisOptionsFiltered.includes(getValues('axis_y'))) + setValue('axis_y', '') }, [setValue, getValues, yAxisOptionsFiltered]) const timeGrainOptions = useMemo(() => { const dateRegex = /^\d{4}-\d{2}-\d{2}$/ - if (!until?.match(dateRegex) || !since?.match(dateRegex)) return ['hour', 'day', 'week', 'month'] + if (!until?.match(dateRegex) || !since?.match(dateRegex)) + return ['hour', 'day', 'week', 'month'] const diff = dayjs(until).diff(dayjs(since), 'day') if (diff < 8) { const availableValues = ['hour', 'day'] - if (!availableValues.includes(getValues('time_grain'))) setValue('time_grain', 'hour') + if (!availableValues.includes(getValues('time_grain'))) + setValue('time_grain', 'hour') return availableValues - } else if (diff >= 8 && diff < 31) { + } + if (diff >= 8 && diff < 31) { const availableValues = ['day', 'week'] - if (!availableValues.includes(getValues('time_grain'))) setValue('time_grain', 'day') + if (!availableValues.includes(getValues('time_grain'))) + setValue('time_grain', 'day') return availableValues - } else if (diff >= 31) { + } + if (diff >= 31) { const availableValues = ['day', 'week', 'month'] - if (!availableValues.includes(getValues('time_grain'))) setValue('time_grain', 'day') + if (!availableValues.includes(getValues('time_grain'))) + setValue('time_grain', 'day') return availableValues } }, [setValue, getValues, since, until]) return (
- - - + +
+
( - + {sortedCountries.map((c, idx) => (
+
( - + )} /> - - - - - ( - setShowDatePicker(true)} - onKeyDown={() => setShowDatePicker(false)} - /> - )} - /> - - - ( - setShowDatePicker(true)} - onKeyDown={() => setShowDatePicker(false)} - /> - )} - /> - - +
+
+
+ ( + setShowDatePicker(true)} + onKeyDown={() => setShowDatePicker(false)} + /> + )} + /> + ( + setShowDatePicker(true)} + onKeyDown={() => setShowDatePicker(false)} + /> + )} + /> +
{showDatePicker && ( { close={() => setShowDatePicker(false)} /> )} - - +
+
( - {timeGrainOptions.map((option, idx) => (
+
( - {xAxisOptionsFiltered.map((option, idx) => ( ))} )} /> - - +
+
( - {yAxisOptionsFiltered.map((option, idx) => ( ))} )} /> - - - - +
+
+
+
( - )} /> - +
{showWebConnectivityFilters && ( <> - +
( )} /> - - +
+
( )} /> - - +
+
( )} /> - +
)} - - - - +
+
+ +
) } diff --git a/components/aggregation/mat/FunnelChart.js b/components/aggregation/mat/FunnelChart.js index 857c75a1c..dde60f509 100644 --- a/components/aggregation/mat/FunnelChart.js +++ b/components/aggregation/mat/FunnelChart.js @@ -1,20 +1,21 @@ -import React from 'react' import { ResponsiveFunnel } from '@nivo/funnel' -import { theme } from 'ooni-components' +import { colors } from 'ooni-components' const stateColors = { - 'ok': theme.colors.green8, - 'failure': theme.colors.gray6, - 'anomaly': theme.colors.yellow9, - 'confirmed': theme.colors.red7, + ok: colors.green['800'], + failure: colors.gray['600'], + anomaly: colors.yellow['900'], + confirmed: colors.red['700'], } const reshapeData = (data) => { - return Object.entries(data).map((entry) => ({ - 'id': entry[0], - 'value': entry[1], - 'label': `${entry[0].split('_')[0]}` - })).sort((a,b) => a.value < b.value) + return Object.entries(data) + .map((entry) => ({ + id: entry[0], + value: entry[1], + label: `${entry[0].split('_')[0]}`, + })) + .sort((a, b) => a.value < b.value) } export const FunnelChart = ({ data }) => ( @@ -22,9 +23,9 @@ export const FunnelChart = ({ data }) => ( data={reshapeData(data)} margin={{ top: 20, right: 20, bottom: 20, left: 20 }} valueFormat=">-.2s" - colors={d => stateColors[d.id.split('_')[0]] ?? theme.colors.blue5} + colors={(d) => stateColors[d.id.split('_')[0]] ?? colors.blue['500']} borderWidth={20} - labelColor={{ from: 'color', modifiers: [ [ 'darker', 3 ] ] }} + labelColor={{ from: 'color', modifiers: [['darker', 3]] }} beforeSeparatorLength={100} beforeSeparatorOffset={20} afterSeparatorLength={100} @@ -33,4 +34,4 @@ export const FunnelChart = ({ data }) => ( currentBorderWidth={40} motionConfig="wobbly" /> -) \ No newline at end of file +) diff --git a/components/aggregation/mat/GridChart.js b/components/aggregation/mat/GridChart.js index 944c0aa19..543fc6b9d 100644 --- a/components/aggregation/mat/GridChart.js +++ b/components/aggregation/mat/GridChart.js @@ -1,18 +1,16 @@ -import React, { useMemo, useRef } from 'react' -import PropTypes from 'prop-types' -import { TooltipProvider, Tooltip } from '@nivo/tooltip' import { Container } from '@nivo/core' -import { Flex } from 'ooni-components' - -import RowChart from './RowChart' -import { sortRows, fillDataHoles } from './computations' -import { useMATContext } from './MATContext' +import { Tooltip, TooltipProvider } from '@nivo/tooltip' +import PropTypes from 'prop-types' +import { memo, useMemo, useRef } from 'react' import { ChartHeader } from './ChartHeader' -import { getRowLabel } from './labels' -import { VirtualRows } from './VirtualRows' -import { XAxis } from './XAxis' import { barThemeForTooltip } from './CustomTooltip' +import { useMATContext } from './MATContext' import { NoCharts } from './NoCharts' +import RowChart from './RowChart' +import { VirtualRows } from './VirtualRows' +import { XAxis } from './XAxis' +import { fillDataHoles, sortRows } from './computations' +import { getRowLabel } from './labels' const ROW_HEIGHT = 70 const XAXIS_HEIGHT = 62 @@ -60,7 +58,9 @@ export const prepareDataForGridChart = (data, query, locale) => { const reshapedDataWithoutHoles = fillDataHoles(reshapedData, query) - const sortedRowKeys = rows.sort((a, b) => (sortRows(rowLabels[a], rowLabels[b], query.axis_y, locale))) + const sortedRowKeys = rows.sort((a, b) => + sortRows(rowLabels[a], rowLabels[b], query.axis_y, locale), + ) return [reshapedDataWithoutHoles, sortedRowKeys, rowLabels] } @@ -86,11 +86,18 @@ export const prepareDataForGridChart = (data, query, locale) => { * header - an element showing some summary information on top of the charts } */ -const GridChart = ({ data, rowKeys, rowLabels, height = 'auto', header, selectedRows = null, noLabels = false }) => { - +const GridChart = ({ + data, + rowKeys, + rowLabels, + height = 'auto', + header, + selectedRows = null, + noLabels = false, +}) => { // Fetch query state from context instead of router // because some params not present in the URL are injected in the context - const [ query ] = useMATContext() + const [query] = useMATContext() const { tooltipIndex } = query const indexBy = query.axis_x const tooltipContainer = useRef(null) @@ -108,39 +115,38 @@ const GridChart = ({ data, rowKeys, rowLabels, height = 'auto', header, selected let gridHeight = height if (height === 'auto') { const rowCount = selectedRows?.length ?? rowKeys.length - gridHeight = Math.min( XAXIS_HEIGHT + (rowCount * ROW_HEIGHT), GRID_MAX_HEIGHT) + gridHeight = Math.min(XAXIS_HEIGHT + rowCount * ROW_HEIGHT, GRID_MAX_HEIGHT) } if (!data || data.size < 1) { - return ( - - ) + return } // To correctly align with the rows, generate a data row with only x-axis values // e.g [ {measurement_start_day: '2022-01-01'}, {measurement_start_day: '2022-01-02'}... ] - const xAxisData = data.get(rowKeys[0]).map(d => ({ [query.axis_x]: d[query.axis_x]})) + const xAxisData = data + .get(rowKeys[0]) + .map((d) => ({ [query.axis_x]: d[query.axis_x] })) const rowHeight = noLabels ? 500 : ROW_HEIGHT return ( - - +
+
{/* Fake axis on top of list. Possible alternative: dummy chart with axis and valid tickValues */} {/* Use a virtual list only for higher count of rows */} {rowsToRender.length < 10 ? ( - {!noLabels && } - {rowsToRender.map((rowKey, index) => + {rowsToRender.map((rowKey, index) => ( - )} - + ))} +
) : ( } @@ -162,8 +168,8 @@ const GridChart = ({ data, rowKeys, rowLabels, height = 'auto', header, selected tooltipIndex={tooltipIndex} /> )} - - +
+
@@ -175,12 +181,9 @@ GridChart.propTypes = { rowKeys: PropTypes.arrayOf(PropTypes.string), rowLabels: PropTypes.objectOf(PropTypes.string), selectedRows: PropTypes.arrayOf(PropTypes.string), - height: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number - ]), + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), header: PropTypes.element, - noLabels: PropTypes.bool + noLabels: PropTypes.bool, } -export default React.memo(GridChart) +export default memo(GridChart) diff --git a/components/aggregation/mat/HeatMapChart.js b/components/aggregation/mat/HeatMapChart.js deleted file mode 100644 index 78def1db7..000000000 --- a/components/aggregation/mat/HeatMapChart.js +++ /dev/null @@ -1,216 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import { HeatMap } from '@nivo/heatmap' -import { Box, Heading } from 'ooni-components' - -import { Debug } from '../Debug' -import { - colorNormal, - colorAnomaly, - colorConfirmed, - colorError -} from '../../colors' - -const getHeatMapData = (data, yAxis) => { - /* - 'IT': { - probe_cc: 'IT', - '2021-03-01': { - anomaly_count: 111, - measurement_count: 200, - confirmed_count: 10 - }, - '2021-03-02': { - anomaly_count: 12, - ... - } - } - */ - const keys = new Set() - const indexBy = yAxis // probe_cc or input - const groupByXAxis = data.reduce((acc, item) => { - const { measurement_start_day, ...restOfItem } = item - const key = item[yAxis] - const value = restOfItem - const date = measurement_start_day // xAxis - keys.add(date) - - if (!(key in acc)) { - // Add a new (day,count) to that - acc[key] = {} - acc[key][yAxis] = key - } - acc[key][date] = value - - return acc - }, {}) - const reducedData = Object.keys(groupByXAxis).map(k => groupByXAxis[k]) - return [ - [...keys], - reducedData, - indexBy - ] -} - -const CustomHeatMapCell = ({ - // data, - value, - x, - y, - width, - height, - // color, - opacity, - borderWidth, - borderColor, - textColor, - // enableLabel, - // onClick, - onHover, - onLeave, - // theme -}) => { - const barsData = [ - ['ok_count', colorNormal], - ['failure_count', colorError], - ['confirmed_count', colorConfirmed], - ['anomaly_count', colorAnomaly], - ] - - let prevHeight = 0 - - return ( - - {value !== undefined && barsData.map(([barLabel, barColor], index) => { - const barHeight = height * (value[barLabel] / value['measurement_count']) // ((index + 1) / 10) - const barY = prevHeight === 0 ? (height * -0.5) : (height * -0.5) + prevHeight - prevHeight = prevHeight + barHeight - return ( - - ) - })} - {value === undefined && ( - - {'❤️'} - - )} - - ) -} - -CustomHeatMapCell.propTypes = { - borderColor: PropTypes.string, - borderWidth: PropTypes.number, - height: PropTypes.number, - onHover: PropTypes.func, - onLeave: PropTypes.func, - opacity: PropTypes.number, - textColor: PropTypes.string, - value: PropTypes.any, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number -} - -const HeatMapCell = React.memo(CustomHeatMapCell) - -const HEIGHT_MULTIPLIER = 30 - -export const HeatmapChart = ({ data, query }) => { - - const yAxis = query.axis_y - const [keys, shapedData, indexBy] = getHeatMapData(data, yAxis) - return ( - - - `\n Total - ${value.measurement_count}\n Anomalies - ${value.anomaly_count}\n Confirmed - ${value.confirmed_count}\n Ok - ${value.ok_count}\n Failures - ${value.failure_count}` - } - forceSquare={true} - isInteractive={true} - animate={false} - /> - - - Processed Data -
-            {JSON.stringify(shapedData, null, 2)}
-          
- API Response Data -
-            {JSON.stringify(data, null, 2)}
-          
-
-
-
- ) -} -HeatmapChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - anomaly_count: PropTypes.number, - confirmed_count: PropTypes.number, - failure_count: PropTypes.number, - measurement_count: PropTypes.number, - ok_count: PropTypes.number, - measurement_start_day: PropTypes.string, - probe_cc: PropTypes.string - }) - ), - query: PropTypes.shape({ - axis_x: PropTypes.string, - axis_y: PropTypes.string, - since: PropTypes.string, - until: PropTypes.string, - test_name: PropTypes.string, - input: PropTypes.string, - probe_cc: PropTypes.string, - category_code: PropTypes.string, - }) -} diff --git a/components/aggregation/mat/Help.js b/components/aggregation/mat/Help.js index 3067d1d81..e36a842cd 100644 --- a/components/aggregation/mat/Help.js +++ b/components/aggregation/mat/Help.js @@ -1,44 +1,41 @@ +import FormattedMarkdown from 'components/FormattedMarkdown' import { DetailsBox } from 'components/measurement/DetailsBox' -import { Flex, Box, Text, Heading } from 'ooni-components' -import { MdHelp } from 'react-icons/md' -import styled from 'styled-components' - import { getCategoryCodesMap } from 'components/utils/categoryCodes' -import FormattedMarkdown from 'components/FormattedMarkdown' +import { MdHelp } from 'react-icons/md' import { FormattedMessage } from 'react-intl' -const Row = styled(Flex)` - padding: 8px 0px; - /* odd/even stying - uses index prop given to Row from Array.map */ - ${props => Number(props.index) % 2 && 'background: inherit;'}; - border-bottom: 1px solid ${props => props.theme.colors.gray2}; -` - -const Name = styled(Box).attrs({ - fontWeight: 'bold' -})`` - const boxTitle = ( - +
- - +
+ +
+
) const Help = () => { return ( - - - {[...getCategoryCodesMap().values()].map(({ code, name, description }, i) => ( - - - - - ))} - + +
+ {[...getCategoryCodesMap().values()].map( + ({ code, name, description }, i) => ( +
+
+ +
+
+ +
+
+ ), + )} +
) } -export default Help \ No newline at end of file +export default Help diff --git a/components/aggregation/mat/MATContext.js b/components/aggregation/mat/MATContext.js index edd8b8712..60902e1ba 100644 --- a/components/aggregation/mat/MATContext.js +++ b/components/aggregation/mat/MATContext.js @@ -1,5 +1,11 @@ -import { createContext, useContext, useCallback, useEffect, useState } from 'react' import { useRouter } from 'next/router' +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react' export const MATStateContext = createContext() @@ -17,7 +23,11 @@ export const defaultMATContext = { tooltipIndex: [-1, ''], } -export const MATContextProvider = ({ children, queryParams, ...initialContext }) => { +export const MATContextProvider = ({ + children, + queryParams, + ...initialContext +}) => { const [state, setState] = useState({ ...defaultMATContext, ...initialContext, @@ -32,10 +42,16 @@ export const MATContextProvider = ({ children, queryParams, ...initialContext }) setState((state) => partial ? Object.assign({}, state, updates) - : Object.assign({}, state, defaultMATContext, initialContext, updates) + : Object.assign( + {}, + state, + defaultMATContext, + initialContext, + updates, + ), ) }, - [initialContext] + [initialContext], ) useEffect(() => { @@ -43,7 +59,9 @@ export const MATContextProvider = ({ children, queryParams, ...initialContext }) }, [MATquery]) return ( - + {children} ) diff --git a/components/aggregation/mat/NoCharts.js b/components/aggregation/mat/NoCharts.js index a3457c989..995dfc61a 100644 --- a/components/aggregation/mat/NoCharts.js +++ b/components/aggregation/mat/NoCharts.js @@ -1,31 +1,20 @@ -import { Flex, Box, Heading, Text } from 'ooni-components' import { FormattedMessage } from 'react-intl' -import styled from 'styled-components' - -const StyledBox = styled(Box)` -text-align: center; -background-color: ${props => props.theme.colors.gray2}; -` export const NoCharts = ({ message }) => { return ( - - +
+
- - +
+
- - {message && - - - - {message} - - - } - - - + {message && ( +
+ +
{message}
+
+ )} +
+
) } diff --git a/components/aggregation/mat/RowChart.js b/components/aggregation/mat/RowChart.js index 632f0dc60..8a03ce343 100644 --- a/components/aggregation/mat/RowChart.js +++ b/components/aggregation/mat/RowChart.js @@ -1,28 +1,27 @@ -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react' -import PropTypes from 'prop-types' -import { Box, Flex } from 'ooni-components' import { ResponsiveBar as Bar } from '@nivo/bar' import { useTooltip } from '@nivo/tooltip' - +import PropTypes from 'prop-types' +import { + createElement, + memo, + useCallback, + useEffect, + useMemo, + useState, +} from 'react' +import { useIntl } from 'react-intl' +import { getDirection } from '../../withIntl' import { CustomBarItem } from './CustomBarItem' -import { CustomToolTip, InvisibleTooltip, themeForInvisibleTooltip } from './CustomTooltip' -import { colorMap } from './colorMap' +import { + CustomToolTip, + InvisibleTooltip, + themeForInvisibleTooltip, +} from './CustomTooltip' import { useMATContext } from './MATContext' +import { colorMap } from './colorMap' import { getXAxisTicks } from './timeScaleXAxis' -import { defineMessages, useIntl } from 'react-intl' -import styled from 'styled-components' -import { getDirection } from '../../withIntl' - -const keys = [ - 'anomaly_count', - 'confirmed_count', - 'failure_count', - 'ok_count', -] -const StyledFlex = styled(Flex)` - direction: ltr; -` +const keys = ['anomaly_count', 'confirmed_count', 'failure_count', 'ok_count'] const colorFunc = (d) => colorMap[d.id] || '#ccc' @@ -33,7 +32,12 @@ const formatXAxisValues = (value, query, intl) => { if (query.axis_x === 'measurement_start_day' && Date.parse(value)) { if (query.time_grain === 'hour') { const dateTime = new Date(value) - return new Intl.DateTimeFormat(intl.locale, { dateStyle: 'short', timeStyle: 'short', timeZone: 'UTC', hourCycle: 'h23' }).format(dateTime) + return new Intl.DateTimeFormat(intl.locale, { + dateStyle: 'short', + timeStyle: 'short', + timeZone: 'UTC', + hourCycle: 'h23', + }).format(dateTime) } } else { return value @@ -44,16 +48,16 @@ const chartProps1D = (query, intl) => ({ colors: colorFunc, indexScale: { type: 'band', - round: false + round: false, }, margin: { top: 30, right: 20, bottom: 80, - left: 70 + left: 70, }, padding: 0.3, - borderColor: { from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }, + borderColor: { from: 'color', modifiers: [['darker', 1.6]] }, axisTop: null, axisRight: null, axisBottom: { @@ -63,7 +67,12 @@ const chartProps1D = (query, intl) => ({ legendPosition: 'middle', legendOffset: 70, tickValues: getXAxisTicks(query), - legend: query.axis_x ? intl.formatMessage({id: `MAT.Form.Label.AxisOption.${query.axis_x}`, defaultMessage: '' }) : '', + legend: query.axis_x + ? intl.formatMessage({ + id: `MAT.Form.Label.AxisOption.${query.axis_x}`, + defaultMessage: '', + }) + : '', format: (values) => formatXAxisValues(values, query, intl), }, axisLeft: { @@ -71,11 +80,11 @@ const chartProps1D = (query, intl) => ({ tickPadding: 5, tickRotation: 0, legendPosition: 'middle', - legendOffset: -60 + legendOffset: -60, }, labelSkipWidth: 80, labelSkipHeight: 20, - labelTextColor: { from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }, + labelTextColor: { from: 'color', modifiers: [['darker', 1.6]] }, animate: true, motionStiffness: 90, motionDamping: 15, @@ -86,14 +95,14 @@ const chartProps2D = (query) => ({ // margin: chartMargins, padding: 0.3, - borderColor: { from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }, + borderColor: { from: 'color', modifiers: [['darker', 1.6]] }, colors: colorFunc, axisTop: null, axisRight: { enable: true, tickSize: 5, tickPadding: 5, - tickValues: 2 + tickValues: 2, }, axisBottom: null, axisLeft: null, @@ -101,23 +110,29 @@ const chartProps2D = (query) => ({ enableGridY: true, indexScale: { type: 'band', - round: false + round: false, }, labelSkipWidth: 100, labelSkipHeight: 100, - labelTextColor: { from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }, + labelTextColor: { from: 'color', modifiers: [['darker', 1.6]] }, // We send the `showTooltip` boolean into the barComponent to control visibility of tooltip motionConfig: { - duration: 1 + duration: 1, }, animate: false, isInteractive: true, layers: barLayers, }) -const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last */}) => { +const RowChart = ({ + data, + indexBy, + label, + height, + rowIndex /* width, first, last */, +}) => { const intl = useIntl() - const [ query, updateMATContext ] = useMATContext() + const [query, updateMATContext] = useMATContext() const { tooltipIndex } = query const { showTooltipFromEvent, hideTooltip } = useTooltip() @@ -125,19 +140,21 @@ const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last hideTooltip() }, [hideTooltip]) - const handleClick = useCallback(({ data }) => { - const column = data[query.axis_x] - updateMATContext({ tooltipIndex: [rowIndex, column]}, true) - showTooltipFromEvent( - React.createElement(CustomToolTip, { - data: data, - onClose - }), - event, - 'top' - ) - - }, [onClose, query.axis_x, rowIndex, showTooltipFromEvent, updateMATContext]) + const handleClick = useCallback( + ({ data }) => { + const column = data[query.axis_x] + updateMATContext({ tooltipIndex: [rowIndex, column] }, true) + showTooltipFromEvent( + createElement(CustomToolTip, { + data: data, + onClose, + }), + event, + 'top', + ) + }, + [onClose, query.axis_x, rowIndex, showTooltipFromEvent, updateMATContext], + ) // Load the chart with an empty data to avoid // react-spring from working on the actual data during @@ -145,24 +162,21 @@ const RowChart = ({ data, indexBy, label, height, rowIndex /* width, first, last // real data, which appears quick enough with animation disabled const [chartData, setChartData] = useState([]) useEffect(() => { - let animation = setTimeout(() => setChartData(data), 1) + const animation = setTimeout(() => setChartData(data), 1) return () => { clearTimeout(animation) } }, [data]) - const chartProps = useMemo(() => { return label === undefined ? chartProps1D(query, intl) : chartProps2D(query) }, [intl, label, query]) return ( - - {label && - {label} - } - +
+ {label &&
{label}
} +
- - +
+
) } RowChart.propTypes = { - data: PropTypes.arrayOf(PropTypes.shape({ - anomaly_count: PropTypes.number, - confirmed_count: PropTypes.number, - failure_count: PropTypes.number, - input: PropTypes.string, - measurement_count: PropTypes.number, - measurement_start_day: PropTypes.string, - ok_count: PropTypes.number, - })), + data: PropTypes.arrayOf( + PropTypes.shape({ + anomaly_count: PropTypes.number, + confirmed_count: PropTypes.number, + failure_count: PropTypes.number, + input: PropTypes.string, + measurement_count: PropTypes.number, + measurement_start_day: PropTypes.string, + ok_count: PropTypes.number, + }), + ), height: PropTypes.number, indexBy: PropTypes.string, label: PropTypes.node, @@ -200,4 +216,4 @@ RowChart.propTypes = { RowChart.displayName = 'RowChart' -export default React.memo(RowChart) +export default memo(RowChart) diff --git a/components/aggregation/mat/StackedBarChart.js b/components/aggregation/mat/StackedBarChart.js index 01aded68b..fb7711fa3 100644 --- a/components/aggregation/mat/StackedBarChart.js +++ b/components/aggregation/mat/StackedBarChart.js @@ -1,26 +1,16 @@ -import React from 'react' import PropTypes from 'prop-types' -import { Flex } from 'ooni-components' -import styled from 'styled-components' import { useIntl } from 'react-intl' - import GridChart, { prepareDataForGridChart } from './GridChart' import { NoCharts } from './NoCharts' -const ChartContainer = styled(Flex)` - position: relative; - // border: 2px solid ${props => props.theme.colors.gray1}; - // padding: 16px; -` - export const StackedBarChart = ({ data, query }) => { const intl = useIntl() - try { - const [gridData, rows ] = prepareDataForGridChart(data, query, intl.locale) + try { + const [gridData, rows] = prepareDataForGridChart(data, query, intl.locale) return ( - +
{ height={500} noLabels={true} /> - +
) } catch (e) { - return () + return } } @@ -42,7 +32,7 @@ StackedBarChart.propTypes = { result: PropTypes.array, }), loadTime: PropTypes.number, - url: PropTypes.string + url: PropTypes.string, }), - query: PropTypes.object + query: PropTypes.object, } diff --git a/components/aggregation/mat/TableView.js b/components/aggregation/mat/TableView.js index d3da068b9..b262c5755 100644 --- a/components/aggregation/mat/TableView.js +++ b/components/aggregation/mat/TableView.js @@ -1,5 +1,4 @@ -import { Flex } from 'ooni-components' -import React, { useMemo, useRef, useState } from 'react' +import { useMemo, useRef, useState } from 'react' import { useIntl } from 'react-intl' import Filters from './Filters' @@ -7,10 +6,19 @@ import GridChart, { prepareDataForGridChart } from './GridChart' const prepareDataforTable = (data, query, locale) => { const table = [] - - const [reshapedData, rows, rowLabels] = prepareDataForGridChart(data, query, locale) - const countKeys = ['anomaly_count', 'confirmed_count', 'failure_count', 'measurement_count'] + const [reshapedData, rows, rowLabels] = prepareDataForGridChart( + data, + query, + locale, + ) + + const countKeys = [ + 'anomaly_count', + 'confirmed_count', + 'failure_count', + 'measurement_count', + ] for (const [key, rowData] of reshapedData) { const row = { [query.axis_y]: key, @@ -21,8 +29,8 @@ const prepareDataforTable = (data, query, locale) => { measurement_count: 0, } - rowData.forEach(d => { - countKeys.forEach(countKey => { + rowData.forEach((d) => { + countKeys.forEach((countKey) => { row[countKey] = row[countKey] + d[countKey] }) }) @@ -46,7 +54,7 @@ const TableView = ({ data, query, showFilters = true }) => { // to then filter out rows based on `selectedRows` generated by the table // - tableData: this has aggregated counts and labels for each row to be // displayed in GridChart. It allows to easily filter and sort aggregate data - // - indexes - + // - indexes - const [reshapedData, tableData, rowKeys, rowLabels] = useMemo(() => { try { return prepareDataforTable(data, query, intl.locale) @@ -58,22 +66,22 @@ const TableView = ({ data, query, showFilters = true }) => { const [dataForCharts, setDataForCharts] = useState(noRowsSelected) return ( - - {showFilters && +
+ {showFilters && ( - } + )} - +
) } -export default TableView \ No newline at end of file +export default TableView diff --git a/components/aggregation/mat/VirtualRows.js b/components/aggregation/mat/VirtualRows.js index fb9f2e14d..8e5a5a7f4 100644 --- a/components/aggregation/mat/VirtualRows.js +++ b/components/aggregation/mat/VirtualRows.js @@ -1,33 +1,20 @@ import PropTypes from 'prop-types' -import React, { useCallback } from 'react' -import { Flex } from 'ooni-components' -import styled from 'styled-components' +import { useCallback, useRef } from 'react' -import RowChart from './RowChart' import { defaultRangeExtractor, useVirtual } from 'react-virtual' +import RowChart from './RowChart' const GRID_ROW_CSS_SELECTOR = 'outerListElement' const ROW_HEIGHT = 70 const retainMountedRows = false -const FlexWithNoScrollbar = styled(Flex)` - width: 100%; - overflow-y: auto; -` -const StickyRow = styled.div` - position: sticky; - top: 0px; - background: white; - z-index: 1; -` - const useKeepMountedRangeExtractor = () => { - const renderedRef = React.useRef(new Set()) + const renderedRef = useRef(new Set()) - const rangeExtractor = React.useCallback(range => { + const rangeExtractor = useCallback((range) => { renderedRef.current = new Set([ ...renderedRef.current, - ...defaultRangeExtractor(range) + ...defaultRangeExtractor(range), ]) return Array.from(renderedRef.current) }, []) @@ -35,9 +22,16 @@ const useKeepMountedRangeExtractor = () => { return rangeExtractor } -export const VirtualRows = ({ data, rows, rowLabels, gridHeight, indexBy, tooltipIndex, xAxis = null }) => { - - const parentRef = React.useRef() +export const VirtualRows = ({ + data, + rows, + rowLabels, + gridHeight, + indexBy, + tooltipIndex, + xAxis = null, +}) => { + const parentRef = useRef() const keepMountedRangeExtractor = useKeepMountedRangeExtractor() const keyExtractor = useCallback((index) => rows[index], [rows]) @@ -49,13 +43,15 @@ export const VirtualRows = ({ data, rows, rowLabels, gridHeight, indexBy, toolti paddingStart: 62, // for the sticky x-axis overscan: 0, keyExtractor, - rangeExtractor: retainMountedRows ? keepMountedRangeExtractor : defaultRangeExtractor + rangeExtractor: retainMountedRows + ? keepMountedRangeExtractor + : defaultRangeExtractor, }) return ( - - {xAxis && - - {xAxis} - - } + {xAxis &&
{xAxis}
} {rowVirtualizer.virtualItems.map((virtualRow) => (
))}
-
+ ) } VirtualRows.propTypes = { diff --git a/components/aggregation/mat/XAxis.js b/components/aggregation/mat/XAxis.js index cb94c9f8c..725107f87 100644 --- a/components/aggregation/mat/XAxis.js +++ b/components/aggregation/mat/XAxis.js @@ -1,34 +1,27 @@ import { ResponsiveBar } from '@nivo/bar' -import { Box, Flex } from 'ooni-components' -import styled from 'styled-components' import { useMATContext } from './MATContext' -import { getXAxisTicks } from './timeScaleXAxis' import { useIntl } from 'react-intl' import { getDirection } from '../../withIntl' - -const StyledFlex = styled(Flex)` - direction: ltr; -` +import { getXAxisTicks } from './timeScaleXAxis' export const XAxis = ({ data }) => { const { locale } = useIntl() - const [ query ] = useMATContext() + const [query] = useMATContext() const xAxisTickValues = getXAxisTicks(query, 30) - const xAxisMargins = {right: 50, left: 0, top: 60, bottom: 0} + const xAxisMargins = { right: 50, left: 0, top: 60, bottom: 0 } const axisTop = { enable: true, tickSize: 5, tickPadding: 5, tickRotation: getDirection(locale) === 'ltr' ? -45 : 315, - tickValues: xAxisTickValues + tickValues: xAxisTickValues, } return ( - - - - +
+
+
{ padding={0.3} indexScale={{ type: 'band', - round: false + round: false, }} layers={['axes']} axisTop={axisTop} @@ -45,7 +38,7 @@ export const XAxis = ({ data }) => { axisRight={null} animate={false} /> - - +
+
) } diff --git a/components/aggregation/mat/colorMap.js b/components/aggregation/mat/colorMap.js index 37c77b172..eba8c826e 100644 --- a/components/aggregation/mat/colorMap.js +++ b/components/aggregation/mat/colorMap.js @@ -1,9 +1,9 @@ -import { theme } from 'ooni-components' +import { colors } from 'ooni-components' export const colorMap = { - 'confirmed_count': theme.colors.red7, - 'anomaly_count': theme.colors.yellow5, - 'failure_count': theme.colors.gray4, - 'ok_count': theme.colors.green5, - 'measurement_count': theme.colors.blue5, + confirmed_count: colors.red['700'], + anomaly_count: colors.yellow['500'], + failure_count: colors.gray['400'], + ok_count: colors.green['500'], + measurement_count: colors.blue['500'], } diff --git a/components/aggregation/mat/computations.js b/components/aggregation/mat/computations.js index d462a8b61..1bff2c37c 100644 --- a/components/aggregation/mat/computations.js +++ b/components/aggregation/mat/computations.js @@ -1,18 +1,18 @@ -import { getCategoryCodesMap } from '../../utils/categoryCodes' -import { getLocalisedRegionName, localisedCountries } from 'utils/i18nCountries' import dayjs from 'services/dayjs' +import { localisedCountries } from 'utils/i18nCountries' +import { getCategoryCodesMap } from '../../utils/categoryCodes' const categoryCodesMap = getCategoryCodesMap() export function getDatesBetween(startDate, endDate, timeGrain) { const dateSet = new Set() - var currentDate = startDate + let currentDate = startDate while (currentDate < endDate) { if (timeGrain === 'hour') { let startOfDay = dayjs(currentDate).utc().startOf('day') const nextDay = startOfDay.add(1, 'day') while (startOfDay.toDate() < nextDay.toDate()) { - dateSet.add(startOfDay.toISOString().split('.')[0] + 'Z') + dateSet.add(`${startOfDay.toISOString().split('.')[0]}Z`) startOfDay = startOfDay.utc().add(1, 'hours') } currentDate = dayjs(currentDate).utc().add(1, 'day') @@ -32,29 +32,35 @@ export function getDatesBetween(startDate, endDate, timeGrain) { return dateSet } -export function fillRowHoles (data, query, locale) { +export function fillRowHoles(data, query, locale) { const newData = [...data] let domain = null - switch(query.axis_x) { + switch (query.axis_x) { case 'measurement_start_day': - domain = getDatesBetween(new Date(query.since), new Date(query.until), query.time_grain) + domain = getDatesBetween( + new Date(query.since), + new Date(query.until), + query.time_grain, + ) break case 'category_code': domain = [...getCategoryCodesMap().keys()] break case 'probe_cc': - domain = localisedCountries(locale).map(cc => cc.iso3166_alpha2) + domain = localisedCountries(locale).map((cc) => cc.iso3166_alpha2) break default: - throw new Error(`x-axis: ${query.axis_x}. Please select a valid value for X-Axis.`) + throw new Error( + `x-axis: ${query.axis_x}. Please select a valid value for X-Axis.`, + ) } - const colsInRow = newData.map(i => i[query.axis_x]) - const missingCols = [...domain].filter(x => !colsInRow.includes(x)) + const colsInRow = newData.map((i) => i[query.axis_x]) + const missingCols = [...domain].filter((x) => !colsInRow.includes(x)) - const sampleDataPoint = {...newData[0]} + const sampleDataPoint = { ...newData[0] } // Add empty datapoints for columns where measurements are not available missingCols.forEach((col) => { @@ -74,19 +80,21 @@ export function fillRowHoles (data, query, locale) { return newData } -export function fillDataHoles (data, query) { +export function fillDataHoles(data, query) { // Object transformation, works like Array.map // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries#object_transformations const newData = new Map( - Object.entries(data) - .map(([ key, rowData ]) => [ key, fillRowHoles(rowData, query) ]) + Object.entries(data).map(([key, rowData]) => [ + key, + fillRowHoles(rowData, query), + ]), ) return newData } export const sortRows = (a, b, type, locale = 'en') => { - switch(type) { + switch (type) { // case 'probe_cc': // return new Intl.Collator(locale).compare(getLocalisedRegionName(a, locale), getLocalisedRegionName(b, locale)) default: diff --git a/components/aggregation/mat/labels.js b/components/aggregation/mat/labels.js index 378aff99c..748e67cdc 100644 --- a/components/aggregation/mat/labels.js +++ b/components/aggregation/mat/labels.js @@ -1,19 +1,13 @@ import PropTypes from 'prop-types' -import { Box } from 'ooni-components' - -import { testNames } from '../../test-info' -import { getCategoryCodesMap } from '../../utils/categoryCodes' import { getLocalisedRegionName } from 'utils/i18nCountries' +import { testNames } from '../../test-info' const InputRowLabel = ({ input }) => { const truncatedInput = input return ( - +
{truncatedInput} - +
) } @@ -21,14 +15,12 @@ InputRowLabel.propTypes = { input: PropTypes.string, } -const categoryCodesMap = getCategoryCodesMap() - const blockingTypeLabels = { '': '', - 'dns': 'DNS Tampering', + dns: 'DNS Tampering', 'http-diff': 'HTTP Diff', 'http-failure': 'HTTP Failure', - 'tcp_ip': 'TCP/IP Blocking' + tcp_ip: 'TCP/IP Blocking', } export const getRowLabel = (key, yAxis, locale = 'en') => { @@ -41,16 +33,16 @@ export const getRowLabel = (key, yAxis, locale = 'en') => { return messages[`CategoryCode.${key}.Name`] case 'input': case 'domain': - return () + return case 'blocking_type': return blockingTypeLabels[key] ?? key case 'probe_asn': return `AS${key}` case 'test_name': - return Object.keys(testNames).includes(key) ? - messages[testNames[key].id] : - key + return Object.keys(testNames).includes(key) + ? messages[testNames[key].id] + : key default: return key } -} \ No newline at end of file +} diff --git a/components/aggregation/mat/timeScaleXAxis.js b/components/aggregation/mat/timeScaleXAxis.js index 7c96f14ad..9f5f9a842 100644 --- a/components/aggregation/mat/timeScaleXAxis.js +++ b/components/aggregation/mat/timeScaleXAxis.js @@ -1,47 +1,56 @@ import { scaleUtc } from 'd3-scale' -import { getDatesBetween } from './computations' import dayjs from 'services/dayjs' - +import { getDatesBetween } from './computations' const defaultCount = 20 const getIntervalTicks = (data, count = defaultCount) => { - if (!(data && data.length)) return [] + if (!data?.length) return [] const start = data[0] const end = data[data.length - 1] const intervalType = 'week' const intervalCount = Math.floor(dayjs(end).diff(start, intervalType)) return data.reduce((accum, point, index) => { - const divisor = Math.ceil(intervalCount / count) - if (index % divisor === 0) - accum.push(point) - return accum + const divisor = Math.ceil(intervalCount / count) + if (index % divisor === 0) accum.push(point) + return accum }, []) } - -export function getXAxisTicks (query, count = defaultCount) { - +export function getXAxisTicks(query, count = defaultCount) { if (query.axis_x === 'measurement_start_day') { - const dateDomain = [...getDatesBetween(new Date(query.since), new Date(query.until), query.time_grain)].map(d => new Date(d)) - const xScale = scaleUtc().domain([dateDomain[0], dateDomain[dateDomain.length-1]]) - const xAxisTickValues = dateDomain.length < 30 ? dateDomain : [ - ...xScale.ticks(count), - ] - + const dateDomain = [ + ...getDatesBetween( + new Date(query.since), + new Date(query.until), + query.time_grain, + ), + ].map((d) => new Date(d)) + const xScale = scaleUtc().domain([ + dateDomain[0], + dateDomain[dateDomain.length - 1], + ]) + const xAxisTickValues = + dateDomain.length < 30 ? dateDomain : [...xScale.ticks(count)] + if (query.time_grain === 'hour') { - return Array.from(xAxisTickValues).map(d => d.toISOString().split('.')[0] + 'Z') - } else if (query.time_grain === 'week') { - return dateDomain.length < 30 ? - Array.from(dateDomain).map(d => d.toISOString().split('T')[0]) : - getIntervalTicks(dateDomain.map((d) => d.toISOString().split('T')[0]), count) + return Array.from(xAxisTickValues).map( + (d) => `${d.toISOString().split('.')[0]}Z`, + ) + } + if (query.time_grain === 'week') { + return dateDomain.length < 30 + ? Array.from(dateDomain).map((d) => d.toISOString().split('T')[0]) + : getIntervalTicks( + dateDomain.map((d) => d.toISOString().split('T')[0]), + count, + ) } - return Array.from(xAxisTickValues).map(d => d.toISOString().split('T')[0]) + return Array.from(xAxisTickValues).map((d) => d.toISOString().split('T')[0]) } return count } - diff --git a/components/as/Calendar.js b/components/as/Calendar.js index ff87375bd..d2aa8ada6 100644 --- a/components/as/Calendar.js +++ b/components/as/Calendar.js @@ -1,18 +1,18 @@ import { ResponsiveCalendar } from '@nivo/calendar' import { add, compareDesc, startOfToday, startOfYear } from 'date-fns' -import { Flex, theme } from 'ooni-components' -import React, { useState } from 'react' -import styled from 'styled-components' +import { colors } from 'ooni-components' +import { memo, useState } from 'react' import { getRange } from 'utils' -const StyledCalendar = styled.div` -height: 180px; -` -const { colors } = theme -const chartColors = [colors.blue2, colors.blue4, colors.blue5, colors.blue7] +const chartColors = [ + colors.blue['200'], + colors.blue['400'], + colors.blue['500'], + colors.blue['700'], +] -const findColor = number => { - if (number === 0) return colors.gray1 +const findColor = (number) => { + if (number === 0) return colors.gray['100'] if (number <= 50) return chartColors[0] if (number <= 500) return chartColors[1] if (number <= 5000) return chartColors[2] @@ -20,10 +20,10 @@ const findColor = number => { } const colorLegend = [ - {color: chartColors[0], range: '1-50'}, - {color: chartColors[1], range: '51-100'}, - {color: chartColors[2], range: '501-5000'}, - {color: chartColors[3], range: '>5000'}, + { color: chartColors[0], range: '1-50' }, + { color: chartColors[1], range: '51-100' }, + { color: chartColors[2], range: '501-5000' }, + { color: chartColors[3], range: '>5000' }, ] const dateRange = (startDate, endDate) => { @@ -40,74 +40,73 @@ const dateRange = (startDate, endDate) => { return dates } -const backfillData = data => { +const backfillData = (data) => { const range = dateRange(new Date(data[0].day), new Date()) - return range.map((r) => (data.find((d) => d.day === r) || { value: 0, day: r})) + return range.map((r) => data.find((d) => d.day === r) || { value: 0, day: r }) } -const Calendar = React.memo(function Calendar({data}) { +const Calendar = memo(function Calendar({ data }) { const currentYear = new Date().getFullYear() const firstMeasurementYear = Number(data[0].day.split('-')[0]) const yearsOptions = getRange(firstMeasurementYear, currentYear) - const [ selectedYear, setSelectedYear ] = useState(currentYear) + const [selectedYear, setSelectedYear] = useState(currentYear) const calendarData = backfillData(data) return ( <> - +
findColor(value)} margin={{ top: 20, right: 0, bottom: 0, left: 20 }} monthBorderColor="#ffffff" dayBorderWidth={2} dayBorderColor="#ffffff" /> - - - - {colorLegend.map(item => ( - - +
+
+
+ {colorLegend.map((item) => ( + + {item.range} ))} - - - {yearsOptions.map(year => ( +
+
+ {yearsOptions.map((year) => ( setSelectedYear(year)} > {year} ))} - - +
+
) }) -export default Calendar \ No newline at end of file +export default Calendar diff --git a/components/as/Form.js b/components/as/Form.js index 607a923cc..e5dff13e2 100644 --- a/components/as/Form.js +++ b/components/as/Form.js @@ -1,9 +1,9 @@ -import { useEffect, useRef, useState, useMemo } from 'react' -import { useForm, Controller } from 'react-hook-form' -import { Box, Flex, Input } from 'ooni-components' +import { format } from 'date-fns' +import { Input } from 'ooni-components' +import { useEffect, useRef, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' -import { format } from 'date-fns' import DateRangePicker from '../DateRangePicker' @@ -47,12 +47,12 @@ const Form = ({ onSubmit, since, until }) => { return (
- - - - +
+
+
+
( { /> )} /> - - +
+
( { /> )} /> - - +
+
{showDatePicker && ( { close={() => setShowDatePicker(false)} /> )} - - +
+
) } diff --git a/components/as/Loader.js b/components/as/Loader.js index 47f874d03..c2a2d5b39 100644 --- a/components/as/Loader.js +++ b/components/as/Loader.js @@ -1,8 +1,7 @@ -import React from 'react' import ContentLoader from 'react-content-loader' const Loader = (props) => ( - ( foregroundColor="#ecebeb" {...props} > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) -export default Loader \ No newline at end of file +export default Loader diff --git a/components/axios-plugins.js b/components/axios-plugins.js index 08b70a453..a94bad9eb 100644 --- a/components/axios-plugins.js +++ b/components/axios-plugins.js @@ -11,7 +11,6 @@ export const axiosResponseTime = (instance) => { }) } - /* https://stackoverflow.com/questions/62186171/measure-network-latency-in-react-native/62257712#62257712 const axiosTiming = (instance) => { diff --git a/components/chart/Slices.js b/components/chart/Slices.js index 45575ff81..b88139716 100644 --- a/components/chart/Slices.js +++ b/components/chart/Slices.js @@ -5,23 +5,31 @@ const SlicesItem = ({ slice, axis, debug, tooltip, isCurrent, setCurrent }) => { const { showTooltipFromEvent, hideTooltip } = useTooltip() const handleMouseEnter = useCallback( - event => { - showTooltipFromEvent(createElement(tooltip, { slice, axis }), event, 'right') + (event) => { + showTooltipFromEvent( + createElement(tooltip, { slice, axis }), + event, + 'right', + ) setCurrent(slice) }, - [showTooltipFromEvent, tooltip, slice] + [showTooltipFromEvent, tooltip, slice], ) const handleMouseMove = useCallback( - event => { - showTooltipFromEvent(createElement(tooltip, { slice, axis }), event, 'right') + (event) => { + showTooltipFromEvent( + createElement(tooltip, { slice, axis }), + event, + 'right', + ) }, - [showTooltipFromEvent, tooltip, slice] + [showTooltipFromEvent, tooltip, slice], ) const handleMouseLeave = useCallback(() => { - hideTooltip() - setCurrent(null) + hideTooltip() + setCurrent(null) }, [hideTooltip]) return ( @@ -43,11 +51,23 @@ const SlicesItem = ({ slice, axis, debug, tooltip, isCurrent, setCurrent }) => { } const Slices = (props) => { - const { width, axis, debug, height, tooltip, sliceTooltip, currentSlice, setCurrentSlice, points, enableSlices, debugSlices } = props + const { + width, + axis, + debug, + height, + tooltip, + sliceTooltip, + currentSlice, + setCurrentSlice, + points, + enableSlices, + debugSlices, + } = props const map = new Map() - points.forEach(point => { + points.forEach((point) => { if (point.data.x === null || point.data.y === null) return if (new Date(point.data.x).getMinutes() !== 0) return if (!map.has(point.x)) map.set(point.x, [point]) @@ -80,7 +100,7 @@ const Slices = (props) => { } }) - return slices.map(slice => ( + return slices.map((slice) => ( { )) } -export default Slices \ No newline at end of file +export default Slices diff --git a/components/colors.js b/components/colors.js index 653fee7c5..f39c50196 100644 --- a/components/colors.js +++ b/components/colors.js @@ -1,7 +1,7 @@ -import { theme } from 'ooni-components' +import { colors } from 'ooni-components' -export const colorNormal = theme.colors.green7 -export const colorError = theme.colors.gray4 -export const colorConfirmed = theme.colors.red8 -export const colorAnomaly = theme.colors.yellow8 -export const colorEmpty = theme.colors.gray3 +export const colorNormal = colors.green['700'] +export const colorError = colors.gray['400'] +export const colorConfirmed = colors.red['800'] +export const colorAnomaly = colors.yellow['800'] +export const colorEmpty = colors.gray['300'] diff --git a/components/country/Apps.js b/components/country/Apps.js index dcd487c8f..1fdd0419b 100644 --- a/components/country/Apps.js +++ b/components/country/Apps.js @@ -1,55 +1,68 @@ +import Chart from 'components/Chart' +import { useRouter } from 'next/router' import { useMemo } from 'react' import { FormattedMessage, useIntl } from 'react-intl' -import { Text, Box } from 'ooni-components' - +import FormattedMarkdown from '../FormattedMarkdown' import SectionHeader from './SectionHeader' import { SimpleBox } from './boxes' -import Chart from 'components/Chart' -import FormattedMarkdown from '../FormattedMarkdown' -import { useRouter } from 'next/router' -const messagingTestNames = ['signal', 'telegram', 'whatsapp', 'facebook_messenger'] +const messagingTestNames = [ + 'signal', + 'telegram', + 'whatsapp', + 'facebook_messenger', +] const circumventionTestNames = ['psiphon', 'tor', 'torsf'] const ChartsContainer = () => { const intl = useIntl() const router = useRouter() - const { query: { since, until, countryCode } } = router + const { + query: { since, until, countryCode }, + } = router - const queryMessagingApps = useMemo(() => ({ - axis_y: 'test_name', - axis_x: 'measurement_start_day', - probe_cc: countryCode, - since, - until, - time_grain: 'day', - test_name: messagingTestNames - }), [countryCode, since, until]) + const queryMessagingApps = useMemo( + () => ({ + axis_y: 'test_name', + axis_x: 'measurement_start_day', + probe_cc: countryCode, + since, + until, + time_grain: 'day', + test_name: messagingTestNames, + }), + [countryCode, since, until], + ) - const queryCircumventionTools = useMemo(() => ({ - axis_y: 'test_name', - axis_x: 'measurement_start_day', - probe_cc: countryCode, - since, - until, - time_grain: 'day', - test_name: circumventionTestNames - }), [countryCode, since, until]) + const queryCircumventionTools = useMemo( + () => ({ + axis_y: 'test_name', + axis_x: 'measurement_start_day', + probe_cc: countryCode, + since, + until, + time_grain: 'day', + test_name: circumventionTestNames, + }), + [countryCode, since, until], + ) return ( <> - +
- - +
+
- +
) } @@ -57,14 +70,14 @@ const ChartsContainer = () => { const AppsSection = () => ( <> - - + + - - - +
+ +
diff --git a/components/country/Calendar.js b/components/country/Calendar.js index a41050574..d1afd37b9 100644 --- a/components/country/Calendar.js +++ b/components/country/Calendar.js @@ -1,12 +1,11 @@ import { ResponsiveCalendar } from '@nivo/calendar' import CTABox from 'components/CallToActionBox' import SpinLoader from 'components/vendor/SpinLoader' -import { Box, Flex, theme } from 'ooni-components' +import { colors } from 'ooni-components' import React, { useMemo, useState } from 'react' import { FormattedMessage } from 'react-intl' import dayjs from 'services/dayjs' import { fetcherWithPreprocessing } from 'services/fetchers' -import { styled } from 'styled-components' import useSWR from 'swr' import { getRange } from 'utils' import FormattedMarkdown from '../FormattedMarkdown' @@ -28,25 +27,28 @@ const CallToActionBox = () => { const { countryName } = useCountry() return ( } - text={ - } /> + title={} + text={ + + } + /> ) } -const StyledCalendar = styled.div` -height: 180px; -` -const { colors } = theme -const chartColors = [colors.blue2, colors.blue4, colors.blue5, colors.blue7] +const chartColors = [ + colors.blue['200'], + colors.blue['400'], + colors.blue['500'], + colors.blue['700'], +] -const findColor = number => { - if (number === 0) return colors.gray1 +const findColor = (number) => { + if (number === 0) return colors.gray['100'] if (number <= 50) return chartColors[0] if (number <= 500) return chartColors[1] if (number <= 5000) return chartColors[2] @@ -54,16 +56,18 @@ const findColor = number => { } const colorLegend = [ - {color: chartColors[0], range: '1-50'}, - {color: chartColors[1], range: '51-100'}, - {color: chartColors[2], range: '501-5000'}, - {color: chartColors[3], range: '>5000'}, + { color: chartColors[0], range: '1-50' }, + { color: chartColors[1], range: '51-100' }, + { color: chartColors[2], range: '501-5000' }, + { color: chartColors[3], range: '>5000' }, ] const dateRange = (startDate, endDate) => { if (!startDate || !endDate) return - const start = new Date(new Date(startDate.getFullYear(), 0, 0, 0).setUTCHours(0, 0, 0, 0)) - const end = new Date(new Date(endDate).setUTCHours(0, 0, 0, 0)) + const start = new Date( + new Date(startDate.getFullYear(), 0, 0, 0).setUTCHours(0, 0, 0, 0), + ) + const end = new Date(new Date(endDate).setUTCHours(0, 0, 0, 0)) const date = new Date(start.getTime()) const dates = [] @@ -74,112 +78,108 @@ const dateRange = (startDate, endDate) => { return dates } -const backfillData = data => { +const backfillData = (data) => { const range = dateRange(new Date(data[0].day), new Date()) - return range.map((r) => (data.find((d) => d.day === r) || { value: 0, day: r})) + return range.map((r) => data.find((d) => d.day === r) || { value: 0, day: r }) } const Calendar = React.memo(function Calendar({ startYear }) { const { countryCode } = useCountry() const today = new Date() const currentYear = today.getFullYear() - const firstMeasurementYear = startYear ? new Date(startYear).getFullYear() : new Date(data[0].day).getFullYear() + const firstMeasurementYear = startYear + ? new Date(startYear).getFullYear() + : new Date(data[0].day).getFullYear() - const [ selectedYear, setSelectedYear ] = useState(currentYear) + const [selectedYear, setSelectedYear] = useState(currentYear) const since = `${selectedYear}-01-01` - const until = selectedYear === currentYear ? - dayjs.utc().add(1, 'day').format('YYYY-MM-DD') : - `${selectedYear + 1}-01-01` + const until = + selectedYear === currentYear + ? dayjs.utc().add(1, 'day').format('YYYY-MM-DD') + : `${selectedYear + 1}-01-01` const yearsOptions = getRange(firstMeasurementYear, currentYear) const { data, error, isLoading } = useSWR( [ '/api/v1/aggregation', - { params: { - probe_cc: countryCode, - since, - until, - axis_x: 'measurement_start_day', + { + params: { + probe_cc: countryCode, + since, + until, + axis_x: 'measurement_start_day', }, resultKey: 'result', preprocessFn: prepareDataForCalendar, }, ], fetcherWithPreprocessing, - swrOptions + swrOptions, ) const calendarData = useMemo(() => { - if (data && data.length) { - return backfillData(data) - } else { - return [] - } - }, - [data] - ) + if (data && data.length) { + return backfillData(data) + } else { + return [] + } + }, [data]) return ( - - {isLoading && } - {!!calendarData.length && - +
+ {isLoading && ( +
+ +
+ )} + {!!calendarData.length && ( +
findColor(value)} margin={{ top: 20, right: 0, bottom: 0, left: 20 }} monthBorderColor="#ffffff" dayBorderWidth={2} dayBorderColor="#ffffff" /> - - } +
+ )} {!calendarData.length && !isLoading && } - {error && - + {error && ( +
Error: {JSON.stringify(error)} - - } - - - {colorLegend.map(item => ( - - +
+ )} +
+
+ {colorLegend.map((item) => ( + + {item.range} ))} - - - {yearsOptions.map(year => ( +
+
+ {yearsOptions.map((year) => ( setSelectedYear(year)} > {year} ))} - - - +
+
+
) }) -export default Calendar \ No newline at end of file +export default Calendar diff --git a/components/country/ConfirmedBlockedCategory.js b/components/country/ConfirmedBlockedCategory.js index 87922d99e..7c9b18f1c 100644 --- a/components/country/ConfirmedBlockedCategory.js +++ b/components/country/ConfirmedBlockedCategory.js @@ -1,8 +1,6 @@ import { CategoryBadge } from 'components/Badge' import { DetailsBox } from 'components/measurement/DetailsBox' -import { getCategoryCodesMap } from 'components/utils/categoryCodes' import { useRouter } from 'next/router' -import { Box, Flex, Heading } from 'ooni-components' import { memo, useMemo } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import { MATFetcher } from 'services/fetchers' @@ -15,27 +13,30 @@ const swrOptions = { const ConfirmedBlockedCategory = () => { const router = useRouter() - const { query: { countryCode, since, until } } = router + const { + query: { countryCode, since, until }, + } = router const intl = useIntl() - const categoryCodeMap = getCategoryCodesMap() - - const query = useMemo(() => ({ - probe_cc: countryCode, - since, - until, - test_name: 'web_connectivity', - axis_x: 'category_code' - }), [countryCode, since, until]) + const query = useMemo( + () => ({ + probe_cc: countryCode, + since, + until, + test_name: 'web_connectivity', + axis_x: 'category_code', + }), + [countryCode, since, until], + ) const prepareDataForBadge = (categoriesData) => { - return categoriesData.filter(category => category.confirmed_count > 0) + return categoriesData.filter((category) => category.confirmed_count > 0) } const { data, error } = useSWR( since && until ? new URLSearchParams(query).toString() : null, MATFetcher, - swrOptions + swrOptions, ) const blockedCategoriesData = useMemo(() => { @@ -49,38 +50,47 @@ const ConfirmedBlockedCategory = () => { }, [data]) return ( - - {intl.formatMessage({id: 'Country.Websites.ConfirmedBlockedCategories'})} - - {(!blockedCategoriesData && !error) ? ( -
Loading ...
- ) : ( - blockedCategoriesData === null || blockedCategoriesData.length === 0 ? ( - - ) : ( - - {blockedCategoriesData && blockedCategoriesData.map(category => ( - - ))} - - ) - )} -
- {error && - -
- Error: {error.message} - - {JSON.stringify(error, null, 2)} - -
- }/> - } - -
+
+

+ {intl.formatMessage({ + id: 'Country.Websites.ConfirmedBlockedCategories', + })} +

+ <> + {!blockedCategoriesData && !error ? ( +
Loading ...
+ ) : blockedCategoriesData === null || + blockedCategoriesData.length === 0 ? ( +
+ +
+ ) : ( +
+ {blockedCategoriesData?.map((category) => ( + + ))} +
+ )} + + {error && ( + +
+ + Error: {error.message} + +
{JSON.stringify(error, null, 2)}
+
+ + } + /> + )} +
) } diff --git a/components/country/CountryContext.js b/components/country/CountryContext.js index 21c8ac17c..9b90b88e7 100644 --- a/components/country/CountryContext.js +++ b/components/country/CountryContext.js @@ -1,20 +1,22 @@ -import React, { useContext } from 'react' import PropTypes from 'prop-types' +import { createContext, useContext } from 'react' // TODO: Maybe add period information to update data in all sections when // period filter is updated in one of the sections -export const CountryContext = React.createContext() +export const CountryContext = createContext() export const CountryContextProvider = ({ countryCode, countryName, - children + children, }) => ( - + {children} ) @@ -22,7 +24,7 @@ export const CountryContextProvider = ({ CountryContextProvider.propTypes = { countryCode: PropTypes.string.isRequired, countryName: PropTypes.string.isRequired, - children: PropTypes.any + children: PropTypes.any, } /* Custom Hook to use CountryContext */ diff --git a/components/country/CountryDetails.js b/components/country/CountryDetails.js index 78613489a..2bb401bdc 100644 --- a/components/country/CountryDetails.js +++ b/components/country/CountryDetails.js @@ -10,48 +10,39 @@ import Overview from 'components/country/Overview' import PageNavMenu from 'components/country/PageNavMenu' import WebsitesSection from 'components/country/Websites' import { useRouter } from 'next/router' -import { - Box, - Container, - Flex, - Heading -} from 'ooni-components' import { useCallback, useEffect, useMemo, useState } from 'react' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' -import styled from 'styled-components' import useScrollPosition from '/hooks/useScrollPosition' import { getLocalisedRegionName } from '/utils/i18nCountries' - -const AnimatedFlex = styled(Flex)` - transition: all 0.5s ease; -` - const Header = ({ countryCode, countryName }) => { const scrollPosition = useScrollPosition() - const miniHeader = scrollPosition >= 150 ? true : false + const miniHeader = scrollPosition >= 150 return ( - - - - - - +
+ +
+

{countryName} - - - - +

+
+ +
) } -const AnimatedHeading = styled(Heading)` - transition: all 0.5s ease; -` - -const CountryDetails = ({ countryCode, overviewStats, reports, coverageDataSSR }) => { +const CountryDetails = ({ + countryCode, + overviewStats, + reports, + coverageDataSSR, +}) => { const intl = useIntl() const countryName = getLocalisedRegionName(countryCode, intl.locale) const [newData, setNewData] = useState(false) @@ -62,9 +53,13 @@ const CountryDetails = ({ countryCode, overviewStats, reports, coverageDataSSR } const today = dayjs.utc().add(1, 'day') const monthAgo = dayjs.utc(today).subtract(1, 'month') - return { - since: dayjs(query.since, 'YYYY-MM-DD', true).isValid() ? query.since : monthAgo.format('YYYY-MM-DD'), - until: dayjs(query.until, 'YYYY-MM-DD', true).isValid() ? query.until : today.format('YYYY-MM-DD') + return { + since: dayjs(query.since, 'YYYY-MM-DD', true).isValid() + ? query.since + : monthAgo.format('YYYY-MM-DD'), + until: dayjs(query.until, 'YYYY-MM-DD', true).isValid() + ? query.until + : today.format('YYYY-MM-DD'), } }, [query]) @@ -82,25 +77,28 @@ const CountryDetails = ({ countryCode, overviewStats, reports, coverageDataSSR } } }, []) - const fetchTestCoverageData = useCallback((testGroupList) => { - - const fetcher = async (testGroupList) => { - let client = axios.create({baseURL: process.env.NEXT_PUBLIC_OONI_API}) // eslint-disable-line - const result = await client.get('/api/_/test_coverage', { - params: { - 'probe_cc': countryCode, - 'test_groups': testGroupList - } - }) - // TODO: Use React.createContext to pass along data and methods - setNewData({ - networkCoverage: result.data.network_coverage, - testCoverage: result.data.test_coverage - }) - } - fetcher(testGroupList) - - }, [countryCode, setNewData]) + const fetchTestCoverageData = useCallback( + (testGroupList) => { + const fetcher = async (testGroupList) => { + const client = axios.create({ + baseURL: process.env.NEXT_PUBLIC_OONI_API, + }) // eslint-disable-line + const result = await client.get('/api/_/test_coverage', { + params: { + probe_cc: countryCode, + test_groups: testGroupList, + }, + }) + // TODO: Use React.createContext to pass along data and methods + setNewData({ + networkCoverage: result.data.network_coverage, + testCoverage: result.data.test_coverage, + }) + } + fetcher(testGroupList) + }, + [countryCode, setNewData], + ) // Sync page URL params with changes from form values const onSubmit = ({ since, until }) => { @@ -119,46 +117,53 @@ const CountryDetails = ({ countryCode, overviewStats, reports, coverageDataSSR } } } - const { testCoverage, networkCoverage } = newData !== false ? newData : coverageDataSSR + const { testCoverage, networkCoverage } = + newData !== false ? newData : coverageDataSSR return ( <> - + - +
- +
- - - - - -
- - - - +
+ + - - - + + + + + +
+
) } diff --git a/components/country/CountryHead.js b/components/country/CountryHead.js index f78e20d2e..e7ae1a211 100644 --- a/components/country/CountryHead.js +++ b/components/country/CountryHead.js @@ -1,47 +1,52 @@ -import React from 'react' import Head from 'next/head' import { useIntl } from 'react-intl' -const CountryHead = ({ - countryName, - measurementCount, - networkCount -}) => { +const CountryHead = ({ countryName, measurementCount, networkCount }) => { const intl = useIntl() return ( - {intl.formatMessage({ id: 'Country.Meta.Title'}, { countryName })} + + {intl.formatMessage({ id: 'Country.Meta.Title' }, { countryName })} + - ) } diff --git a/components/country/Overview.js b/components/country/Overview.js index 499d886ba..4ba55552c 100644 --- a/components/country/Overview.js +++ b/components/country/Overview.js @@ -1,7 +1,5 @@ import BlockText from 'components/BlockText' import Calendar from 'components/country/Calendar' -import { Box, Heading, Link, Text } from 'ooni-components' -import React from 'react' import { FormattedMessage, useIntl } from 'react-intl' import FormattedMarkdown from '../FormattedMarkdown' import { useCountry } from './CountryContext' @@ -10,12 +8,17 @@ import { BoxWithTitle } from './boxes' const ooniBlogBaseURL = 'https://ooni.org' -const FeaturedArticle = ({link, title}) => ( - - +const FeaturedArticle = ({ link, title }) => ( + ) const Overview = ({ @@ -23,7 +26,7 @@ const Overview = ({ networkCount, measurementCount, measuredSince, - featuredArticles = [] + featuredArticles = [], }) => { const intl = useIntl() const { countryCode } = useCountry() @@ -31,45 +34,45 @@ const Overview = ({ return ( <> - - + + {/* */} - + {/* */} - - - - - - - +

+ +

+
+ +
- - }> - { - (featuredArticles.length === 0) - ? - :
    - {featuredArticles.map((article, index) => ( -
  • - -
  • - ))} -
- } + } + > + {featuredArticles.length === 0 ? ( + + ) : ( +
    + {featuredArticles.map((article, index) => ( +
  • + +
  • + ))} +
+ )}
{/* Highlight Box */} diff --git a/components/country/PageNavMenu.js b/components/country/PageNavMenu.js index b6acbe68b..9dda3ca5b 100644 --- a/components/country/PageNavMenu.js +++ b/components/country/PageNavMenu.js @@ -1,40 +1,21 @@ -import React, { useState } from 'react' import PropTypes from 'prop-types' -import { Flex, Box, Link } from 'ooni-components' -import styled from 'styled-components' -import { FormattedMessage } from 'react-intl' +import { useState } from 'react' import { MdExpandLess } from 'react-icons/md' +import { FormattedMessage } from 'react-intl' import SocialButtons from '../SocialButtons' const HideInLargeScreens = ({ children }) => ( - - {children} - +
{children}
) const PageNavItem = ({ link, children }) => ( - - {children} - + ) -const ToggleIcon = styled(MdExpandLess).attrs({ - -})` - cursor: pointer; - background-color: ${props => props.theme.colors.gray3}; - border-radius: 50%; - transform: ${props => props.open ? 'rotate(0deg)': 'rotate(180deg)'}; - transition: transform 0.1s linear; -` - const PageNavMenu = ({ countryCode }) => { const [isOpen, setOpen] = useState(true) @@ -42,33 +23,41 @@ const PageNavMenu = ({ countryCode }) => { <> {/* Show a trigger to open and close the nav menu, but hide it on desktops */} - setOpen(!isOpen)} /> + setOpen(!isOpen)} + /> - - {isOpen && - - - - - - - - - - - - - } - - - - + {/* width={[1, 'unset']} */} +
+ {isOpen && ( +
+ + + + + + + + + + + + +
+ )} +
+ +
+
) } PageNavMenu.propTypes = { - countryCode: PropTypes.string + countryCode: PropTypes.string, } export default PageNavMenu diff --git a/components/country/SectionHeader.js b/components/country/SectionHeader.js index 71cbdd641..d70a0befe 100644 --- a/components/country/SectionHeader.js +++ b/components/country/SectionHeader.js @@ -1,30 +1,24 @@ -import { Flex } from 'ooni-components' -import styled from 'styled-components' +const SectionHeader = ({ children }) => ( +
+ {children} +
+) -const SectionHeader = styled(Flex)` - border-bottom: 1px solid ${props => props.theme.colors.gray3}; -` +SectionHeader.Title = ({ children, ...props }) => ( + + {children} + +) -SectionHeader.defaultProps = { - pb: 3, - my: 4, - flexWrap: 'wrap' -} - -SectionHeader.Title = styled.a` - color: ${props => props.theme.colors.blue5}; - font-size: 42px; - font-weight: 600; - /* To compenstate for the sticky navigation bar - :target selector applies only the element with id that matches - the current URL fragment (e.g '/#Overview') */ - :target::before { - content: ' '; - display: block; - width: 0; - /* NOTE: This is the combined height of the NavBar and PageNavMenu */ - height: 140px; - } -` +// /* To compenstate for the sticky navigation bar +// :target selector applies only the element with id that matches +// the current URL fragment (e.g '/#Overview') */ +// :target::before { +// content: ' '; +// display: block; +// width: 0; +// /* NOTE: This is the combined height of the NavBar and PageNavMenu */ +// height: 140px; +// } export default SectionHeader diff --git a/components/country/Tooltip.js b/components/country/Tooltip.js index fb8a2d6c5..2bf292a3e 100644 --- a/components/country/Tooltip.js +++ b/components/country/Tooltip.js @@ -1,11 +1,6 @@ -import React from 'react' +import { VictoryLabel, VictoryTooltip } from 'victory' -import { - VictoryLabel, - VictoryTooltip -} from 'victory' - -import { theme } from 'ooni-components' +import { colors } from 'ooni-components' import { firaSans } from '../../pages/_app' const Tooltip = (props) => ( @@ -14,17 +9,17 @@ const Tooltip = (props) => ( labelComponent={ } flyoutStyle={{ strokeWidth: 0, - fill: theme.colors.gray8, + fill: colors.gray['800'], padding: 2, - pointerEvents: 'none' + pointerEvents: 'none', }} /> ) diff --git a/components/country/Websites.js b/components/country/Websites.js index 48a34ffdd..1573e8db4 100644 --- a/components/country/Websites.js +++ b/components/country/Websites.js @@ -1,48 +1,49 @@ -import React, {useCallback, useMemo} from 'react' -import { useIntl, FormattedMessage } from 'react-intl' -import { Box, Text } from 'ooni-components' import ChartCountry from 'components/Chart' -import SectionHeader from './SectionHeader' -import { SimpleBox } from './boxes' +import { useRouter } from 'next/router' +import { useMemo } from 'react' +import { FormattedMessage } from 'react-intl' import FormattedMarkdown from '../FormattedMarkdown' import ConfirmedBlockedCategory from './ConfirmedBlockedCategory' -import { useRouter } from 'next/router' +import SectionHeader from './SectionHeader' +import { SimpleBox } from './boxes' const WebsitesSection = ({ countryCode }) => { const router = useRouter() - const { query: { since, until } } = router + const { + query: { since, until }, + } = router - const query = useMemo(() => ({ - axis_y: 'domain', - axis_x: 'measurement_start_day', - probe_cc: countryCode, - since, - until, - test_name: 'web_connectivity', - time_grain: 'day', - }), [countryCode, since, until]) + const query = useMemo( + () => ({ + axis_y: 'domain', + axis_x: 'measurement_start_day', + probe_cc: countryCode, + since, + until, + test_name: 'web_connectivity', + time_grain: 'day', + }), + [countryCode, since, until], + ) return ( <> - - + + - - - +
+ +
- - - +
+ +
) } -export default WebsitesSection \ No newline at end of file +export default WebsitesSection diff --git a/components/country/boxes.js b/components/country/boxes.js index 1cb59f1d6..f94949ad7 100644 --- a/components/country/boxes.js +++ b/components/country/boxes.js @@ -1,35 +1,19 @@ -import React from 'react' import PropTypes from 'prop-types' -import styled from 'styled-components' -import { Box, Container, Text } from 'ooni-components' - -const StyledBox = styled(Box)` - border: 1px solid ${props => props.theme.colors.gray4}; -` export const SimpleBox = ({ children }) => ( - - {children} - +
{children}
) -SimpleBox.propTypes = { - children: PropTypes.node -} - export const BoxWithTitle = ({ title, children }) => ( - - - {title} +
+
+
{title}
{children} - - +
+
) BoxWithTitle.propTypes = { - title: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element - ]).isRequired, - children: PropTypes.node + title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, + children: PropTypes.node, } diff --git a/components/dashboard/Charts.js b/components/dashboard/Charts.js index a47d01e31..8d60b2977 100644 --- a/components/dashboard/Charts.js +++ b/components/dashboard/Charts.js @@ -1,15 +1,16 @@ -import React, { useMemo } from 'react' -import { Flex, Box, Heading } from 'ooni-components' -import { useRouter } from 'next/router' -import useSWR from 'swr' import axios from 'axios' +import { useRouter } from 'next/router' +import { memo, useMemo } from 'react' import { useIntl } from 'react-intl' +import useSWR from 'swr' -import GridChart, { prepareDataForGridChart } from '../aggregation/mat/GridChart' +import GridChart, { + prepareDataForGridChart, +} from '../aggregation/mat/GridChart' import { MATContextProvider } from '../aggregation/mat/MATContext' import { axiosResponseTime } from '../axios-plugins' -import { testNames } from '../test-info' import { DetailsBox } from '../measurement/DetailsBox' +import { testNames } from '../test-info' const swrOptions = { revalidateOnFocus: false, @@ -22,42 +23,52 @@ axiosResponseTime(axios) // TODO export from mat.js const fetcher = (query) => { const reqUrl = `${baseURL}/api/v1/aggregation?${query}` - return axios.get(reqUrl).then(r => { - if (!r?.data?.result) { - const error = new Error(`Request ${reqUrl} did not contain expected result`) - error.data = r - throw error - } - return { - data: r.data.result, - loadTime: r.loadTime, - url: r.config.url - } - }).catch(e => { - console.log(e) - e.message = e?.request?.response ?? e.message - throw e - }) + return axios + .get(reqUrl) + .then((r) => { + if (!r?.data?.result) { + const error = new Error( + `Request ${reqUrl} did not contain expected result`, + ) + error.data = r + throw error + } + return { + data: r.data.result, + loadTime: r.loadTime, + url: r.config.url, + } + }) + .catch((e) => { + console.log(e) + e.message = e?.request?.response ?? e.message + throw e + }) } const fixedQuery = { axis_x: 'measurement_start_day', axis_y: 'probe_cc', } -const Chart = React.memo(function Chart({ testName }) { +const Chart = memo(function Chart({ testName }) { const intl = useIntl() - const { query: {probe_cc, since, until} } = useRouter() + const { + query: { probe_cc, since, until }, + } = useRouter() // Construct a `query` object that matches the router.query // used by MAT, because in this case axis_x, axis_y are not part // of router.query - const query = useMemo(() => ({ - ...fixedQuery, - test_name: testName, - since: since, - until: until, - time_grain: 'day' - }), [since, testName, until]) + const query = useMemo( + () => ({ + ...fixedQuery, + test_name: testName, + since: since, + until: until, + time_grain: 'day', + }), + [since, testName, until], + ) const apiQuery = useMemo(() => { const qs = new URLSearchParams(query).toString() @@ -65,38 +76,44 @@ const Chart = React.memo(function Chart({ testName }) { }, [query]) const { data, error } = useSWR( - () => since && until ? apiQuery : null, + () => (since && until ? apiQuery : null), fetcher, - swrOptions + swrOptions, ) - + const [chartData, rowKeys, rowLabels] = useMemo(() => { if (!data) { return [null, 0] } - let chartData = data.data.sort((a, b) => (new Intl.Collator(intl.locale).compare(a.probe_cc, b.probe_cc))) + let chartData = data.data.sort((a, b) => + new Intl.Collator(intl.locale).compare(a.probe_cc, b.probe_cc), + ) const selectedCountries = probe_cc?.length > 1 ? probe_cc.split(',') : [] if (selectedCountries.length > 0) { - chartData = chartData.filter(d => selectedCountries.includes(d.probe_cc)) + chartData = chartData.filter((d) => + selectedCountries.includes(d.probe_cc), + ) } - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart(chartData, query, intl.locale) + const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart( + chartData, + query, + intl.locale, + ) return [reshapedData, rowKeys, rowLabels] - }, [data, probe_cc, query, intl]) - const headerOptions = { probe_cc: false, subtitle: false } return ( - - {testNames[testName].name} - - {(!chartData && !error) ? ( -
{intl.formatMessage({id: 'General.Loading'})}
+
+

{testNames[testName].name}

+
+ {!chartData && !error ? ( +
{intl.formatMessage({ id: 'General.Loading' })}
) : ( )} - - {error && - -
- {intl.formatMessage({id: 'General.Error'})}: {error.message} - - {JSON.stringify(error, null, 2)} - -
- }/> - } - - +
+ {error && ( + +
+ + + {intl.formatMessage({ id: 'General.Error' })}:{' '} + {error.message} + + +
{JSON.stringify(error, null, 2)}
+
+ + } + /> + )} +
) }) const testsOnPage = ['psiphon', 'tor', 'torsf'] const ChartsContainer = () => { - return ( - testsOnPage.map(testName => ( - - - - )) - ) + return testsOnPage.map((testName) => ( + + + + )) } -export default ChartsContainer \ No newline at end of file +export default ChartsContainer diff --git a/components/dashboard/Form.js b/components/dashboard/Form.js index 9cba051d3..1024aedaf 100644 --- a/components/dashboard/Form.js +++ b/components/dashboard/Form.js @@ -1,8 +1,8 @@ +import { format } from 'date-fns' +import { Input, MultiSelect } from 'ooni-components' import { useEffect, useMemo, useState } from 'react' -import { useForm, Controller } from 'react-hook-form' -import { Box, Flex, Input, MultiSelect } from 'ooni-components' +import { Controller, useForm } from 'react-hook-form' import { useIntl } from 'react-intl' -import { format } from 'date-fns' import { getLocalisedRegionName } from '../../utils/i18nCountries' import DateRangePicker from '../DateRangePicker' @@ -17,8 +17,10 @@ export const Form = ({ onChange, query, availableCountries }) => { label: getLocalisedRegionName(cc, intl.locale), value: cc, })) - .sort((a, b) => new Intl.Collator(intl.locale).compare(a.label, b.label)), - [availableCountries, intl] + .sort((a, b) => + new Intl.Collator(intl.locale).compare(a.label, b.label), + ), + [availableCountries, intl], ) const query2formValues = useMemo(() => { @@ -26,7 +28,9 @@ export const Form = ({ onChange, query, availableCountries }) => { return { since: query?.since, until: query?.until, - probe_cc: countryOptions.filter(country => countriesInQuery.includes(country.value)), + probe_cc: countryOptions.filter((country) => + countriesInQuery.includes(country.value), + ), } }, [countryOptions, query]) @@ -41,7 +45,9 @@ export const Form = ({ onChange, query, availableCountries }) => { search: intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.SearchPlaceholder', }), - selectAll: intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.SelectAll' }), + selectAll: intl.formatMessage({ + id: 'ReachabilityDash.Form.Label.CountrySelect.SelectAll', + }), selectAllFiltered: intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.SelectAllFiltered', }), @@ -50,7 +56,7 @@ export const Form = ({ onChange, query, availableCountries }) => { }), // 'create': 'Create', }), - [intl] + [intl], ) const { control, getValues, watch, setValue, reset } = useForm({ @@ -66,7 +72,10 @@ export const Form = ({ onChange, query, availableCountries }) => { return { since, until, - probe_cc: probe_cc.length > 0 ? probe_cc.map((d) => d.value).join(',') : undefined, + probe_cc: + probe_cc.length > 0 + ? probe_cc.map((d) => d.value).join(',') + : undefined, } } @@ -88,31 +97,32 @@ export const Form = ({ onChange, query, availableCountries }) => { useEffect(() => { const subscription = watch((value, { name, type }) => { - if (name === 'probe_cc' && type === 'change') onChange(cleanedUpData(getValues())) + if (name === 'probe_cc' && type === 'change') + onChange(cleanedUpData(getValues())) }) return () => subscription.unsubscribe() }, [watch, getValues]) return ( - - +
+
( + render={({ field }) => ( )} - name='probe_cc' + name="probe_cc" control={control} /> - - - - +
+
+
+
{ /> )} /> - - +
+
{ /> )} /> - - +
+
{showDatePicker && ( setShowDatePicker(false)} /> )} - - +
+
) } diff --git a/components/dashboard/MetaTags.js b/components/dashboard/MetaTags.js index a0500050a..a8b72218f 100644 --- a/components/dashboard/MetaTags.js +++ b/components/dashboard/MetaTags.js @@ -3,25 +3,22 @@ import { useIntl } from 'react-intl' export const MetaTags = () => { const intl = useIntl() - const title = intl.formatMessage({ id: 'ReachabilityDash.Heading.CircumventionTools' }) - const description = intl.formatMessage({ id: 'ReachabilityDash.Meta.Description' }) + const title = intl.formatMessage({ + id: 'ReachabilityDash.Heading.CircumventionTools', + }) + const description = intl.formatMessage({ + id: 'ReachabilityDash.Meta.Description', + }) return ( {title} - + - + ) -} \ No newline at end of file +} diff --git a/components/domain/Form.js b/components/domain/Form.js index 6b738332e..65c051cdc 100644 --- a/components/domain/Form.js +++ b/components/domain/Form.js @@ -1,13 +1,13 @@ -import { useEffect, useState, useMemo } from 'react' -import { useForm, Controller } from 'react-hook-form' -import { Box, Flex, Input, Select } from 'ooni-components' +import { format } from 'date-fns' +import { Input, Select } from 'ooni-components' +import { useEffect, useMemo, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' -import { format } from 'date-fns' +import { useRouter } from 'next/router' import { getLocalisedRegionName } from 'utils/i18nCountries' import DateRangePicker from '../DateRangePicker' -import { useRouter } from 'next/router' const tomorrow = dayjs.utc().add(1, 'day').format('YYYY-MM-DD') const lastMonthToday = dayjs.utc().subtract(30, 'day').format('YYYY-MM-DD') @@ -61,13 +61,13 @@ const Form = ({ onSubmit, availableCountries = [] }) => { const subscription = watch((value, { name, type }) => { if ( value[name] !== query[name] && - dayjs(value['since'], 'YYYY-MM-DD', true).isValid() && - dayjs(value['until'], 'YYYY-MM-DD', true).isValid() + dayjs(value.since, 'YYYY-MM-DD', true).isValid() && + dayjs(value.until, 'YYYY-MM-DD', true).isValid() ) { onSubmit({ - since: value['since'], - until: value['until'], - probe_cc: value['probe_cc'], + since: value.since, + until: value.until, + probe_cc: value.probe_cc, }) } }) @@ -92,10 +92,10 @@ const Form = ({ onSubmit, availableCountries = [] }) => { return (
- - - - +
+
+
+
{ /> )} /> - - +
+
{ /> )} /> - - +
+
{showDatePicker && ( { close={() => setShowDatePicker(false)} /> )} - - +
+
{ )} /> - - +
+
) } diff --git a/components/findings/FindingDisplay.js b/components/findings/FindingDisplay.js index 8a7e08a7e..aea01ef9c 100644 --- a/components/findings/FindingDisplay.js +++ b/components/findings/FindingDisplay.js @@ -1,12 +1,11 @@ -import { Heading, Flex, Box, Text } from 'ooni-components' import { Badge } from 'components/Badge' import Flag from 'components/Flag' -import Markdown from 'markdown-to-jsx' import { MATChartWrapper } from 'components/MATChart' -import { getLocalisedRegionName } from 'utils/i18nCountries' +import Markdown from 'markdown-to-jsx' +import Link from 'next/link' import { useIntl } from 'react-intl' import { formatLongDate } from 'utils' -import NLink from 'next/link' +import { getLocalisedRegionName } from 'utils/i18nCountries' const FormattedMarkdown = ({ children }) => { return ( @@ -28,39 +27,59 @@ const FindingDisplay = ({ incident }) => { const intl = useIntl() const reportedBy = incident?.reported_by - const formattedCreationDate = incident?.create_time && formatLongDate(incident?.create_time, intl.locale) - const listOfNetworks = incident?.ASNs?.map((as) => ({`AS${as}`})).reduce((prev, curr) => (prev ? [prev, ', ', curr] : curr), null) + const formattedCreationDate = + incident?.create_time && formatLongDate(incident?.create_time, intl.locale) + const listOfNetworks = incident?.ASNs?.map((as) => ( + {`AS${as}`} + )).reduce((prev, curr) => (prev ? [prev, ', ', curr] : curr), null) return ( <> - - {incident?.title} - +

{incident?.title}

{!!incident?.CCs?.length && ( - +
- +

{getLocalisedRegionName(incident.CCs[0], intl.locale)} - - +

+
)} - {incident?.start_time && formatLongDate(incident?.start_time, intl.locale)} - {incident?.end_time ? formatLongDate(incident?.end_time, intl.locale) : 'ongoing'} +
+ {incident?.start_time && + formatLongDate(incident?.start_time, intl.locale)}{' '} + -{' '} + {incident?.end_time + ? formatLongDate(incident?.end_time, intl.locale) + : 'ongoing'} +
{!!incident?.tags?.length && ( - +
{incident.tags.map((tag) => ( - +
{tag} - +
))} - +
+ )} +
+ {intl.formatMessage( + { id: 'Findings.Display.CreatedByOn' }, + { reportedBy, formattedDate: formattedCreationDate }, + )} +
+ {!!incident?.ASNs?.length && ( +
+ {intl.formatMessage( + { id: 'Findings.Display.Network' }, + { listOfNetworks }, + )} +
)} - {intl.formatMessage({id: 'Findings.Display.CreatedByOn'}, {reportedBy, formattedDate: formattedCreationDate})} - {!!incident?.ASNs?.length && - - {intl.formatMessage({id: 'Findings.Display.Network'}, { listOfNetworks })} - - } - {incident?.text && {incident.text}} +
+ {incident?.text && ( + {incident.text} + )} +
) } diff --git a/components/findings/Form.js b/components/findings/Form.js index af93c25c5..b82d6aac2 100644 --- a/components/findings/Form.js +++ b/components/findings/Form.js @@ -1,18 +1,29 @@ -import { Input, Textarea, Button, Flex, Box, Checkbox, Modal, MultiSelectCreatable, MultiSelect, Text} from 'ooni-components' -import { useForm, Controller } from 'react-hook-form' -import { useIntl } from 'react-intl' -import { useCallback, useState } from 'react' -import { HtmlValidate, StaticConfigLoader, defineMetadata } from 'html-validate/browser' import { yupResolver } from '@hookform/resolvers/yup' +import { + HtmlValidate, + StaticConfigLoader, + defineMetadata, +} from 'html-validate/browser' +import { + Checkbox, + Input, + Modal, + MultiSelect, + MultiSelectCreatable, + Textarea, +} from 'ooni-components' +import { useCallback, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' +import { useIntl } from 'react-intl' import { localisedCountries } from 'utils/i18nCountries' +import * as yup from 'yup' +import useUser from '../../hooks/useUser' import FindingDisplay from './FindingDisplay' import { testNames } from '/components/test-info' -import useUser from '../../hooks/useUser' -import * as yup from 'yup' const elements = [ defineMetadata({ - 'MAT': { + MAT: { void: false, attributes: { link: { @@ -25,83 +36,94 @@ const elements = [ boolean: false, omit: false, }, - } + }, }, - }) + }), ] const loader = new StaticConfigLoader({ rules: { - 'void-style': ['error', { 'style': 'selfclose' }], + 'void-style': ['error', { style: 'selfclose' }], 'void-content': 'error', - 'element-case': ['error', {'style': 'uppercase'}], - 'element-name': ['error', {'whitelist': ['MAT']}], - 'attr-quotes': ['error', {'style': 'auto', 'unquoted': false}], + 'element-case': ['error', { style: 'uppercase' }], + 'element-name': ['error', { whitelist: ['MAT'] }], + 'attr-quotes': ['error', { style: 'auto', unquoted: false }], 'element-required-attributes': 'error', 'attribute-allowed-values': 'error', - 'attribute-boolean-style': 'error', - 'attribute-empty-style': 'error', + 'attribute-boolean-style': 'error', + 'attribute-empty-style': 'error', }, elements, }) const htmlvalidate = new HtmlValidate(loader) -const schema = yup - .object({ - title: yup.string().required(), - email_address: yup.string().required(), - reported_by: yup.string().required(), - short_description: yup.string().required(), - ASNs: yup.array().test({ - name: 'ASNsError', - message: 'Only numeric values allowed', - test: (val) => val.every((v) => !isNaN(v.value)), - }), - start_time: yup.string().required(), - end_time: yup.string().nullable().test({ +const schema = yup.object({ + title: yup.string().required(), + email_address: yup.string().required(), + reported_by: yup.string().required(), + short_description: yup.string().required(), + ASNs: yup.array().test({ + name: 'ASNsError', + message: 'Only numeric values allowed', + test: (val) => val.every((v) => !isNaN(v.value)), + }), + start_time: yup.string().required(), + end_time: yup + .string() + .nullable() + .test({ name: 'EndTimeError', message: 'Must be after start time', test: (val, testContext) => { if (val) - return new Date(testContext.parent.start_time).getTime() < new Date(val).getTime() + return ( + new Date(testContext.parent.start_time).getTime() < + new Date(val).getTime() + ) return true }, }), - text: yup.string().required().test({ + text: yup + .string() + .required() + .test({ test: async (value, context) => { const validation = await htmlvalidate.validateString(value) if (!validation.valid) { - const message = validation.results.map((obj) => obj.messages.map((m) => m.message).join(', ')).join(', ') + const message = validation.results + .map((obj) => obj.messages.map((m) => m.message).join(', ')) + .join(', ') return context.createError({ message, path: 'text' }) } else { return true } }, }), - }) +}) const Form = ({ defaultValues, onSubmit }) => { const intl = useIntl() const { user } = useUser() defaultValues = { - ...defaultValues, + ...defaultValues, CCs: defaultValues.CCs.map((cc) => { - const ccObj = localisedCountries(intl.locale).find((co) => (co.iso3166_alpha2 === cc)) + const ccObj = localisedCountries(intl.locale).find( + (co) => co.iso3166_alpha2 === cc, + ) return { - label: ccObj.localisedCountryName, - value: ccObj.iso3166_alpha2 + label: ccObj.localisedCountryName, + value: ccObj.iso3166_alpha2, } }), test_names: defaultValues.test_names.map((tn) => ({ - label: testNames[tn] ? intl.formatMessage({id: testNames[tn].id}) : tn, - value: tn - } - )), - tags: defaultValues.tags.map((t) => ({label: t, value: t})), - ASNs: defaultValues.ASNs.map((as) => ({label: as, value: as})), - domains: defaultValues.domains.map((d) => ({label: d, value: d})) + label: testNames[tn] ? intl.formatMessage({ id: testNames[tn].id }) : tn, + value: tn, + })), + tags: defaultValues.tags.map((t) => ({ label: t, value: t })), + ASNs: defaultValues.ASNs.map((as) => ({ label: as, value: as })), + domains: defaultValues.domains.map((d) => ({ label: d, value: d })), } const { handleSubmit, control, getValues, formState } = useForm({ @@ -115,12 +137,15 @@ const Form = ({ defaultValues, onSubmit }) => { const testNamesOptions = Object.entries(testNames).map(([k, v]) => ({ value: k, - label: intl.formatMessage({id: v.id}), + label: intl.formatMessage({ id: v.id }), })) const sortedCountries = localisedCountries(intl.locale) .sort((a, b) => - new Intl.Collator(intl.locale).compare(a.localisedCountryName, b.localisedCountryName) + new Intl.Collator(intl.locale).compare( + a.localisedCountryName, + b.localisedCountryName, + ), ) .map(({ iso3166_alpha2, localisedCountryName }) => ({ value: iso3166_alpha2, @@ -143,61 +168,97 @@ const Form = ({ defaultValues, onSubmit }) => { return onSubmit({ ...incident, start_time: `${incident.start_time}T00:00:00Z`, - ...(incident.end_time ? { end_time: `${incident.end_time}T00:00:00Z` } : {end_time: null}), - test_names: incident.test_names.length ? incident.test_names.map((test_name) => test_name.value) : [], + ...(incident.end_time + ? { end_time: `${incident.end_time}T00:00:00Z` } + : { end_time: null }), + test_names: incident.test_names.length + ? incident.test_names.map((test_name) => test_name.value) + : [], CCs: incident.CCs.length ? incident.CCs.map((cc) => cc.value) : [], tags: incident.tags.length ? incident.tags.map((t) => t.value) : [], - ASNs: incident.ASNs.length ? incident.ASNs.map((as) => Number(as.value)) : [], - domains: incident.domains.length ? incident.domains.map((d) => d.value) : [], + ASNs: incident.ASNs.length + ? incident.ASNs.map((as) => Number(as.value)) + : [], + domains: incident.domains.length + ? incident.domains.map((d) => d.value) + : [], }) } return ( <> setShowPreview(!showPreview)} > - +
- +
-
handleSubmit(submit)(e).catch((e) => setSubmitError(e.message))}> - {user?.role === 'admin' && - + + handleSubmit(submit)(e).catch((e) => setSubmitError(e.message)) + } + > + {user?.role === 'admin' && ( +
} + render={({ field }) => ( + + )} /> - - } +
+ )} ( - + )} /> ( - + )} /> ( - + )} /> - - +
+
{ )} /> - - +
+
{ {...field} type="date" error={errors?.end_time?.message} - label={intl.formatMessage({ id: 'Findings.Form.EndTime.Label' })} + label={intl.formatMessage({ + id: 'Findings.Form.EndTime.Label', + })} id="end_time" /> )} /> - - +
+
- - } + render={({ field }) => ( + + )} /> } + render={({ field }) => ( + + )} /> } + render={({ field }) => ( + + )} /> } + render={({ field }) => ( + + )} /> } + render={({ field }) => ( + + )} /> ( - + )} /> { name="text" render={({ field }) => (