Skip to content

Commit

Permalink
Incident reporting platform (#862)
Browse files Browse the repository at this point in the history
  • Loading branch information
majakomel authored Oct 18, 2023
1 parent a3cdee6 commit 022498c
Show file tree
Hide file tree
Showing 48 changed files with 1,638 additions and 705 deletions.
2 changes: 1 addition & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# To override locally, make a copy called `.env.development.local`
# Refer: https://nextjs.org/docs/basic-features/environment-variables

NEXT_PUBLIC_OONI_API=https://api.ooni.io
NEXT_PUBLIC_OONI_API=https://ams-pg-test.ooni.org
NEXT_PUBLIC_USER_FEEDBACK_API=https://ams-pg-test.ooni.org
NEXT_PUBLIC_EXPLORER_URL=http://localhost:3100

Expand Down
12 changes: 7 additions & 5 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"root": true,
"plugins": ["cypress"],
"extends": [
"next/core-web-vitals",
"plugin:cypress/recommended"
],
"extends": ["next/core-web-vitals", "plugin:cypress/recommended", "prettier"],
"rules": {
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "never"]
},
"ignorePatterns": ["components/vendor", "static"]
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"ignorePatterns": ["node_modules", "components/vendor", "static"]
}
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"semi": false,
"printWidth": 100
}
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"prettier.singleQuote": false,
"prettier.semi": false,
"prettier.singleQuote": true
"eslint.validate": ["javascript"]
}
24 changes: 11 additions & 13 deletions components/Badge.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,28 @@ import { getTestMetadata } from './utils'
import * as icons from 'ooni-components/icons'

// XXX replace what is inside of search/results-list.StyledResultTag
const Badge = styled(Box)`
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};
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;
`

