Merge branch 'dev' into feat/um-202-event-data-new

This commit is contained in:
Brian Cao 2023-03-20 20:40:22 -07:00
commit da7f02bb73
38 changed files with 437 additions and 414 deletions

View file

@ -0,0 +1,25 @@
import { useEffect, useState } from 'react';
import { Tooltip } from 'react-basics';
import styles from './HoverTooltip.module.css';
export default function HoverTooltip({ tooltip }) {
const [position, setPosition] = useState({ x: -1000, y: -1000 });
useEffect(() => {
const handler = e => {
setPosition({ x: e.clientX, y: e.clientY });
};
document.addEventListener('mousemove', handler);
return () => {
document.removeEventListener('mousemove', handler);
};
}, []);
return (
<div className={styles.tooltip} style={{ left: position.x, top: position.y }}>
<Tooltip position="top" action="none" label={tooltip} />
</div>
);
}

View file

@ -3,7 +3,7 @@
}
.tooltip {
color: var(--msgColor);
position: fixed;
pointer-events: none;
z-index: 1;
}

View file

@ -1,6 +1,5 @@
import { useState, useMemo } from 'react';
import { useRouter } from 'next/router';
import ReactTooltip from 'react-tooltip';
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
import classNames from 'classnames';
import { colord } from 'colord';
@ -9,6 +8,8 @@ import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants';
import styles from './WorldMap.module.css';
import useCountryNames from 'hooks/useCountryNames';
import useLocale from 'hooks/useLocale';
import HoverTooltip from './HoverTooltip';
import { formatLongNumber } from '../../lib/format';
function WorldMap({ data, className }) {
const { basePath } = useRouter();
@ -46,7 +47,7 @@ function WorldMap({ data, className }) {
function handleHover(code) {
if (code === 'AQ') return;
const country = data?.find(({ x }) => x === code);
setTooltip(`${countryNames[code]}: ${country?.y || 0} visitors`);
setTooltip(`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} visitors`);
}
return (
@ -83,7 +84,7 @@ function WorldMap({ data, className }) {
</Geographies>
</ZoomableGroup>
</ComposableMap>
<ReactTooltip id="world-map-tooltip">{tooltip}</ReactTooltip>
{tooltip && <HoverTooltip tooltip={tooltip} />}
</div>
);
}

View file

@ -12,7 +12,7 @@
gap: 10px;
font-size: var(--font-size-lg);
font-weight: 700;
color: var(--font-color100);
color: var(--font-color100) !important;
}
.buttons {

View file

@ -1,224 +1,185 @@
import { useState, useRef, useEffect } from 'react';
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { StatusLight } from 'react-basics';
import classNames from 'classnames';
import ChartJS from 'chart.js';
import Chart from 'chart.js/auto';
import HoverTooltip from 'components/common/HoverTooltip';
import Legend from 'components/metrics/Legend';
import { formatLongNumber } from 'lib/format';
import { dateFormat } from 'lib/date';
import useLocale from 'hooks/useLocale';
import useTheme from 'hooks/useTheme';
import useForceUpdate from 'hooks/useForceUpdate';
import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
import styles from './BarChart.module.css';
import ChartTooltip from './ChartTooltip';
export default function BarChart({
chartId,
datasets,
unit,
records,
animationDuration = DEFAULT_ANIMATION_DURATION,
className,
stacked = false,
loading = false,
onCreate = () => {},
onUpdate = () => {},
className,
}) {
const canvas = useRef();
const chart = useRef();
const chart = useRef(null);
const [tooltip, setTooltip] = useState(null);
const { locale } = useLocale();
const [theme] = useTheme();
const forceUpdate = useForceUpdate();
const colors = {
text: THEME_COLORS[theme].gray700,
line: THEME_COLORS[theme].gray200,
zeroLine: THEME_COLORS[theme].gray500,
const colors = useMemo(
() => ({
text: THEME_COLORS[theme].gray700,
line: THEME_COLORS[theme].gray200,
}),
[theme],
);
const renderYLabel = label => {
return +label > 1000 ? formatLongNumber(label) : label;
};
function renderXLabel(label, index, values) {
if (loading) return '';
const d = new Date(values[index].value);
const sw = canvas.current.width / window.devicePixelRatio;
const renderTooltip = useCallback(
model => {
const { opacity, labelColors, dataPoints } = model.tooltip;
switch (unit) {
case 'minute':
return index % 2 === 0 ? dateFormat(d, 'H:mm', locale) : '';
case 'hour':
return dateFormat(d, 'p', locale);
case 'day':
if (records > 25) {
if (sw <= 275) {
return index % 10 === 0 ? dateFormat(d, 'M/d', locale) : '';
}
if (sw <= 550) {
return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
}
if (sw <= 700) {
return index % 2 === 0 ? dateFormat(d, 'M/d', locale) : '';
}
return dateFormat(d, 'MMM d', locale);
}
if (sw <= 375) {
return index % 2 === 0 ? dateFormat(d, 'MMM d', locale) : '';
}
if (sw <= 425) {
return dateFormat(d, 'MMM d', locale);
}
return dateFormat(d, 'EEE M/d', locale);
case 'month':
if (sw <= 330) {
return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : '';
}
return dateFormat(d, 'MMM', locale);
default:
return label;
}
}
if (!dataPoints?.length || !opacity) {
setTooltip(null);
return;
}
function renderYLabel(label) {
return +label > 1000 ? formatLongNumber(label) : label;
}
const formats = {
millisecond: 'T',
second: 'pp',
minute: 'p',
hour: 'h aaa',
day: 'PPPP',
week: 'PPPP',
month: 'LLLL yyyy',
quarter: 'qqq',
year: 'yyyy',
};
function renderTooltip(model) {
const { opacity, title, body, labelColors } = model;
setTooltip(
<div className={styles.tooltip}>
<div>{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}</div>
<div>
<StatusLight color={labelColors?.[0]?.backgroundColor}>
<div className={styles.value}>
{formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label}
</div>
</StatusLight>
</div>
</div>,
);
},
[unit],
);
if (!opacity || !title) {
setTooltip(null);
return;
}
const [label, value] = body[0].lines[0].split(':');
setTooltip({
title: dateFormat(new Date(+title[0]), getTooltipFormat(unit), locale),
value,
label,
labelColor: labelColors[0].backgroundColor,
});
}
function getTooltipFormat(unit) {
switch (unit) {
case 'hour':
return 'EEE p — PPP';
default:
return 'PPPP';
}
}
function createChart() {
const options = {
const getOptions = useCallback(() => {
return {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: animationDuration,
resize: {
duration: 0,
},
active: {
duration: 0,
},
},
tooltips: {
enabled: false,
custom: renderTooltip,
},
hover: {
animationDuration: 0,
},
responsive: true,
responsiveAnimationDuration: 0,
maintainAspectRatio: false,
legend: {
display: false,
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
external: renderTooltip,
},
},
scales: {
xAxes: [
{
type: 'time',
distribution: 'series',
time: {
unit,
tooltipFormat: 'x',
},
ticks: {
callback: renderXLabel,
minRotation: 0,
maxRotation: 0,
fontColor: colors.text,
autoSkipPadding: 1,
},
gridLines: {
display: false,
},
offset: true,
stacked: true,
x: {
type: 'time',
stacked: true,
time: {
unit,
},
],
yAxes: [
{
ticks: {
callback: renderYLabel,
beginAtZero: true,
fontColor: colors.text,
},
gridLines: {
color: colors.line,
zeroLineColor: colors.zeroLine,
},
stacked,
grid: {
display: false,
},
],
border: {
color: colors.line,
},
ticks: {
color: colors.text,
autoSkip: false,
maxRotation: 0,
},
},
y: {
type: 'linear',
min: 0,
beginAtZero: true,
stacked,
grid: {
color: colors.line,
},
border: {
color: colors.line,
},
ticks: {
color: colors.text,
callback: renderYLabel,
},
},
},
};
}, [animationDuration, renderTooltip, stacked, colors]);
const createChart = () => {
Chart.defaults.font.family = 'Inter';
const options = getOptions();
onCreate(options);
chart.current = new ChartJS(canvas.current, {
chart.current = new Chart(canvas.current, {
type: 'bar',
data: {
datasets,
},
options,
});
}
};
function updateChart() {
const { options } = chart.current;
const updateChart = () => {
setTooltip(null);
options.legend.labels.fontColor = colors.text;
options.scales.xAxes[0].time.unit = unit;
options.scales.xAxes[0].ticks.callback = renderXLabel;
options.scales.xAxes[0].ticks.fontColor = colors.text;
options.scales.yAxes[0].ticks.fontColor = colors.text;
options.scales.yAxes[0].ticks.precision = 0;
options.scales.yAxes[0].gridLines.color = colors.line;
options.scales.yAxes[0].gridLines.zeroLineColor = colors.zeroLine;
options.animation.duration = animationDuration;
options.tooltips.custom = renderTooltip;
chart.current.options = getOptions();
onUpdate(chart.current);
chart.current.update();
forceUpdate();
}
};
useEffect(() => {
if (datasets) {
if (!chart.current) {
createChart();
} else {
setTooltip(null);
updateChart();
}
}
}, [datasets, unit, animationDuration, locale, theme]);
}, [datasets, unit, theme, animationDuration, locale, loading]);
return (
<>
<div
data-tip=""
data-for={`${chartId}-tooltip`}
className={classNames(styles.chart, className)}
>
<div className={classNames(styles.chart, className)}>
<canvas ref={canvas} />
</div>
<Legend chart={chart.current} />
<ChartTooltip chartId={chartId} tooltip={tooltip} />
{tooltip && <HoverTooltip tooltip={tooltip} />}
</>
);
}

View file

@ -1,10 +1,21 @@
.chart {
position: relative;
height: 400px;
overflow: hidden;
}
.tooltip {
display: flex;
flex-direction: column;
gap: 10px;
}
.tooltip .value {
text-transform: lowercase;
}
@media only screen and (max-width: 992px) {
.chart {
height: 200px;
/*height: 200px;*/
}
}

View file

@ -1,26 +0,0 @@
import { StatusLight } from 'react-basics';
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 (
<ReactTooltip id={`${chartId}-tooltip`}>
<div className={styles.tooltip}>
<div className={styles.content}>
<div className={styles.title}>{title}</div>
<div className={styles.metric}>
<StatusLight color={labelColor}>
{value} {label}
</StatusLight>
</div>
</div>
</div>
</ReactTooltip>
);
}

View file

@ -33,12 +33,12 @@ export default function EventsChart({ websiteId, className, token }) {
if (!data) return [];
if (isLoading) return data;
const map = data.reduce((obj, { x, t, y }) => {
const map = data.reduce((obj, { x, y }) => {
if (!obj[x]) {
obj[x] = [];
}
obj[x].push({ t, y });
obj[x].push({ x, y });
return obj;
}, {});
@ -76,7 +76,6 @@ export default function EventsChart({ websiteId, className, token }) {
return (
<BarChart
chartId={`events-${websiteId}`}
className={className}
datasets={datasets}
unit={unit}

View file

@ -32,15 +32,13 @@ export default function FilterTags({ websiteId, params, onClick }) {
return null;
}
return (
<div key={key} className={styles.tag}>
<Button onClick={() => handleCloseFilter(key)} variant="primary" size="sm">
<Text>
<b>{`${key}`}</b> {`${safeDecodeURI(params[key])}`}
</Text>
<Icon>
<Icons.Close />
</Icon>
</Button>
<div key={key} className={styles.tag} onClick={() => handleCloseFilter(key)}>
<Text>
<b>{`${key}`}</b> = {`${safeDecodeURI(params[key])}`}
</Text>
<Icon>
<Icons.Close />
</Icon>
</div>
);
})}

View file

@ -1,11 +1,22 @@
.filters {
display: flex;
justify-content: flex-start;
align-items: flex-start;
align-items: center;
gap: 10px;
}
.tag {
text-align: center;
margin-bottom: 10px;
margin-right: 20px;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
font-size: var(--font-size-sm);
border: 1px solid var(--base600);
border-radius: var(--border-radius);
line-height: 30px;
padding: 0 8px;
cursor: pointer;
}
.tag:hover {
background: var(--base75);
}

View file

@ -2,13 +2,13 @@
display: flex;
justify-content: center;
flex-wrap: wrap;
margin-top: 10px;
padding: 10px 0;
}
.label {
display: flex;
align-items: center;
font-size: var(--font-size-xs);
font-size: var(--font-size-sm);
cursor: pointer;
}

View file

@ -40,7 +40,7 @@ export default function MetricsTable({
const { data, isLoading, isFetched, error } = useQuery(
[
'websites:mnetrics',
'websites:metrics',
{ websiteId, type, modified, url, referrer, os, browser, device, country },
],
() =>

View file

@ -25,12 +25,16 @@ export default function PageviewsChart({
const primaryColor = colord(THEME_COLORS[theme].primary);
return {
views: {
background: primaryColor.alpha(0.4).toRgbString(),
border: primaryColor.alpha(0.5).toRgbString(),
hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(),
backgroundColor: primaryColor.alpha(0.4).toRgbString(),
borderColor: primaryColor.alpha(0.7).toRgbString(),
hoverBorderColor: primaryColor.toRgbString(),
},
visitors: {
background: primaryColor.alpha(0.6).toRgbString(),
border: primaryColor.alpha(0.7).toRgbString(),
hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(),
backgroundColor: primaryColor.alpha(0.6).toRgbString(),
borderColor: primaryColor.alpha(0.9).toRgbString(),
hoverBorderColor: primaryColor.toRgbString(),
},
};
}, [theme]);
@ -50,30 +54,28 @@ export default function PageviewsChart({
return null;
}
const datasets = [
{
label: formatMessage(labels.uniqueVisitors),
data: data.sessions,
borderWidth: 1,
...colors.visitors,
},
{
label: formatMessage(labels.pageViews),
data: data.pageviews,
borderWidth: 1,
...colors.views,
},
];
return (
<div ref={ref}>
<BarChart
{...props}
key={websiteId}
className={className}
chartId={websiteId}
datasets={[
{
label: formatMessage(labels.uniqueVisitors),
data: data.sessions,
lineTension: 0,
backgroundColor: colors.visitors.background,
borderColor: colors.visitors.border,
borderWidth: 1,
},
{
label: formatMessage(labels.pageViews),
data: data.pageviews,
lineTension: 0,
backgroundColor: colors.views.background,
borderColor: colors.views.border,
borderWidth: 1,
},
]}
datasets={datasets}
unit={unit}
records={records}
animationDuration={visible ? animationDuration : 0}

View file

@ -1,29 +1,11 @@
import { useState } from 'react';
import { useIntl } from 'react-intl';
import MetricsTable from './MetricsTable';
import FilterButtons from 'components/common/FilterButtons';
import FilterLink from 'components/common/FilterLink';
import { refFilter } from 'lib/filters';
import { labels } from 'components/messages';
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
const filters = {
[FILTER_RAW]: null,
[FILTER_COMBINED]: refFilter,
};
export default function ReferrersTable({ websiteId, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED);
export default function ReferrersTable({ websiteId, ...props }) {
const { formatMessage } = useIntl();
const items = [
{
label: formatMessage(labels.filterCombined),
key: FILTER_COMBINED,
},
{ label: formatMessage(labels.filterRaw), key: FILTER_RAW },
];
const renderLink = ({ w: link, x: referrer }) => {
return referrer ? (
<FilterLink id="referrer" value={referrer} externalUrl={link} />
@ -34,14 +16,12 @@ export default function ReferrersTable({ websiteId, showFilters, ...props }) {
return (
<>
{showFilters && <FilterButtons items={items} selectedKey={filter} onSelect={setFilter} />}
<MetricsTable
{...props}
title={formatMessage(labels.referrers)}
type="referrer"
metric={formatMessage(labels.views)}
websiteId={websiteId}
dataFilter={filters[filter]}
renderLabel={renderLink}
/>
</>

View file

@ -7,7 +7,7 @@
.chart {
position: relative;
padding-bottom: 10px;
overflow: hidden;
}
.title {

View file

@ -5,7 +5,7 @@ import { FixedSizeList } from 'react-window';
import firstBy from 'thenby';
import FilterButtons from 'components/common/FilterButtons';
import NoData from 'components/common/NoData';
import { getDeviceMessage, labels, messages } from 'components/messages';
import { labels, messages } from 'components/messages';
import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames';
import { BROWSERS } from 'lib/constants';
@ -102,7 +102,7 @@ export default function RealtimeLog({ data, websiteDomain }) {
country: <b>{countryNames[country] || formatMessage(labels.unknown)}</b>,
browser: <b>{BROWSERS[browser]}</b>,
os: <b>{os}</b>,
device: <b>{formatMessage(getDeviceMessage(device))}</b>,
device: <b>{formatMessage(labels[device] || labels.unknown)}</b>,
}}
/>
);

View file

@ -17,14 +17,14 @@ import { labels } from 'components/messages';
import useUser from 'hooks/useUser';
import useApi from 'hooks/useApi';
export default function TeamWebsitesTable({ teamId, data = [], onSave }) {
export default function TeamWebsitesTable({ data = [], onSave }) {
const { formatMessage } = useIntl();
const { user } = useUser();
const { del, useMutation } = useApi();
const { mutate } = useMutation(data => del(`/teamWebsites/${data.teamWebsiteId}`));
const { mutate } = useMutation(({ teamWebsiteId }) => del(`/teamWebsites/${teamWebsiteId}`));
const columns = [
{ name: 'name', label: formatMessage(labels.name), style: { flex: 2 } },
{ name: 'name', label: formatMessage(labels.name) },
{ name: 'domain', label: formatMessage(labels.domain) },
{ name: 'action', label: ' ' },
];