From 9087e6d7242c555a585c15c0ebfc7ea47c0f8b12 Mon Sep 17 00:00:00 2001 From: majakomel Date: Tue, 22 Oct 2024 14:59:43 +0200 Subject: [PATCH] Add thematic pages --- components/BlockText.js | 2 +- components/Chart.js | 43 +- components/ChartWrapper.tsx | 59 + components/DomainChart.js | 2 - components/FindingsSection.js | 28 + components/Layout.js | 6 +- components/MATChart.js | 55 +- components/NavBar.js | 25 +- components/ReportsSection.js | 26 + components/SharedStyledComponents.js | 16 + components/ThematicPage.tsx | 125 + components/ThirdPartyDataChart.js | 20 +- components/VirtualizedGrid.js | 4 +- components/aggregation/mat/ChartHeader.js | 32 +- components/aggregation/mat/CustomTooltip.js | 4 +- components/aggregation/mat/GridChart.js | 4 +- components/as/Form.js | 1 + components/country/Apps.js | 4 +- components/country/CountryDetails.js | 2 + components/country/Overview.js | 6 +- components/country/PageNavMenu.js | 2 +- components/country/Websites.js | 8 +- components/country/boxes.js | 15 +- components/dashboard/Charts.js | 16 +- components/dashboard/Form.js | 241 +- components/domain/Form.js | 4 +- components/findings/Form.js | 5 +- components/landing/HighlightBox.js | 48 + components/measurement/CommonDetails.js | 6 +- components/measurement/CommonSummary.js | 4 +- components/measurement/FeedbackBox.js | 4 +- components/measurement/InfoBoxItem.js | 2 +- components/measurement/nettests/Tor.js | 2 +- components/search/FilterSidebar.js | 2 +- components/vendor/SpinLoader.js | 1 - hooks/useFilterWithSort.js | 3 +- hooks/useFindings.js | 76 + jsconfig.json | 7 - lib/api.js | 39 +- next-env.d.ts | 5 + next.config.js | 14 +- package.json | 9 +- pages/as/[probe_asn].js | 3 +- pages/chart/circumvention.js | 103 - pages/circumvention.js | 84 + pages/countries.js | 7 +- pages/country/[countryCode].js | 17 +- pages/domain/[domain].js | 3 +- pages/findings/create.js | 2 + pages/findings/index.js | 110 +- pages/human-rights.js | 107 + pages/login.js | 4 +- pages/news-media.js | 97 + pages/social-media.js | 144 + postcss.config.js | 6 - postcss.config.mjs | 9 + public/mockServiceWorker.js | 2 +- public/static/lang/en.json | 6 +- public/static/lang/translations.js | 1 - scripts/build-translations.js | 8 +- tailwind.config.js => tailwind.config.ts | 6 +- tsconfig.json | 37 + yarn.lock | 3139 +++++++++---------- 63 files changed, 2779 insertions(+), 2093 deletions(-) create mode 100644 components/ChartWrapper.tsx create mode 100644 components/FindingsSection.js create mode 100644 components/ReportsSection.js create mode 100644 components/ThematicPage.tsx create mode 100644 hooks/useFindings.js delete mode 100644 jsconfig.json create mode 100644 next-env.d.ts delete mode 100644 pages/chart/circumvention.js create mode 100644 pages/circumvention.js create mode 100644 pages/human-rights.js create mode 100644 pages/news-media.js create mode 100644 pages/social-media.js delete mode 100644 postcss.config.js create mode 100644 postcss.config.mjs delete mode 100644 public/static/lang/translations.js rename tailwind.config.js => tailwind.config.ts (75%) create mode 100644 tsconfig.json diff --git a/components/BlockText.js b/components/BlockText.js index 3e0321be..a888fbc7 100644 --- a/components/BlockText.js +++ b/components/BlockText.js @@ -1,6 +1,6 @@ const BlockText = ({ className, ...props }) => (
) diff --git a/components/Chart.js b/components/Chart.js index df7d1dd4..eadf024e 100644 --- a/components/Chart.js +++ b/components/Chart.js @@ -3,6 +3,7 @@ import GridChart, { } from 'components/aggregation/mat/GridChart' import { MATContextProvider } from 'components/aggregation/mat/MATContext' import { DetailsBox } from 'components/measurement/DetailsBox' +import SpinLoader from 'components/vendor/SpinLoader' import Link from 'next/link' import { memo, useEffect, useMemo } from 'react' import { MdBarChart, MdOutlineFileDownload } from 'react-icons/md' @@ -46,11 +47,19 @@ export const MATLink = ({ query }) => { ) } -const Chart = memo(function Chart({ - testGroup = null, - queryParams = {}, - setState, -}) { +export const ChartSpinLoader = ({ height = '300px' }) => { + return ( +
+ {/* */} + +
+ ) +} + +const Chart = ({ queryParams = {}, setState = null, headerOptions = {} }) => { const apiQuery = useMemo(() => { const qs = new URLSearchParams(queryParams).toString() return qs @@ -75,20 +84,18 @@ const Chart = memo(function Chart({ if (setState && data?.data) setState(data.data) }, [data, setState]) - const headerOptions = { probe_cc: false, subtitle: false } - return ( - //
{!chartData && !error ? ( - + ) : ( <> {!!chartData?.size && } @@ -97,20 +104,18 @@ const Chart = memo(function Chart({ -
- - Error: {error.message} - -
{JSON.stringify(error, null, 2)}
-
- +
+ + Error: {error.message} + +
{JSON.stringify(error, null, 2)}
+
} /> )}
) -}) +} -export default Chart +export default memo(Chart) diff --git a/components/ChartWrapper.tsx b/components/ChartWrapper.tsx new file mode 100644 index 00000000..be15d460 --- /dev/null +++ b/components/ChartWrapper.tsx @@ -0,0 +1,59 @@ +import Chart, { ChartSpinLoader } from 'components/Chart' +import { useRouter } from 'next/router' +import { useMemo } from 'react' +import { useInView } from 'react-intersection-observer' + +type ChartWrapperProps = { + domain?: string + testName?: string + headerOptions?: object +} + +const ChartWrapper = ({ + domain, + testName = 'web_connectivity', + headerOptions, +}: ChartWrapperProps) => { + const router = useRouter() + + const { + query: { since, until, probe_cc }, + } = router + + const query = useMemo( + () => ({ + axis_x: 'measurement_start_day', + axis_y: 'probe_cc', + since, + until, + test_name: testName, + ...(domain && { domain }), + ...(probe_cc && { probe_cc }), + time_grain: 'day', + }), + [domain, since, until, probe_cc, testName], + ) + + const { ref, inView } = useInView({ + triggerOnce: true, + rootMargin: '-300px 0px 0px 0px', + threshold: 0.5, + initialInView: false, + }) + + return ( +
+ {inView ? ( + + ) : ( + + )} +
+ ) +} + +export default ChartWrapper diff --git a/components/DomainChart.js b/components/DomainChart.js index 44c7a067..f721a4b8 100644 --- a/components/DomainChart.js +++ b/components/DomainChart.js @@ -52,11 +52,9 @@ const Chart = memo(function Chart({ queryParams = {}, setState }) { if (setState && data?.data) setState(data.data) }, [data, setState]) - const headerOptions = { probe_cc: false, subtitle: false } const linkParams = { ...queryParams, ...(probe_cc && { probe_cc }) } return ( - //
diff --git a/components/FindingsSection.js b/components/FindingsSection.js new file mode 100644 index 00000000..59756d4a --- /dev/null +++ b/components/FindingsSection.js @@ -0,0 +1,28 @@ +import { FindingBox } from 'components/landing/HighlightBox' +import { sortData } from 'hooks/useFindings' + +const FindingsSection = ({ title, findings = [] }) => { + return ( +
+

{title}

+ {findings.length ? ( + <> +
+ {findings.map((finding) => ( + + ))} +
+
+ +
+ + ) : ( +
No findings available
+ )} +
+ ) +} + +export default FindingsSection diff --git a/components/Layout.js b/components/Layout.js index 1b497263..c08068eb 100644 --- a/components/Layout.js +++ b/components/Layout.js @@ -23,6 +23,10 @@ const Layout = ({ children }) => { return ( pathname === '/countries' || pathname === '/domains' || + pathname === '/human-rights' || + pathname === '/social-media' || + pathname === '/news-media' || + pathname === '/circumvention' || pathname === '/networks' || pathname === '/findings' || pathname.match(/^\/country\/\S+/) @@ -31,7 +35,7 @@ const Layout = ({ children }) => { return ( -
+
{navbarSticky ? ( diff --git a/components/MATChart.js b/components/MATChart.js index 1482c5af..0c2c29f5 100644 --- a/components/MATChart.js +++ b/components/MATChart.js @@ -9,6 +9,7 @@ import { useMemo } from 'react' import { useIntl } from 'react-intl' import dayjs from 'services/dayjs' import useSWR from 'swr' +import { ChartSpinLoader } from './Chart' import { FormattedMarkdownBase } from './FormattedMarkdown' axiosResponseTime(axios) @@ -87,37 +88,37 @@ const MATChart = ({ query, showFilters = true }) => { ) const showLoadingIndicator = useMemo(() => isValidating, [isValidating]) + return ( <> {error && } - <> - {showLoadingIndicator ? ( -

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

- ) : ( - <> - {data?.data?.result?.length > 0 ? ( - <> - {data && data.data.dimension_count === 0 && ( - - )} - {data && data.data.dimension_count === 1 && ( - - )} - {data && data.data.dimension_count > 1 && ( - - )} - - ) : ( - - )} - - )} - + {showLoadingIndicator ? ( + //

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

+ + ) : ( + <> + {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 a459e0f8..a85f82d2 100644 --- a/components/NavBar.js +++ b/components/NavBar.js @@ -83,8 +83,23 @@ const SubMenu = () => { const menuItem = [ { - label: , - href: '/chart/circumvention', + label: , + href: '/social-media', + umami: 'navigation-social-media', + }, + { + label: , + href: '/news-media', + umami: 'navigation-news-media', + }, + // { + // label: , + // href: '/human-rights', + // umami: 'navigation-human-rights', + // }, + { + label: , + href: '/circumvention', umami: 'navigation-circumvention', }, { @@ -170,7 +185,7 @@ export const NavBar = ({ color }) => { onClick={() => setShowMenu(!showMenu)} />
{showMenu && (
@@ -182,7 +197,7 @@ export const NavBar = ({ color }) => {
)}
a]:border-black [&>a]:hover:border-black [&>*]:opacity-100 [&>*]:text-black [&>*]:hover:text-black'}`} + className={`flex gap-4 lg:gap-8 text-sm ${showMenu && 'pt-2 flex-col items-start [&>a]:border-black [&>a]:hover:border-black [&>*]:opacity-100 [&>*]:text-black [&>*]:hover:text-black'}`} > } @@ -191,7 +206,7 @@ export const NavBar = ({ color }) => { /> } - href="/chart/circumvention" + href="/circumvention" data-umami-event="navigation-censorship" /> { + return ( + + {/*
*/} + {/*

{title}

*/} + {reports?.length === 0 ? ( + + ) : ( +
    + {reports?.map((article, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: +
  • + +
  • + ))} +
+ )} + + ) +} + +export default ReportsSection diff --git a/components/SharedStyledComponents.js b/components/SharedStyledComponents.js index ef9f2e75..39bf2429 100644 --- a/components/SharedStyledComponents.js +++ b/components/SharedStyledComponents.js @@ -16,6 +16,22 @@ export const StyledStickySubMenu = ({ topClass, ...props }) => ( /> ) +export const StickySubMenuUpdated = ({ title, menu, topClass, ...props }) => { + return ( +
+
+

{title}

+
+
{menu}
+
+
+
+ ) +} + export const StickySubMenu = ({ title, children, topClass }) => { return ( diff --git a/components/ThematicPage.tsx b/components/ThematicPage.tsx new file mode 100644 index 00000000..9d8648be --- /dev/null +++ b/components/ThematicPage.tsx @@ -0,0 +1,125 @@ +import { useRouter } from 'next/router' +import { FormattedMessage, useIntl } from 'react-intl' + +import ChartWrapper from 'components/ChartWrapper' +import FindingsSection from 'components/FindingsSection' +import FormattedMarkdown from 'components/FormattedMarkdown' +import ReportsSection from 'components/ReportsSection' +import { + StickySubMenuUpdated, + StyledStickySubMenu, +} from 'components/SharedStyledComponents' +import { Form } from 'components/dashboard/Form' +import { MetaTags } from 'components/dashboard/MetaTags' +import { useMemo, useState } from 'react' + +// type ThematicPageProps = { +// countries: string[], +// reports: string[], +// } + +export const AnchorLink = ({ id }) => ( +
+) + +const AnchorLinkLower = ({ id }) => ( +
+) + +const ThematicPage = ({ + countries, + reports, + findings, + selectedCountries, + domains, + apps, + title, + findingsTitle, + reportsTitle, + text, +}) => { + // const sortedDomains = useMemo( + // () => + // domains.sort((a, b) => { + // return a.replace('www.', '').localeCompare(b.replace('www.', '')) + // }), + // [domains], + // ) + const { query } = useRouter() + // const intl = useIntl() + const [filteredDomains, setFilteredDomains] = useState(domains) + const [filteredApps, setFilteredApps] = useState(apps) + + return ( + <> + +
+ + Findings + Reports + Circumvention Tools + Websites + + } + /> + + + + + + +
{text}
+ +
+
+
+
+ {Object.keys(query).length > 0 && ( + <> + {apps?.length && ( +
+ +

Apps

+ {filteredApps?.map((testName: string) => ( +
+ +
+ ))} +
+ )} +
+ +

Websites

+ {filteredDomains?.map((domain: string) => ( +
+ + +
+ ))} +
+ + )} +
+ + ) +} + +export default ThematicPage diff --git a/components/ThirdPartyDataChart.js b/components/ThirdPartyDataChart.js index 5d4576cf..f1d986ba 100644 --- a/components/ThirdPartyDataChart.js +++ b/components/ThirdPartyDataChart.js @@ -114,15 +114,13 @@ const ThirdPartyDataChart = ({ since, until, country, asn, ...props }) => { -
- -
+
) : ( @@ -217,9 +215,7 @@ const ThirdPartyDataChart = ({ since, until, country, asn, ...props }) => { ]} /> )} - {error && ( -
Unable to retrieve the data
- )} + {error &&
Unable to retrieve the data
}
) diff --git a/components/VirtualizedGrid.js b/components/VirtualizedGrid.js index f35c305b..6889f168 100644 --- a/components/VirtualizedGrid.js +++ b/components/VirtualizedGrid.js @@ -14,9 +14,7 @@ export const GridBox = ({ return (
-
- {title} -
+
{title}
{(hasCount || tag) && ( <> diff --git a/components/aggregation/mat/ChartHeader.js b/components/aggregation/mat/ChartHeader.js index af095fe5..97b8a60e 100644 --- a/components/aggregation/mat/ChartHeader.js +++ b/components/aggregation/mat/ChartHeader.js @@ -25,9 +25,10 @@ const getTestNameGroupName = (testNameArray) => { return [testNames[testNameArray[0]]?.id || testNameArray[0]] const testGroup = new Set() - testNameArray.forEach((t) => { + for (const t of testNameArray) { testNames[t]?.group && testGroup.add(testNames[t]?.group) - }) + } + // if all test names belong to a single group, show group name if (testGroup.size === 1) { const testGroupName = testGroups[[...testGroup][0]].id @@ -37,18 +38,18 @@ const getTestNameGroupName = (testNameArray) => { return testNameArray.map((t) => testNames[t].id) } -export const SubtitleStr = ({ query }) => { +export const SubtitleStr = ({ query, options }) => { const intl = useIntl() const params = new Set() - if (query.test_name) { + if (options.test_name && query.test_name) { const testNameQuery = Array.isArray(query.test_name) ? query.test_name : [query.test_name] const testNames = getTestNameGroupName(testNameQuery) - testNames.forEach((testName) => { + for (const testName of testNames) { params.add(intl.formatMessage({ id: testName, defaultMessage: '' })) - }) + } } if (query.domain) { params.add(query.domain.split(',').join(', ')) @@ -75,27 +76,34 @@ export const SubtitleStr = ({ query }) => { * @param {Object} options - Object with flags for header components eg. { probe_cc: false } * @param {boolean} options.probe_cc - Show/hide country name */ -export const ChartHeader = ({ options = {} }) => { +export const ChartHeader = ({ options: opts }) => { const intl = useIntl() const [query] = useMATContext() + const options = { + subtitle: true, + probe_cc: true, + test_name: true, + legend: true, + ...opts, + } - const subTitle = + const subTitle = return ( <>
- {options.subtitle !== false && ( + {options.subtitle && ( {subTitle} )} - {options.probe_cc !== false && query.probe_cc && ( - + {options.probe_cc && query.probe_cc && ( + )}
- {options.legend !== false && ( + {options.legend && (
{ return (
-
+
{derivedTitle}
{dataKeysToShow.map((k) => ( -
+
diff --git a/components/aggregation/mat/GridChart.js b/components/aggregation/mat/GridChart.js index 543fc6b9..27067f13 100644 --- a/components/aggregation/mat/GridChart.js +++ b/components/aggregation/mat/GridChart.js @@ -43,7 +43,7 @@ export const prepareDataForGridChart = (data, query, locale) => { const rowLabels = {} const reshapedData = {} - data.forEach((item) => { + for (const item of data) { // Convert non-string keys (e.g `probe_asn`) to string // because they get casted to strings during Object transformations const key = String(item[query.axis_y]) @@ -54,7 +54,7 @@ export const prepareDataForGridChart = (data, query, locale) => { reshapedData[key] = [item] rowLabels[key] = getRowLabel(key, query.axis_y, locale) } - }) + } const reshapedDataWithoutHoles = fillDataHoles(reshapedData, query) diff --git a/components/as/Form.js b/components/as/Form.js index e5dff13e..90325a8d 100644 --- a/components/as/Form.js +++ b/components/as/Form.js @@ -32,6 +32,7 @@ const Form = ({ onSubmit, since, until }) => { setShowDatePicker(false) } + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { // trigger submit only when the dates are valid if ( diff --git a/components/country/Apps.js b/components/country/Apps.js index 1fdd0419..a3acc273 100644 --- a/components/country/Apps.js +++ b/components/country/Apps.js @@ -75,9 +75,7 @@ const AppsSection = () => ( -
- -
+
diff --git a/components/country/CountryDetails.js b/components/country/CountryDetails.js index 2bb401bd..a642c4b9 100644 --- a/components/country/CountryDetails.js +++ b/components/country/CountryDetails.js @@ -63,6 +63,7 @@ const CountryDetails = ({ } }, [query]) + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { if (query.since !== since || query.until !== until) { const href = { @@ -77,6 +78,7 @@ const CountryDetails = ({ } }, []) + // biome-ignore lint/correctness/useExhaustiveDependencies: const fetchTestCoverageData = useCallback( (testGroupList) => { const fetcher = async (testGroupList) => { diff --git a/components/country/Overview.js b/components/country/Overview.js index 5bfeefe4..7f8a6dfc 100644 --- a/components/country/Overview.js +++ b/components/country/Overview.js @@ -8,7 +8,7 @@ import { BoxWithTitle } from './boxes' const ooniBlogBaseURL = 'https://ooni.org' -const FeaturedArticle = ({ link, title }) => ( +export const FeaturedArticle = ({ link, title }) => (
-
- -
+ } diff --git a/components/country/PageNavMenu.js b/components/country/PageNavMenu.js index 9dda3ca5..68e5b9c8 100644 --- a/components/country/PageNavMenu.js +++ b/components/country/PageNavMenu.js @@ -10,7 +10,7 @@ const HideInLargeScreens = ({ children }) => ( const PageNavItem = ({ link, children }) => (
diff --git a/components/country/Websites.js b/components/country/Websites.js index 1573e8db..e0677797 100644 --- a/components/country/Websites.js +++ b/components/country/Websites.js @@ -1,4 +1,4 @@ -import ChartCountry from 'components/Chart' +import Chart from 'components/Chart' import { useRouter } from 'next/router' import { useMemo } from 'react' import { FormattedMessage } from 'react-intl' @@ -34,13 +34,11 @@ const WebsitesSection = ({ countryCode }) => { -
- -
+
- +
) diff --git a/components/country/boxes.js b/components/country/boxes.js index f94949ad..209d8959 100644 --- a/components/country/boxes.js +++ b/components/country/boxes.js @@ -1,16 +1,17 @@ import PropTypes from 'prop-types' +import { twMerge } from 'tailwind-merge' export const SimpleBox = ({ children }) => (
{children}
) -export const BoxWithTitle = ({ title, children }) => ( -
-
-
{title}
- {children} -
-
+export const BoxWithTitle = ({ title, children, className }) => ( +
+

{title}

+ {children} +
) BoxWithTitle.propTypes = { diff --git a/components/dashboard/Charts.js b/components/dashboard/Charts.js index 8d60b297..c6812cd5 100644 --- a/components/dashboard/Charts.js +++ b/components/dashboard/Charts.js @@ -63,11 +63,12 @@ const Chart = memo(function Chart({ testName }) { () => ({ ...fixedQuery, test_name: testName, - since: since, - until: until, + since, + until, + ...(probe_cc && { probe_cc }), time_grain: 'day', }), - [since, testName, until], + [since, testName, until, probe_cc], ) const apiQuery = useMemo(() => { @@ -86,17 +87,10 @@ const Chart = memo(function Chart({ testName }) { return [null, 0] } - let chartData = data.data.sort((a, b) => + const 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), - ) - } - const [reshapedData, rowKeys, rowLabels] = prepareDataForGridChart( chartData, query, diff --git a/components/dashboard/Form.js b/components/dashboard/Form.js index 1024aeda..60ee3234 100644 --- a/components/dashboard/Form.js +++ b/components/dashboard/Form.js @@ -1,30 +1,103 @@ import { format } from 'date-fns' +import dayjs from 'dayjs' +import { useRouter } from 'next/router' import { Input, MultiSelect } from 'ooni-components' -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { Controller, useForm } from 'react-hook-form' import { useIntl } from 'react-intl' -import { getLocalisedRegionName } from '../../utils/i18nCountries' -import DateRangePicker from '../DateRangePicker' +import DateRangePicker from 'components/DateRangePicker' +import { getLocalisedRegionName } from 'utils/i18nCountries' -export const Form = ({ onChange, query, availableCountries }) => { +const cleanedUpData = (values) => { + const { since, until, probe_cc } = values + return { + since, + until, + probe_cc: + probe_cc.length > 0 ? probe_cc.map((d) => d.value).join(',') : undefined, + } +} + +export const Form = ({ + countries = [], + selectedCountries = ['CN', 'IR', 'RU'], + domains, + apps, + setDomains, + setApps, +}) => { const intl = useIntl() + const router = useRouter() + const { query } = router + + const domainOptions = useMemo(() => { + return domains.map((d) => ({ label: d, value: d })) + }, [domains]) + + const appOptions = useMemo(() => { + return apps?.map((a) => ({ label: a, value: a })) + }, [apps]) + + // initial placement of query params when they are not defined + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + console.log('query---', query) + const tomorrow = dayjs.utc().add(1, 'day').format('YYYY-MM-DD') + const monthAgo = dayjs.utc().subtract(30, 'day').format('YYYY-MM-DD') + const probe_cc = selectedCountries.join(',') + const href = { + query: { + since: monthAgo, + until: tomorrow, + probe_cc, + ...query, // override default query params + }, + } + router.replace(href, undefined, { shallow: true }) + }, []) + + console.log('query', query) + + // Sync page URL params with changes from form values + const onChange = useCallback( + ({ since, until, probe_cc }) => { + // since: "2022-01-02", + // until: "2022-02-01", + // probe_cc: "IT,AL,IR" + const params = { + since, + until, + } + if (probe_cc) { + params.probe_cc = probe_cc + } + if ( + query.since !== since || + query.until !== until || + query.probe_cc !== probe_cc + ) { + router.push({ query: params }, undefined, { shallow: true }) + } + }, + [router, query], + ) const countryOptions = useMemo( () => - availableCountries + countries .map((cc) => ({ - label: getLocalisedRegionName(cc, intl.locale), - value: cc, + label: getLocalisedRegionName(cc.alpha_2, intl.locale), + value: cc.alpha_2, })) .sort((a, b) => new Intl.Collator(intl.locale).compare(a.label, b.label), ), - [availableCountries, intl], + [countries, intl], ) const query2formValues = useMemo(() => { - const countriesInQuery = query.probe_cc?.split(',') ?? '' + const countriesInQuery = query?.probe_cc?.split(',') ?? '' return { since: query?.since, until: query?.until, @@ -39,9 +112,6 @@ export const Form = ({ onChange, query, availableCountries }) => { allItemsAreSelected: intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.AllSelected', }), - // 'clearSearch': 'Clear Search', - // 'clearSelected': 'Clear Selected', - // 'noOptions': 'No options', search: intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.SearchPlaceholder', }), @@ -54,7 +124,6 @@ export const Form = ({ onChange, query, availableCountries }) => { selectSomeItems: intl.formatMessage({ id: 'ReachabilityDash.Form.Label.CountrySelect.InputPlaceholder', }), - // 'create': 'Create', }), [intl], ) @@ -67,18 +136,6 @@ export const Form = ({ onChange, query, availableCountries }) => { reset(query2formValues) }, [query2formValues, reset]) - const cleanedUpData = (values) => { - const { since, until, probe_cc } = values - return { - since, - until, - probe_cc: - probe_cc.length > 0 - ? probe_cc.map((d) => d.value).join(',') - : undefined, - } - } - const [showDatePicker, setShowDatePicker] = useState(false) const handleRangeSelect = (range) => { if (range?.from) { @@ -95,67 +152,52 @@ export const Form = ({ onChange, query, availableCountries }) => { onChange(cleanedUpData(getValues())) } + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { - const subscription = watch((value, { name, type }) => { + const subscription = watch((_, { name, type }) => { if (name === 'probe_cc' && type === 'change') onChange(cleanedUpData(getValues())) + if (name === 'domains' && type === 'change') + getValues('domains').length + ? setDomains(getValues('domains').map((d) => d.value)) + : setDomains(domains) + if (name === 'apps' && type === 'change') + getValues('apps').length + ? setApps(getValues('apps').map((a) => a.value)) + : setApps(apps) }) return () => subscription.unsubscribe() - }, [watch, getValues]) + }, [watch, getValues, onChange]) return ( -
-
- ( - - )} - name="probe_cc" - control={control} - /> -
-
-
-
- ( - setShowDatePicker(true)} - onKeyDown={() => setShowDatePicker(false)} - name={field.name} - value={field.value} - onChange={field.onChange} - /> - )} - /> -
-
- ( - setShowDatePicker(true)} - onKeyDown={() => setShowDatePicker(false)} - name={field.name} - value={field.value} - onChange={field.onChange} - /> - )} - /> -
+
+
+
+ ( + setShowDatePicker(true)} + onKeyDown={() => setShowDatePicker(false)} + /> + )} + /> + ( + setShowDatePicker(true)} + onKeyDown={() => setShowDatePicker(false)} + /> + )} + />
{showDatePicker && ( { /> )}
+ ( + + )} + name="probe_cc" + control={control} + /> + {domains && ( + ( + + )} + name="domains" + control={control} + /> + )} + {apps && ( + ( + + )} + name="apps" + control={control} + /> + )}
) diff --git a/components/domain/Form.js b/components/domain/Form.js index 65c051cd..3d784694 100644 --- a/components/domain/Form.js +++ b/components/domain/Form.js @@ -35,7 +35,7 @@ const Form = ({ onSubmit, availableCountries = [] }) => { return { since: query?.since ?? defaultDefaultValues.since, until: query?.until ?? defaultDefaultValues.until, - probe_cc: defaultDefaultValues.probe_cc, + probe_cc: query?.probe_cc ?? defaultDefaultValues.probe_cc, } }, [query]) @@ -58,7 +58,7 @@ const Form = ({ onSubmit, availableCountries = [] }) => { } useEffect(() => { - const subscription = watch((value, { name, type }) => { + const subscription = watch((value, { name }) => { if ( value[name] !== query[name] && dayjs(value.since, 'YYYY-MM-DD', true).isValid() && diff --git a/components/findings/Form.js b/components/findings/Form.js index 0682adb4..bf74e956 100644 --- a/components/findings/Form.js +++ b/components/findings/Form.js @@ -66,7 +66,7 @@ const schema = yup.object({ ASNs: yup.array().test({ name: 'ASNsError', message: 'Only numeric values allowed', - test: (val) => val.every((v) => !isNaN(v.value)), + test: (val) => val.every((v) => !Number.isNaN(v.value)), }), start_time: yup.string().required(), end_time: yup @@ -95,9 +95,8 @@ const schema = yup.object({ .map((obj) => obj.messages.map((m) => m.message).join(', ')) .join(', ') return context.createError({ message, path: 'text' }) - } else { - return true } + return true }, }), }) diff --git a/components/landing/HighlightBox.js b/components/landing/HighlightBox.js index 9372979a..3233a081 100644 --- a/components/landing/HighlightBox.js +++ b/components/landing/HighlightBox.js @@ -3,6 +3,8 @@ import PropTypes from 'prop-types' import { useIntl } from 'react-intl' import { getLocalisedRegionName } from 'utils/i18nCountries' +import Link from 'next/link' +import { formatLongDate } from '../../utils' import Flag from '../Flag' const HighlightBox = ({ countryCode, title, text, dates, footer }) => { @@ -40,3 +42,49 @@ HighlightBox.propTypes = { } export default HighlightBox + +export const FindingBox = ({ incident }) => { + const intl = useIntl() + + return ( + +
+ {incident.start_time && + formatLongDate(incident.start_time, intl.locale)}{' '} + -{' '} + {incident.end_time + ? formatLongDate(incident.end_time, intl.locale) + : 'ongoing'} +
+
+ {intl.formatMessage( + { id: 'Findings.Index.HighLightBox.CreatedOn' }, + { + date: + incident?.create_time && + formatLongDate(incident?.create_time, intl.locale), + }, + )} +
+
+ } + footer={ +
+ + + +
+ } + /> + ) +} diff --git a/components/measurement/CommonDetails.js b/components/measurement/CommonDetails.js index 52fb7916..51d4f95d 100644 --- a/components/measurement/CommonDetails.js +++ b/components/measurement/CommonDetails.js @@ -8,11 +8,7 @@ import { FormattedMessage, useIntl } from 'react-intl' import { DetailsBox, DetailsBoxTable } from './DetailsBox' const LoadingRawData = () => { - return ( -
- -
- ) + return } const ReactJson = dynamic(() => import('react-json-view'), { diff --git a/components/measurement/CommonSummary.js b/components/measurement/CommonSummary.js index 21fc9b99..aca0cac7 100644 --- a/components/measurement/CommonSummary.js +++ b/components/measurement/CommonSummary.js @@ -32,7 +32,7 @@ const CommonSummary = ({ >
-
{formattedDate}
+
{formattedDate}
{network} {networkName} diff --git a/components/measurement/FeedbackBox.js b/components/measurement/FeedbackBox.js index 92ae39c7..522c948a 100644 --- a/components/measurement/FeedbackBox.js +++ b/components/measurement/FeedbackBox.js @@ -138,9 +138,9 @@ const FeedbackBox = ({ const submitEnabled = useMemo(() => !!firstLevelRadio, [firstLevelRadio]) return ( -
+
setShowModal(false)} /> <> diff --git a/components/measurement/InfoBoxItem.js b/components/measurement/InfoBoxItem.js index 68f399b8..2ad6e04c 100644 --- a/components/measurement/InfoBoxItem.js +++ b/components/measurement/InfoBoxItem.js @@ -6,7 +6,7 @@ export const InfoBoxItem = ({ label, content, unit }) => (
{content} {unit && {unit}}
-
{label}
+
{label}
) diff --git a/components/measurement/nettests/Tor.js b/components/measurement/nettests/Tor.js index 54d6c34f..1182ac2d 100644 --- a/components/measurement/nettests/Tor.js +++ b/components/measurement/nettests/Tor.js @@ -96,7 +96,7 @@ const Table = ({ columns, data }) => { const ConnectionStatusCell = ({ cell: { value } }) => { let statusIcon = null if (value === false) { - statusIcon =
N/A
+ statusIcon =
N/A
} else { statusIcon = value === null ? ( diff --git a/components/search/FilterSidebar.js b/components/search/FilterSidebar.js index e04b5a94..1df52c4f 100644 --- a/components/search/FilterSidebar.js +++ b/components/search/FilterSidebar.js @@ -387,7 +387,7 @@ const FilterSidebar = ({ {(showConfirmedFilter || showAnomalyFilter) && ( <> -