diff --git a/.gitignore b/.gitignore index 715ff703..32d3cbce 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ .DS_Store .idea *.iml +*.log .vscode/* # debug diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..edc6c9a0 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: npm run start-env diff --git a/assets/bolt.svg b/assets/bolt.svg new file mode 100644 index 00000000..4654a1eb --- /dev/null +++ b/assets/bolt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/eye.svg b/assets/eye.svg new file mode 100644 index 00000000..09c93453 --- /dev/null +++ b/assets/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/visitor.svg b/assets/visitor.svg new file mode 100644 index 00000000..591873a5 --- /dev/null +++ b/assets/visitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/common/CopyButton.js b/components/common/CopyButton.js index 399da90d..460c68ac 100644 --- a/components/common/CopyButton.js +++ b/components/common/CopyButton.js @@ -3,7 +3,7 @@ import Button from './Button'; import { FormattedMessage } from 'react-intl'; const defaultText = ( - + ); export default function CopyButton({ element, ...props }) { diff --git a/components/common/Dot.js b/components/common/Dot.js new file mode 100644 index 00000000..d5dcf914 --- /dev/null +++ b/components/common/Dot.js @@ -0,0 +1,17 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './Dot.module.css'; + +export default function Dot({ color, size, className }) { + return ( +
+
+
+ ); +} diff --git a/components/common/Dot.module.css b/components/common/Dot.module.css new file mode 100644 index 00000000..258d6e87 --- /dev/null +++ b/components/common/Dot.module.css @@ -0,0 +1,22 @@ +.wrapper { + background: var(--gray50); + margin-right: 10px; + border-radius: 100%; +} + +.dot { + background: var(--green400); + width: 10px; + height: 10px; + border-radius: 100%; +} + +.dot.small { + width: 8px; + height: 8px; +} + +.dot.large { + width: 16px; + height: 16px; +} diff --git a/components/common/EmptyPlaceholder.js b/components/common/EmptyPlaceholder.js index a7b0720c..26a9fcbf 100644 --- a/components/common/EmptyPlaceholder.js +++ b/components/common/EmptyPlaceholder.js @@ -6,7 +6,7 @@ import styles from './EmptyPlaceholder.module.css'; export default function EmptyPlaceholder({ msg, children }) { return (
- } size="xlarge" /> + } size="xlarge" />

{msg}

{children}
diff --git a/components/common/EmptyPlaceholder.module.css b/components/common/EmptyPlaceholder.module.css index 7d2a93a5..58332566 100644 --- a/components/common/EmptyPlaceholder.module.css +++ b/components/common/EmptyPlaceholder.module.css @@ -5,3 +5,7 @@ align-items: center; min-height: 600px; } + +.icon { + margin-bottom: 30px; +} diff --git a/components/common/Favicon.js b/components/common/Favicon.js new file mode 100644 index 00000000..07ec696c --- /dev/null +++ b/components/common/Favicon.js @@ -0,0 +1,21 @@ +import React from 'react'; +import styles from './Favicon.module.css'; + +function getHostName(url) { + const match = url.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n?=]+)/im); + return match && match.length > 1 ? match[1] : null; +} + +export default function Favicon({ domain, ...props }) { + const hostName = domain ? getHostName(domain) : null; + + return hostName ? ( + + ) : null; +} diff --git a/components/common/Favicon.module.css b/components/common/Favicon.module.css new file mode 100644 index 00000000..82c85c42 --- /dev/null +++ b/components/common/Favicon.module.css @@ -0,0 +1,3 @@ +.favicon { + margin-right: 8px; +} diff --git a/components/common/FilterButtons.js b/components/common/FilterButtons.js new file mode 100644 index 00000000..5b898bf4 --- /dev/null +++ b/components/common/FilterButtons.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ButtonLayout from 'components/layout/ButtonLayout'; +import ButtonGroup from './ButtonGroup'; + +export default function FilterButtons({ buttons, selected, onClick }) { + return ( + + + + ); +} diff --git a/components/common/NoData.module.css b/components/common/NoData.module.css index d1c712eb..82f9c3ee 100644 --- a/components/common/NoData.module.css +++ b/components/common/NoData.module.css @@ -1,5 +1,6 @@ .container { color: var(--gray500); + font-size: var(--font-size-normal); position: absolute; top: 50%; left: 50%; diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js index af754a9c..b1b80a83 100644 --- a/components/common/RefreshButton.js +++ b/components/common/RefreshButton.js @@ -12,7 +12,7 @@ export default function RefreshButton({ websiteId }) { const dispatch = useDispatch(); const [dateRange] = useDateRange(websiteId); const [loading, setLoading] = useState(false); - const completed = useSelector(state => state.queries[`/api/website/${websiteId}/metrics`]); + const completed = useSelector(state => state.queries[`/api/website/${websiteId}/stats`]); function handleClick() { if (dateRange) { @@ -28,7 +28,7 @@ export default function RefreshButton({ websiteId }) { return (
diff --git a/components/forms/AccountEditForm.js b/components/forms/AccountEditForm.js index ccce3463..cc000cf7 100644 --- a/components/forms/AccountEditForm.js +++ b/components/forms/AccountEditForm.js @@ -58,22 +58,26 @@ export default function AccountEditForm({ values, onSave, onClose }) { - - +
+ + +
- - +
+ + +
{message} diff --git a/components/forms/ChangePasswordForm.js b/components/forms/ChangePasswordForm.js index c41b6e6b..9c41bd55 100644 --- a/components/forms/ChangePasswordForm.js +++ b/components/forms/ChangePasswordForm.js @@ -66,29 +66,35 @@ export default function ChangePasswordForm({ values, onSave, onClose }) { - - +
+ + +
- - +
+ + +
- - +
+ + +
{message} diff --git a/components/forms/DatePickerForm.js b/components/forms/DatePickerForm.js index 673cd7bd..9669f741 100644 --- a/components/forms/DatePickerForm.js +++ b/components/forms/DatePickerForm.js @@ -33,11 +33,11 @@ export default function DatePickerForm({ const buttons = [ { - label: , + label: , value: FILTER_DAY, }, { - label: , + label: , value: FILTER_RANGE, }, ]; @@ -72,10 +72,10 @@ export default function DatePickerForm({ diff --git a/components/forms/DeleteForm.js b/components/forms/DeleteForm.js index 689cd1fc..145a342a 100644 --- a/components/forms/DeleteForm.js +++ b/components/forms/DeleteForm.js @@ -73,8 +73,10 @@ export default function DeleteForm({ values, onSave, onClose }) { />

- - +
+ + +
{message} diff --git a/components/forms/LoginForm.js b/components/forms/LoginForm.js index 3866f240..38b85125 100644 --- a/components/forms/LoginForm.js +++ b/components/forms/LoginForm.js @@ -71,19 +71,23 @@ export default function LoginForm() { - - +
+ + +
- - +
+ + +
{message} diff --git a/components/forms/ShareUrlForm.js b/components/forms/ShareUrlForm.js index ea162f67..dbb1b656 100644 --- a/components/forms/ShareUrlForm.js +++ b/components/forms/ShareUrlForm.js @@ -30,7 +30,7 @@ export default function TrackingCodeForm({ values, onClose }) { diff --git a/components/forms/TrackingCodeForm.js b/components/forms/TrackingCodeForm.js index 1f44f835..a8d5a344 100644 --- a/components/forms/TrackingCodeForm.js +++ b/components/forms/TrackingCodeForm.js @@ -29,7 +29,7 @@ export default function TrackingCodeForm({ values, onClose }) { diff --git a/components/forms/WebsiteEditForm.js b/components/forms/WebsiteEditForm.js index 3432c2a4..7be89f79 100644 --- a/components/forms/WebsiteEditForm.js +++ b/components/forms/WebsiteEditForm.js @@ -63,15 +63,19 @@ export default function WebsiteEditForm({ values, onSave, onClose }) { - - +
+ + +
- - +
+ + +
@@ -91,10 +95,10 @@ export default function WebsiteEditForm({ values, onSave, onClose }) { {message} diff --git a/components/layout/Footer.js b/components/layout/Footer.js index 6fd0a46c..73e010bc 100644 --- a/components/layout/Footer.js +++ b/components/layout/Footer.js @@ -24,7 +24,9 @@ export default function Footer() { }} /> -
{`v${current}`}
+
+ {`v${current}`} +
); diff --git a/components/layout/FormLayout.module.css b/components/layout/FormLayout.module.css index 90e1b8c2..1ae393bb 100644 --- a/components/layout/FormLayout.module.css +++ b/components/layout/FormLayout.module.css @@ -17,6 +17,10 @@ line-height: 1.8; } +.row > div { + position: relative; +} + .buttons { display: flex; justify-content: center; @@ -33,9 +37,9 @@ justify-content: center; align-items: center; top: 0; - left: 100%; + left: calc(100% + 16px); bottom: 0; - margin-left: 16px; + z-index: 1; } .msg { @@ -68,3 +72,15 @@ color: var(--gray50); background: var(--gray800); } + +@media only screen and (max-width: 576px) { + .error { + align-items: flex-start; + top: calc(100% + 7px); + left: 0; + } + + .error:after { + left: 10px; + } +} diff --git a/components/layout/GridLayout.js b/components/layout/GridLayout.js new file mode 100644 index 00000000..1d3170d6 --- /dev/null +++ b/components/layout/GridLayout.js @@ -0,0 +1,31 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './GridLayout.module.css'; + +export default function GridLayout({ className, children }) { + return
{children}
; +} + +export const GridRow = ({ className, children }) => { + return
{children}
; +}; + +export const GridColumn = ({ xs, sm, md, lg, xl, className, children }) => { + const classes = []; + + classes.push(xs ? `col-${xs}` : 'col-12'); + + if (sm) { + classes.push(`col-sm-${sm}`); + } + if (md) { + classes.push(`col-md-${md}`); + } + if (lg) { + classes.push(`col-lg-${lg}`); + } + if (xl) { + classes.push(`col-lg-${xl}`); + } + return
{children}
; +}; diff --git a/components/layout/GridLayout.module.css b/components/layout/GridLayout.module.css new file mode 100644 index 00000000..f17c195e --- /dev/null +++ b/components/layout/GridLayout.module.css @@ -0,0 +1,40 @@ +.grid { + display: flex; + flex-direction: column; +} + +.col { + display: flex; + flex-direction: column; +} + +.row { + border-top: 1px solid var(--gray300); + min-height: 430px; +} + +.row > .col { + border-left: 1px solid var(--gray300); + padding: 20px; +} + +.row > .col:first-child { + border-left: 0; + padding-left: 0; +} + +.row > .col:last-child { + padding-right: 0; +} + +@media only screen and (max-width: 992px) { + .row { + border: 0; + } + + .row > .col { + border-top: 1px solid var(--gray300); + border-left: 0; + padding: 0; + } +} diff --git a/components/layout/Header.js b/components/layout/Header.js index c48fdd11..cc8baae3 100644 --- a/components/layout/Header.js +++ b/components/layout/Header.js @@ -30,6 +30,9 @@ export default function Header() { + + + diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index 3d7b7001..6dabd3d5 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -1,11 +1,18 @@ import React, { useMemo } from 'react'; +import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import useFetch from 'hooks/useFetch'; +import Dot from 'components/common/Dot'; +import { TOKEN_HEADER } from 'lib/constants'; +import useShareToken from 'hooks/useShareToken'; import styles from './ActiveUsers.module.css'; -import { FormattedMessage } from 'react-intl'; -export default function ActiveUsers({ websiteId, token, className }) { - const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 }); +export default function ActiveUsers({ websiteId, className }) { + const shareToken = useShareToken(); + const { data } = useFetch(`/api/website/${websiteId}/active`, { + interval: 60000, + headers: { [TOKEN_HEADER]: shareToken?.token }, + }); const count = useMemo(() => { return data?.[0]?.x || 0; }, [data]); @@ -16,7 +23,7 @@ export default function ActiveUsers({ websiteId, token, className }) { return (
-
+
1 ? formatLongNumber(label) : label; + return +label > 1000 ? formatLongNumber(label) : label; } function renderTooltip(model) { @@ -109,9 +115,7 @@ export default function BarChart({ responsiveAnimationDuration: 0, maintainAspectRatio: false, legend: { - labels: { - fontColor: colors.text, - }, + display: false, }, scales: { xAxes: [ @@ -177,6 +181,10 @@ export default function BarChart({ options.tooltips.custom = renderTooltip; onUpdate(chart.current); + + chart.current.update(); + + forceUpdate(); } useEffect(() => { @@ -200,23 +208,8 @@ export default function BarChart({ >
- - {tooltip ? : null} - + + ); } - -const Tooltip = ({ title, value, label, labelColor }) => ( -
-
-
{title}
-
-
-
-
- {value} {label} -
-
-
-); diff --git a/components/metrics/BarChart.module.css b/components/metrics/BarChart.module.css index cd26d3af..aea86a4c 100644 --- a/components/metrics/BarChart.module.css +++ b/components/metrics/BarChart.module.css @@ -1,43 +1,3 @@ .chart { position: relative; } - -.tooltip { - color: var(--msgColor); - pointer-events: none; - z-index: 1; -} - -.content { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; -} - -.title { - font-size: var(--font-size-xsmall); - font-weight: 600; -} - -.metric { - display: flex; - justify-content: center; - align-items: center; - font-size: var(--font-size-small); - font-weight: 600; -} - -.dot { - position: relative; - overflow: hidden; - border-radius: 100%; - margin-right: 8px; - background: var(--gray50); -} - -.color { - width: 10px; - height: 10px; -} diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index 97f9bfbd..12c1087b 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -3,15 +3,14 @@ import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import { browserFilter } from 'lib/filters'; -export default function BrowsersTable({ websiteId, token, limit }) { +export default function BrowsersTable({ websiteId, ...props }) { return ( } type="browser" metric={} websiteId={websiteId} - token={token} - limit={limit} dataFilter={browserFilter} /> ); diff --git a/components/metrics/ChartTooltip.js b/components/metrics/ChartTooltip.js new file mode 100644 index 00000000..fb290b66 --- /dev/null +++ b/components/metrics/ChartTooltip.js @@ -0,0 +1,26 @@ +import React from 'react'; +import Dot from 'components/common/Dot'; +import styles from './ChartTooltip.module.css'; +import ReactTooltip from 'react-tooltip'; + +export default function ChartTooltip({ chartId, tooltip }) { + if (!tooltip) { + return null; + } + + const { title, value, label, labelColor } = tooltip; + + return ( + +
+
+
{title}
+
+ + {value} {label} +
+
+
+
+ ); +} diff --git a/components/metrics/ChartTooltip.module.css b/components/metrics/ChartTooltip.module.css new file mode 100644 index 00000000..cd26d3af --- /dev/null +++ b/components/metrics/ChartTooltip.module.css @@ -0,0 +1,43 @@ +.chart { + position: relative; +} + +.tooltip { + color: var(--msgColor); + pointer-events: none; + z-index: 1; +} + +.content { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; +} + +.title { + font-size: var(--font-size-xsmall); + font-weight: 600; +} + +.metric { + display: flex; + justify-content: center; + align-items: center; + font-size: var(--font-size-small); + font-weight: 600; +} + +.dot { + position: relative; + overflow: hidden; + border-radius: 100%; + margin-right: 8px; + background: var(--gray50); +} + +.color { + width: 10px; + height: 10px; +} diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js index d562b464..59d17dfb 100644 --- a/components/metrics/CountriesTable.js +++ b/components/metrics/CountriesTable.js @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl'; import useCountryNames from 'hooks/useCountryNames'; import useLocale from 'hooks/useLocale'; -export default function CountriesTable({ websiteId, token, limit, onDataLoad = () => {} }) { +export default function CountriesTable({ websiteId, onDataLoad, ...props }) { const [locale] = useLocale(); const countryNames = useCountryNames(locale); @@ -15,13 +15,12 @@ export default function CountriesTable({ websiteId, token, limit, onDataLoad = ( return ( } type="country" metric={} websiteId={websiteId} - token={token} - limit={limit} - onDataLoad={data => onDataLoad(percentFilter(data))} + onDataLoad={data => onDataLoad?.(percentFilter(data))} renderLabel={renderLabel} /> ); diff --git a/components/metrics/DataTable.js b/components/metrics/DataTable.js new file mode 100644 index 00000000..5cf9ddb4 --- /dev/null +++ b/components/metrics/DataTable.js @@ -0,0 +1,91 @@ +import React, { useState } from 'react'; +import { FixedSizeList } from 'react-window'; +import { useSpring, animated, config } from 'react-spring'; +import classNames from 'classnames'; +import NoData from 'components/common/NoData'; +import { formatNumber, formatLongNumber } from 'lib/format'; +import styles from './DataTable.module.css'; + +export default function DataTable({ + data, + title, + metric, + className, + renderLabel, + height, + animate = true, + virtualize = false, +}) { + const [format, setFormat] = useState(true); + const formatFunc = format ? formatLongNumber : formatNumber; + + const handleSetFormat = () => setFormat(state => !state); + + const getRow = row => { + const { x: label, y: value, z: percent } = row; + + return ( + + ); + }; + + const Row = ({ index, style }) => { + return
{getRow(data[index])}
; + }; + + return ( +
+
+
{title}
+
+ {metric} +
+
+
+ {data?.length === 0 && } + {virtualize && data.length > 0 ? ( + + {Row} + + ) : ( + data.map(row => getRow(row)) + )} +
+
+ ); +} + +const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => { + const props = useSpring({ + width: percent, + y: value, + from: { width: 0, y: 0 }, + config: animate ? config.default : { duration: 0 }, + }); + + return ( +
+
{label}
+
+ {props.y?.interpolate(format)} +
+
+ `${n}%`) }} + /> + + {props.width.interpolate(n => `${n.toFixed(0)}%`)} + +
+
+ ); +}; diff --git a/components/metrics/DataTable.module.css b/components/metrics/DataTable.module.css new file mode 100644 index 00000000..79a60577 --- /dev/null +++ b/components/metrics/DataTable.module.css @@ -0,0 +1,95 @@ +.table { + position: relative; + font-size: var(--font-size-small); + display: flex; + flex-direction: column; + flex: 1; +} + +.body { + position: relative; + overflow: auto; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + line-height: 40px; +} + +.title { + display: flex; + font-weight: 600; + font-size: var(--font-size-normal); +} + +.metric { + font-size: var(--font-size-small); + font-weight: 600; + text-align: center; + width: 100px; + cursor: pointer; +} + +.row { + position: relative; + height: 30px; + line-height: 30px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 5px; + overflow: hidden; +} + +.label { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + flex: 2; +} + +.label a { + color: inherit; + text-decoration: none; +} + +.label a:hover { + color: var(--primary400); +} + +.label:empty { + color: #b3b3b3; +} + +.label:empty:before { + content: 'Unknown'; +} + +.value { + width: 50px; + text-align: right; + margin-right: 10px; + font-weight: 600; + cursor: pointer; +} + +.percent { + position: relative; + width: 50px; + color: var(--gray600); + border-left: 1px solid var(--gray600); + padding-left: 10px; + z-index: 1; +} + +.bar { + position: absolute; + top: 0; + left: 0; + height: 30px; + opacity: 0.1; + background: var(--primary400); + z-index: -1; +} diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index 7d87d1c1..52b6b5fc 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -4,15 +4,14 @@ import { deviceFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; import { getDeviceMessage } from 'components/messages'; -export default function DevicesTable({ websiteId, token, limit }) { +export default function DevicesTable({ websiteId, ...props }) { return ( } type="device" metric={} websiteId={websiteId} - token={token} - limit={limit} dataFilter={deviceFilter} renderLabel={({ x }) => getDeviceMessage(x)} /> diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index 113c6f56..68556c96 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -5,29 +5,36 @@ import { getDateArray, getDateLength } from 'lib/date'; import useFetch from 'hooks/useFetch'; import useDateRange from 'hooks/useDateRange'; import useTimezone from 'hooks/useTimezone'; -import { EVENT_COLORS } from 'lib/constants'; -import usePageQuery from '../../hooks/usePageQuery'; +import usePageQuery from 'hooks/usePageQuery'; +import useShareToken from 'hooks/useShareToken'; +import { EVENT_COLORS, TOKEN_HEADER } from 'lib/constants'; -export default function EventsChart({ websiteId, token }) { +export default function EventsChart({ websiteId, className, token }) { const [dateRange] = useDateRange(websiteId); const { startDate, endDate, unit, modified } = dateRange; const [timezone] = useTimezone(); const { query } = usePageQuery(); + const shareToken = useShareToken(); - const { data } = useFetch( + const { data, loading } = useFetch( `/api/website/${websiteId}/events`, { - start_at: +startDate, - end_at: +endDate, - unit, - tz: timezone, - url: query.url, - token, + params: { + start_at: +startDate, + end_at: +endDate, + unit, + tz: timezone, + url: query.url, + token, + }, + headers: { [TOKEN_HEADER]: shareToken?.token }, }, - { update: [modified] }, + [modified], ); + const datasets = useMemo(() => { if (!data) return []; + if (loading) return data; const map = data.reduce((obj, { x, t, y }) => { if (!obj[x]) { @@ -44,7 +51,7 @@ export default function EventsChart({ websiteId, token }) { }); return Object.keys(map).map((key, index) => { - const color = tinycolor(EVENT_COLORS[index]); + const color = tinycolor(EVENT_COLORS[index % EVENT_COLORS.length]); return { label: key, data: map[key], @@ -54,15 +61,7 @@ export default function EventsChart({ websiteId, token }) { borderWidth: 1, }; }); - }, [data]); - - function handleCreate(options) { - const legend = { - position: 'bottom', - }; - - options.legend = legend; - } + }, [data, loading]); function handleUpdate(chart) { chart.data.datasets = datasets; @@ -77,11 +76,13 @@ export default function EventsChart({ websiteId, token }) { return ( ); diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js index 9a7a09cb..c415a3e9 100644 --- a/components/metrics/EventsTable.js +++ b/components/metrics/EventsTable.js @@ -1,19 +1,17 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; -import styles from './EventsTable.module.css'; +import Tag from 'components/common/Tag'; -export default function EventsTable({ websiteId, token, limit, onDataLoad }) { +export default function EventsTable({ websiteId, ...props }) { return ( } type="event" metric={} websiteId={websiteId} - token={token} - limit={limit} renderLabel={({ x }) =>