const TestGroupBadge = ({ testName, ...props }) => {
const {icon, groupName, color} = getTestMetadata(testName)
const { icon, groupName, color } = getTestMetadata(testName)

return (
<Badge bg={color} color='white' {...props}>
<Flex sx={{gap: 1}} alignItems='center'>
<Text>
{groupName}
</Text>
{cloneElement(icon, {size: 12})}
<Badge bg={color} color="white" {...props}>
<Flex sx={{ gap: 1 }} alignItems="center">
<Text>{groupName}</Text>
{cloneElement(icon, { size: 12 })}
</Flex>
</Badge>
)
Expand All @@ -45,8 +43,8 @@ export const CategoryBadge = ({ categoryCode }) => {
}

return (
<Badge bg='#ffffff' borderColor={theme.colors.gray5} color={theme.colors.gray7}>
<Flex sx={{gap: 1}} alignItems='center'>
<Badge bg="#ffffff" borderColor={theme.colors.gray5} color={theme.colors.gray7}>
<Flex sx={{ gap: 1 }} alignItems="center">
<Box>
<FormattedMessage id={`CategoryCode.${categoryCode}.Name`} />
</Box>
Expand Down
16 changes: 16 additions & 0 deletions components/ButtonSpinner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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 = () => <StyledSpinner />

export default ButtonSpinner
1 change: 0 additions & 1 deletion components/Chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, set
data={chartData}
rowKeys={rowKeys}
rowLabels={rowLabels}
isGrouped={false}
/>
{!!chartData?.size && <MATLink query={queryParams} />}
</>
Expand Down
1 change: 0 additions & 1 deletion components/DomainChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ const Chart = React.memo(function Chart({testGroup = null, queryParams = {}, set
data={chartData}
rowKeys={rowKeys}
rowLabels={rowLabels}
isGrouped={false}
/>
{!!chartData?.size && <MATLink query={linkParams} />}
</>
Expand Down
15 changes: 12 additions & 3 deletions components/FormattedMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { useIntl } from 'react-intl'
import Markdown from 'markdown-to-jsx'
import { Link, theme } from 'ooni-components'

const FormattedMarkdown = ({ id, defaultMessage, values }) => {
const intl = useIntl()
export const FormattedMarkdownBase = ({ children }) => {
return (
<Markdown
options={{
Expand All @@ -21,11 +20,21 @@ const FormattedMarkdown = ({ id, defaultMessage, values }) => {
}
}}
>
{intl.formatMessage({id, defaultMessage}, values )}
{children}
</Markdown>
)
}

const FormattedMarkdown = ({ id, defaultMessage, values }) => {
const intl = useIntl()

return (
<FormattedMarkdownBase>
{intl.formatMessage({id, defaultMessage}, values )}
</FormattedMarkdownBase>
)
}

FormattedMarkdown.propTypes = {
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string,
Expand Down
112 changes: 112 additions & 0 deletions components/MATChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Box, Text } from 'ooni-components'
import { StackedBarChart } from 'components/aggregation/mat/StackedBarChart'
import { FunnelChart } from 'components/aggregation/mat/FunnelChart'
import { NoCharts } from 'components/aggregation/mat/NoCharts'
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 { useIntl } from 'react-intl'
import { ResizableBox } from './aggregation/mat/Resizable'
import { FormattedMarkdownBase } from './FormattedMarkdown'

axiosResponseTime(axios)

const swrOptions = {
revalidateOnFocus: false,
dedupingInterval: 10 * 60 * 1000,
}

const fetcher = (query) => {
const qs = new URLSearchParams(query).toString()
const reqUrl = `${process.env.NEXT_PUBLIC_OONI_API}/api/v1/aggregation?${qs}`
console.debug(`API Query: ${reqUrl}`)
return axios
.get(reqUrl)
.then((r) => {
return {
data: r.data,
loadTime: r.loadTime,
url: r.config.url,
}
})
.catch((e) => {
// throw new Error(e?.response?.data?.error ?? e.message)
const error = new Error('An error occurred while fetching the data.')
// Attach extra info to the error object.
error.info = e.response.data.error
error.status = e.response.status
throw error
})
}

export const MATChartReportWrapper = ({link, caption}) => {
let searchParams
const today = dayjs.utc().add(1, 'day')
const monthAgo = dayjs.utc(today).subtract(1, 'month')

try {
if (link) searchParams = Object.fromEntries(new URL(link).searchParams)
} catch (e) {
console.log('e', link, e)
searchParams = null
}

//TODO: make sure searchParams are only the ones that are allowed
const query = {
test_name: 'web_connectivity',
axis_x: 'measurement_start_day',
since: monthAgo.format('YYYY-MM-DD'),
until: today.format('YYYY-MM-DD'),
time_grain: 'day',
...searchParams,
}

return !!searchParams &&
<Box my={4}>
<MATChart query={query} />
{caption && (
<Text fontSize={1} mt={2}>
<FormattedMarkdownBase>{caption}</FormattedMarkdownBase>
</Text>
)}
</Box>
}

const MATChart = ({ query }) => {
const intl = useIntl()
const { data, error, isValidating } = useSWR(query ? query : null, fetcher, swrOptions)

const showLoadingIndicator = useMemo(() => isValidating, [isValidating])
return (
<Box>
<MATContextProvider queryParams={query}>
{error && <NoCharts message={error?.info ?? JSON.stringify(error)} />}
<Box>
{showLoadingIndicator ? (
<Box>
<h2>{intl.formatMessage({ id: 'General.Loading' })}</h2>
</Box>
) : (
<>
{data?.data?.result?.length > 0 ?
<Box sx={{ minHeight: '500px' }}>
{data && data.data.dimension_count == 0 && <FunnelChart data={data.data.result} />}
{data && data.data.dimension_count == 1 && <StackedBarChart data={data.data.result} query={query} />}
{data && data.data.dimension_count > 1 && (
<TableView data={data.data.result} query={query} />
)}
</Box> : <ResizableBox><NoCharts /></ResizableBox>
}
</>
)}
</Box>
</MATContextProvider>
</Box>
)
}

export default MATChart
23 changes: 15 additions & 8 deletions components/NavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,21 +106,28 @@ export const NavBar = ({ color }) => {
</Box>
<Box mt={[2, 0]}>
<Flex flexDirection={['column', 'row']} alignItems={'center'}>
<NavItem label={<FormattedMessage id='Navbar.Search' />} href='/search' />
<NavItem label={<FormattedMessage id='Navbar.Charts.MAT' />} href='/chart/mat' />
<NavItem label={<FormattedMessage id='Navbar.Charts.Circumvention' />} href='/chart/circumvention' />
<NavItem label={<FormattedMessage id='Navbar.Countries' />} href='/countries' />
<NavItem label={<FormattedMessage id='Navbar.Networks' />} href='/networks' />
<NavItem label={<FormattedMessage id='Navbar.Domains' />} href='/domains' />
{user?.logged_in && (
<NavItem label={<FormattedMessage id="Navbar.Search" />} href="/search" />
<NavItem label={<FormattedMessage id="Navbar.Charts.MAT" />} href="/chart/mat" />
<NavItem
label={<FormattedMessage id="Navbar.Charts.Circumvention" />}
href="/chart/circumvention"
/>
<NavItem label={<FormattedMessage id="Navbar.Countries" />} href="/countries" />
<NavItem label={<FormattedMessage id="Navbar.Networks" />} href="/networks" />
<NavItem label={<FormattedMessage id="Navbar.Domains" />} href="/domains" />
{/* <NavItem label={<FormattedMessage id="Navbar.Incidents" />} href="/incidents" /> */}
{user?.logged_in ? (
<Box ml={[0, 4]} my={[2, 0]}>
<StyledNavItem>
<NavItemLabel onClick={logoutUser}>
<FormattedMessage id='General.Logout' />
<FormattedMessage id="General.Logout" />
</NavItemLabel>
<Underline />
</StyledNavItem>
</Box>
) : (
<></>
// <NavItem label={<FormattedMessage id="General.Login" />} href="/login" />
)}
<Box ml={[0, 4]} my={[2, 0]}>
<LanguageSelect ml={[0, 4]} onChange={handleLocaleChange} value={locale}>
Expand Down
26 changes: 26 additions & 0 deletions components/NotFound.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* global process */
import React from 'react'
import { Container, Flex, Box, Heading, Text } from 'ooni-components'
import { useRouter } from 'next/router'

import OONI404 from '../public/static/images/OONI_404.svg'
import { useIntl } from 'react-intl'

const NotFound = ({ title }) => {
const { asPath } = useRouter()
const intl = useIntl()

return (
<Container>
<Flex justifyContent="space-around" alignItems="center" my={5}>
<OONI404 height="200px" />
<Box width={1 / 2}>
<Heading h={4}>{title}</Heading>
<Text color="gray8">{`${process.env.NEXT_PUBLIC_EXPLORER_URL}${asPath}`}</Text>
</Box>
</Flex>
</Container>
)
}

export default NotFound
2 changes: 1 addition & 1 deletion components/SharedStyledComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ z-index: 100;

export const StyledStickySubMenu = styled.div`
position: sticky;
top: 65.5px;
top: 66px;
background: white;
z-index: 100;
border-bottom: 1px solid ${props => props.theme.colors.gray3};
Expand Down
2 changes: 1 addition & 1 deletion components/aggregation/mat/ChartHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const ChartHeader = ({ options = {}}) => {
</Heading>}
</Box>
<Box>
<Flex mb={2} justifyContent='space-between'>
<Flex mb={2} justifyContent='space-between' fontSize={14}>
{options.legend !== false && <Flex justifyContent='center' my={2} flexWrap="wrap">
<Legend label={intl.formatMessage({id: 'MAT.Table.Header.ok_count'})} color={colorMap['ok_count']} />
<Legend label={intl.formatMessage({id: 'MAT.Table.Header.confirmed_count'})} color={colorMap['confirmed_count']} />
Expand Down
6 changes: 1 addition & 5 deletions components/aggregation/mat/GridChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,6 @@ export const prepareDataForGridChart = (data, query, locale) => {
*
* selectedRows - a subset of `rowKeys` representing which rows to render in the grid
*
* isGrouped - Whether the data is already grouped by y-axis value
* If `false`, `reshapeChartData()` will group the data as required
*
* height - uses a specific height provided by the container (e.g ResizableBox)
* If not speicied, it calculates a height based on the number of rows, capped
* at GRID_MAX_HEIGHT, which allows <VirtualRows> to render a subset of the data
Expand All @@ -89,7 +86,7 @@ export const prepareDataForGridChart = (data, query, locale) => {
* header - an element showing some summary information on top of the charts
}
*/
const GridChart = ({ data, rowKeys, rowLabels, isGrouped = true, 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
Expand Down Expand Up @@ -178,7 +175,6 @@ GridChart.propTypes = {
rowKeys: PropTypes.arrayOf(PropTypes.string),
rowLabels: PropTypes.objectOf(PropTypes.string),
selectedRows: PropTypes.arrayOf(PropTypes.string),
isGrouped: PropTypes.bool,
height: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
Expand Down
Loading

1 comment on commit 022498c

@vercel
Copy link

@vercel vercel bot commented on 022498c Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

explorer – ./

explorer-one.vercel.app
explorer-git-master-ooni1.vercel.app
explorer-ooni1.vercel.app

Please sign in to comment.