mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Merge branch 'dev' into unknown-count
This commit is contained in:
commit
490fc99a8a
41 changed files with 615 additions and 379 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import classNames from 'classnames';
|
||||
import Icon from './Icon';
|
||||
import styles from './Button.module.css';
|
||||
|
||||
export default function Button({
|
||||
function Button({
|
||||
type = 'button',
|
||||
icon,
|
||||
size,
|
||||
|
|
@ -43,3 +44,19 @@ export default function Button({
|
|||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
type: PropTypes.oneOf(['button', 'submit', 'reset']),
|
||||
icon: PropTypes.node,
|
||||
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
|
||||
variant: PropTypes.oneOf(['action', 'danger', 'light']),
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
tooltip: PropTypes.node,
|
||||
tooltipId: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
iconRight: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Button;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Button from './Button';
|
||||
import styles from './ButtonGroup.module.css';
|
||||
|
||||
export default function ButtonGroup({
|
||||
items = [],
|
||||
selectedItem,
|
||||
className,
|
||||
size,
|
||||
icon,
|
||||
onClick = () => {},
|
||||
}) {
|
||||
function ButtonGroup({ items = [], selectedItem, className, size, icon, onClick = () => {} }) {
|
||||
return (
|
||||
<div className={classNames(styles.group, className)}>
|
||||
{items.map(item => {
|
||||
|
|
@ -30,3 +24,19 @@ export default function ButtonGroup({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ButtonGroup.propTypes = {
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any.isRequired,
|
||||
}),
|
||||
),
|
||||
selectedItem: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
|
||||
icon: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ButtonGroup;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Check from 'assets/check.svg';
|
||||
import styles from './Checkbox.module.css';
|
||||
|
||||
export default function Checkbox({ name, value, label, onChange }) {
|
||||
function Checkbox({ name, value, label, onChange }) {
|
||||
const ref = useRef();
|
||||
|
||||
return (
|
||||
|
|
@ -25,3 +26,12 @@ export default function Checkbox({ name, value, label, onChange }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Checkbox.propTypes = {
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
label: PropTypes.node,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from './Button';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
|
@ -6,7 +7,7 @@ const defaultText = (
|
|||
<FormattedMessage id="label.copy-to-clipboard" defaultMessage="Copy to clipboard" />
|
||||
);
|
||||
|
||||
export default function CopyButton({ element, ...props }) {
|
||||
function CopyButton({ element, ...props }) {
|
||||
const [text, setText] = useState(defaultText);
|
||||
|
||||
function handleClick() {
|
||||
|
|
@ -24,3 +25,13 @@ export default function CopyButton({ element, ...props }) {
|
|||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
CopyButton.propTypes = {
|
||||
element: PropTypes.shape({
|
||||
current: PropTypes.shape({
|
||||
select: PropTypes.func.isRequired,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export default CopyButton;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { endOfYear, isSameDay } from 'date-fns';
|
||||
import Modal from './Modal';
|
||||
|
|
@ -54,7 +55,7 @@ const filterOptions = [
|
|||
},
|
||||
];
|
||||
|
||||
export default function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||
function DateFilter({ value, startDate, endDate, onChange, className }) {
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const displayValue =
|
||||
value === 'custom' ? (
|
||||
|
|
@ -117,3 +118,13 @@ const CustomRange = ({ startDate, endDate, onClick }) => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DateFilter.propTypes = {
|
||||
value: PropTypes.string,
|
||||
startDate: PropTypes.instanceOf(Date),
|
||||
endDate: PropTypes.instanceOf(Date),
|
||||
onChange: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default DateFilter;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Dot.module.css';
|
||||
|
||||
export default function Dot({ color, size, className }) {
|
||||
function Dot({ color, size, className }) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div
|
||||
|
|
@ -15,3 +16,11 @@ export default function Dot({ color, size, className }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Dot.propTypes = {
|
||||
color: PropTypes.string,
|
||||
size: PropTypes.oneOf(['small', 'large']),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Dot;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Menu from './Menu';
|
||||
import useDocumentClick from 'hooks/useDocumentClick';
|
||||
|
|
@ -6,13 +7,7 @@ import Chevron from 'assets/chevron-down.svg';
|
|||
import styles from './Dropdown.module.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
export default function DropDown({
|
||||
value,
|
||||
className,
|
||||
menuClassName,
|
||||
options = [],
|
||||
onChange = () => {},
|
||||
}) {
|
||||
function DropDown({ value, className, menuClassName, options = [], onChange = () => {} }) {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const ref = useRef();
|
||||
const selectedOption = options.find(e => e.value === value);
|
||||
|
|
@ -52,3 +47,18 @@ export default function DropDown({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DropDown.propTypes = {
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
menuClassName: PropTypes.string,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.any.isRequired,
|
||||
label: PropTypes.node,
|
||||
}),
|
||||
),
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default DropDown;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import styles from './EmptyPlaceholder.module.css';
|
||||
|
||||
export default function EmptyPlaceholder({ msg, children }) {
|
||||
function EmptyPlaceholder({ msg, children }) {
|
||||
return (
|
||||
<div className={styles.placeholder}>
|
||||
<Icon className={styles.icon} icon={<Logo />} size="xlarge" />
|
||||
|
|
@ -12,3 +13,10 @@ export default function EmptyPlaceholder({ msg, children }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EmptyPlaceholder.propTypes = {
|
||||
msg: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default EmptyPlaceholder;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Favicon.module.css';
|
||||
|
||||
function getHostName(url) {
|
||||
|
|
@ -6,7 +7,7 @@ function getHostName(url) {
|
|||
return match && match.length > 1 ? match[1] : null;
|
||||
}
|
||||
|
||||
export default function Favicon({ domain, ...props }) {
|
||||
function Favicon({ domain, ...props }) {
|
||||
const hostName = domain ? getHostName(domain) : null;
|
||||
|
||||
return hostName ? (
|
||||
|
|
@ -19,3 +20,9 @@ export default function Favicon({ domain, ...props }) {
|
|||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
Favicon.propTypes = {
|
||||
domain: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Favicon;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,25 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
|
||||
export default function FilterButtons({ buttons, selected, onClick }) {
|
||||
function FilterButtons({ buttons, selected, onClick }) {
|
||||
return (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
}
|
||||
|
||||
FilterButtons.propTypes = {
|
||||
buttons: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any.isRequired,
|
||||
}),
|
||||
),
|
||||
selected: PropTypes.any,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default FilterButtons;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Icon.module.css';
|
||||
|
||||
export default function Icon({ icon, className, size = 'medium', ...props }) {
|
||||
function Icon({ icon, className, size = 'medium', ...props }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.icon, className, {
|
||||
|
|
@ -18,3 +19,11 @@ export default function Icon({ icon, className, size = 'medium', ...props }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Icon.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node.isRequired,
|
||||
size: PropTypes.oneOf(['xlarge', 'large', 'medium', 'small', 'xsmall']),
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import NextLink from 'next/link';
|
||||
import Icon from './Icon';
|
||||
import styles from './Link.module.css';
|
||||
|
||||
export default function Link({ className, icon, children, size, iconRight, ...props }) {
|
||||
function Link({ className, icon, children, size, iconRight, ...props }) {
|
||||
return (
|
||||
<NextLink {...props}>
|
||||
<a
|
||||
|
|
@ -21,3 +22,13 @@ export default function Link({ className, icon, children, size, iconRight, ...pr
|
|||
</NextLink>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
size: PropTypes.oneOf(['large', 'small', 'xsmall']),
|
||||
iconRight: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Link;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Loading.module.css';
|
||||
|
||||
export default function Loading({ className }) {
|
||||
function Loading({ className }) {
|
||||
return (
|
||||
<div className={classNames(styles.loading, className)}>
|
||||
<div />
|
||||
|
|
@ -11,3 +12,9 @@ export default function Loading({ className }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Menu.module.css';
|
||||
|
||||
export default function Menu({
|
||||
function Menu({
|
||||
options = [],
|
||||
selectedOption,
|
||||
className,
|
||||
|
|
@ -46,3 +47,24 @@ export default function Menu({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Menu.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
divider: PropTypes.bool,
|
||||
}),
|
||||
),
|
||||
selectedOption: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
float: PropTypes.oneOf(['top', 'bottom']),
|
||||
align: PropTypes.oneOf(['left', 'right']),
|
||||
optionClassName: PropTypes.string,
|
||||
selectedClassName: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Menu from 'components/common/Menu';
|
||||
import Button from 'components/common/Button';
|
||||
import useDocumentClick from 'hooks/useDocumentClick';
|
||||
import styles from './MenuButton.module.css';
|
||||
|
||||
export default function MenuButton({
|
||||
function MenuButton({
|
||||
icon,
|
||||
value,
|
||||
options,
|
||||
|
|
@ -58,3 +59,25 @@ export default function MenuButton({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MenuButton.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
divider: PropTypes.bool,
|
||||
}),
|
||||
),
|
||||
buttonClassName: PropTypes.string,
|
||||
menuClassName: PropTypes.string,
|
||||
menuPosition: PropTypes.oneOf(['top', 'bottom']),
|
||||
menuAlign: PropTypes.oneOf(['left', 'right']),
|
||||
onSelect: PropTypes.func,
|
||||
renderValue: PropTypes.func,
|
||||
};
|
||||
|
||||
export default MenuButton;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import styles from './Modal.module.css';
|
||||
|
||||
export default function Modal({ title, children }) {
|
||||
function Modal({ title, children }) {
|
||||
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
|
|
@ -16,3 +17,10 @@ export default function Modal({ title, children }) {
|
|||
document.getElementById('__modals'),
|
||||
);
|
||||
}
|
||||
|
||||
Modal.propTypes = {
|
||||
title: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useRouter } from 'next/router';
|
||||
import classNames from 'classnames';
|
||||
import styles from './NavMenu.module.css';
|
||||
|
||||
export default function NavMenu({ options = [], className, onSelect = () => {} }) {
|
||||
function NavMenu({ options = [], className, onSelect = () => {} }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
|
|
@ -30,3 +31,17 @@ export default function NavMenu({ options = [], className, onSelect = () => {} }
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NavMenu.propTypes = {
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
className: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
}),
|
||||
),
|
||||
className: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
export default NavMenu;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styles from './NoData.module.css';
|
||||
|
||||
export default function NoData({ className }) {
|
||||
function NoData({ className }) {
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<FormattedMessage id="message.no-data-available" defaultMessage="No data available." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NoData.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NoData;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { setDateRange } from 'redux/actions/websites';
|
||||
|
|
@ -8,7 +9,7 @@ import Dots from 'assets/ellipsis-h.svg';
|
|||
import useDateRange from 'hooks/useDateRange';
|
||||
import { getDateRange } from '../../lib/date';
|
||||
|
||||
export default function RefreshButton({ websiteId }) {
|
||||
function RefreshButton({ websiteId }) {
|
||||
const dispatch = useDispatch();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -35,3 +36,9 @@ export default function RefreshButton({ websiteId }) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
RefreshButton.propTypes = {
|
||||
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default RefreshButton;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import NoData from 'components/common/NoData';
|
||||
import styles from './Table.module.css';
|
||||
|
||||
export default function Table({
|
||||
function Table({
|
||||
columns,
|
||||
rows,
|
||||
empty,
|
||||
|
|
@ -45,6 +46,34 @@ export default function Table({
|
|||
);
|
||||
}
|
||||
|
||||
const styledObject = PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
});
|
||||
|
||||
Table.propTypes = {
|
||||
columns: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
cell: styledObject,
|
||||
className: PropTypes.string,
|
||||
header: styledObject,
|
||||
key: PropTypes.string,
|
||||
label: PropTypes.node,
|
||||
render: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
}),
|
||||
),
|
||||
rows: PropTypes.arrayOf(PropTypes.object),
|
||||
empty: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
bodyClassName: PropTypes.string,
|
||||
rowKey: PropTypes.func,
|
||||
showHeader: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
export const TableRow = ({ columns, row }) => (
|
||||
<div className={classNames(styles.row, 'row')}>
|
||||
{columns.map(({ key, render, className, style, cell }, index) => (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import styles from './Tag.module.css';
|
||||
|
||||
export default function Tag({ className, children }) {
|
||||
function Tag({ className, children }) {
|
||||
return <span className={classNames(styles.tag, className)}>{children}</span>;
|
||||
}
|
||||
|
||||
Tag.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import styles from './Toast.module.css';
|
||||
import Icon from 'components/common/Icon';
|
||||
import Close from 'assets/times.svg';
|
||||
|
||||
export default function Toast({ message, timeout = 3000, onClose }) {
|
||||
function Toast({ message, timeout = 3000, onClose }) {
|
||||
const props = useSpring({
|
||||
opacity: 1,
|
||||
transform: 'translate3d(0,0px,0)',
|
||||
|
|
@ -24,3 +25,11 @@ export default function Toast({ message, timeout = 3000, onClose }) {
|
|||
document.getElementById('__modals'),
|
||||
);
|
||||
}
|
||||
|
||||
Toast.propTypes = {
|
||||
message: PropTypes.node,
|
||||
timeout: PropTypes.number,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Toast;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps';
|
||||
import classNames from 'classnames';
|
||||
|
|
@ -12,7 +13,7 @@ import { useRouter } from 'next/router';
|
|||
|
||||
const geoUrl = '/world-110m.json';
|
||||
|
||||
export default function WorldMap({ data, className }) {
|
||||
function WorldMap({ data, className }) {
|
||||
const { basePath } = useRouter();
|
||||
const [tooltip, setTooltip] = useState();
|
||||
const [theme] = useTheme();
|
||||
|
|
@ -89,3 +90,16 @@ export default function WorldMap({ data, className }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
WorldMap.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
x: PropTypes.string,
|
||||
y: PropTypes.number,
|
||||
z: PropTypes.number,
|
||||
}),
|
||||
),
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default WorldMap;
|
||||
|
|
|
|||
|
|
@ -44,4 +44,7 @@
|
|||
.header {
|
||||
padding: 0 15px;
|
||||
}
|
||||
.nav {
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import classNames from 'classnames';
|
|||
import ChartJS from 'chart.js';
|
||||
import Legend from 'components/metrics/Legend';
|
||||
import { formatLongNumber } from 'lib/format';
|
||||
import { dateFormat } from 'lib/lang';
|
||||
import { dateFormat, timeFormat } from 'lib/lang';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants';
|
||||
|
|
@ -44,9 +44,9 @@ export default function BarChart({
|
|||
|
||||
switch (unit) {
|
||||
case 'minute':
|
||||
return index % 2 === 0 ? dateFormat(d, 'h:mm', locale) : '';
|
||||
return index % 2 === 0 ? dateFormat(d, 'H:mm', locale) : '';
|
||||
case 'hour':
|
||||
return dateFormat(d, 'ha', locale);
|
||||
return timeFormat(d, locale);
|
||||
case 'day':
|
||||
if (records > 31) {
|
||||
if (w <= 500) {
|
||||
|
|
@ -131,6 +131,7 @@ export default function BarChart({
|
|||
minRotation: 0,
|
||||
maxRotation: 0,
|
||||
fontColor: colors.text,
|
||||
autoSkipPadding: 1,
|
||||
},
|
||||
gridLines: {
|
||||
display: false,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,66 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import Tag from 'components/common/Tag';
|
||||
import DropDown from 'components/common/DropDown';
|
||||
import { eventTypeFilter } from 'lib/filters';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import useShareToken from 'hooks/useShareToken';
|
||||
import { TOKEN_HEADER } from 'lib/constants';
|
||||
import styles from './EventsTable.module.css';
|
||||
|
||||
const EVENT_FILTER_DEFAULT = {
|
||||
value: 'EVENT_FILTER_DEFAULT',
|
||||
label: <FormattedMessage id="label.all-events" defaultMessage="All events" />,
|
||||
};
|
||||
|
||||
export default function EventsTable({ websiteId, ...props }) {
|
||||
const [eventType, setEventType] = useState(EVENT_FILTER_DEFAULT.value);
|
||||
|
||||
const {
|
||||
query: { url },
|
||||
} = usePageQuery();
|
||||
|
||||
const shareToken = useShareToken();
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
const { data, loading, error } = useFetch(
|
||||
`/api/website/${websiteId}/event-types`,
|
||||
{
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
url,
|
||||
},
|
||||
headers: { [TOKEN_HEADER]: shareToken?.token },
|
||||
},
|
||||
[modified],
|
||||
);
|
||||
|
||||
const eventTypes = data ? [...new Set(data.map(({ x }) => x))] : [];
|
||||
const dropDownOptions = [EVENT_FILTER_DEFAULT, ...eventTypes.map(t => ({ value: t, label: t }))];
|
||||
|
||||
return (
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
||||
type="event"
|
||||
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
|
||||
websiteId={websiteId}
|
||||
renderLabel={({ x }) => <Label value={x} />}
|
||||
/>
|
||||
<>
|
||||
{!loading && !error && eventTypes.length > 1 && (
|
||||
<div className={styles.filter}>
|
||||
<DropDown value={eventType} options={dropDownOptions} onChange={setEventType} />
|
||||
</div>
|
||||
)}
|
||||
<MetricsTable
|
||||
{...props}
|
||||
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
|
||||
type="event"
|
||||
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
|
||||
websiteId={websiteId}
|
||||
dataFilter={eventTypeFilter}
|
||||
filterOptions={eventType === EVENT_FILTER_DEFAULT.value ? [] : [eventType]}
|
||||
renderLabel={({ x }) => <Label value={x} />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
6
components/metrics/EventsTable.module.css
Normal file
6
components/metrics/EventsTable.module.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.filter {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
|
@ -11,8 +11,6 @@
|
|||
@media only screen and (max-width: 992px) {
|
||||
.bar {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.bar > div:nth-child(n + 3) {
|
||||
display: none;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue