diff --git a/README.md b/README.md index ce9f2bfe..5fe9dd96 100644 --- a/README.md +++ b/README.md @@ -48,13 +48,7 @@ mysql://username:mypassword@localhost:3306/mydb yarn build ``` -### Create database tables - -```bash -yarn update-db -``` - -This will also create a login account with username **admin** and password **umami**. +The build step will also create tables in your database if you ae installing for the first time. It will also create a login account with username **admin** and password **umami**. ### Start the application diff --git a/components/common/EventDataButton.js b/components/common/EventDataButton.js new file mode 100644 index 00000000..2b895840 --- /dev/null +++ b/components/common/EventDataButton.js @@ -0,0 +1,48 @@ +import List from 'assets/list-ul.svg'; +import Modal from 'components/common/Modal'; +import PropTypes from 'prop-types'; +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import Button from './Button'; +import EventDataForm from 'components/forms/EventDataForm'; +import styles from './EventDataButton.module.css'; + +function EventDataButton({ websiteId }) { + const [showEventData, setShowEventData] = useState(false); + + function handleClick() { + if (!showEventData) { + setShowEventData(true); + } + } + + function handleClose() { + setShowEventData(false); + } + + return ( + <> + + {showEventData && ( + }> + + + )} + + ); +} + +EventDataButton.propTypes = { + websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), +}; + +export default EventDataButton; diff --git a/components/common/EventDataButton.module.css b/components/common/EventDataButton.module.css new file mode 100644 index 00000000..cd2a2ed6 --- /dev/null +++ b/components/common/EventDataButton.module.css @@ -0,0 +1,3 @@ +.button { + width: fit-content; +} diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js index 0b197bd2..be1a8a1d 100644 --- a/components/common/RefreshButton.js +++ b/components/common/RefreshButton.js @@ -11,7 +11,7 @@ import useDateRange from 'hooks/useDateRange'; function RefreshButton({ websiteId }) { const [dateRange] = useDateRange(websiteId); const [loading, setLoading] = useState(false); - const selector = useCallback(state => state[`/website/${websiteId}/stats`], [websiteId]); + const selector = useCallback(state => state[`/websites/${websiteId}/stats`], [websiteId]); const completed = useStore(selector); function handleClick() { diff --git a/components/common/UpdateNotice.js b/components/common/UpdateNotice.js index 7419abe8..54c5fb83 100644 --- a/components/common/UpdateNotice.js +++ b/components/common/UpdateNotice.js @@ -18,7 +18,7 @@ export default function UpdateNotice() { function handleViewClick() { updateCheck(); setDismissed(true); - location.href = releaseUrl || REPO_URL; + open(releaseUrl || REPO_URL, '_blank'); } function handleDismissClick() { diff --git a/components/forms/AccountEditForm.js b/components/forms/AccountEditForm.js index 97317aff..70125656 100644 --- a/components/forms/AccountEditForm.js +++ b/components/forms/AccountEditForm.js @@ -15,13 +15,13 @@ const initialValues = { password: '', }; -const validate = ({ user_id, username, password }) => { +const validate = ({ id, username, password }) => { const errors = {}; if (!username) { errors.username = ; } - if (!user_id && !password) { + if (!id && !password) { errors.password = ; } @@ -33,7 +33,8 @@ export default function AccountEditForm({ values, onSave, onClose }) { const [message, setMessage] = useState(); const handleSubmit = async values => { - const { ok, data } = await post('/account', values); + const { id } = values; + const { ok, data } = await post(id ? `/accounts/${id}` : '/accounts', values); if (ok) { onSave(); diff --git a/components/forms/ChangePasswordForm.js b/components/forms/ChangePasswordForm.js index 29f34521..4ee657e6 100644 --- a/components/forms/ChangePasswordForm.js +++ b/components/forms/ChangePasswordForm.js @@ -9,6 +9,7 @@ import FormLayout, { FormRow, } from 'components/layout/FormLayout'; import useApi from 'hooks/useApi'; +import useUser from '../../hooks/useUser'; const initialValues = { current_password: '', @@ -39,9 +40,10 @@ const validate = ({ current_password, new_password, confirm_password }) => { export default function ChangePasswordForm({ values, onSave, onClose }) { const { post } = useApi(); const [message, setMessage] = useState(); + const { user } = useUser(); const handleSubmit = async values => { - const { ok, data } = await post('/account/password', values); + const { ok, data } = await post(`/accounts/${user.userId}/password`, values); if (ok) { onSave(); diff --git a/components/forms/EventDataForm.js b/components/forms/EventDataForm.js new file mode 100644 index 00000000..db8e7fbb --- /dev/null +++ b/components/forms/EventDataForm.js @@ -0,0 +1,262 @@ +import classNames from 'classnames'; +import Button from 'components/common/Button'; +import DateFilter from 'components/common/DateFilter'; +import DropDown from 'components/common/DropDown'; +import FormLayout, { + FormButtons, + FormError, + FormMessage, + FormRow, +} from 'components/layout/FormLayout'; +import DataTable from 'components/metrics/DataTable'; +import FilterTags from 'components/metrics/FilterTags'; +import { Field, Form, Formik } from 'formik'; +import useApi from 'hooks/useApi'; +import useDateRange from 'hooks/useDateRange'; +import { useState, useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; +import styles from './EventDataForm.module.css'; +import useTimezone from 'hooks/useTimezone'; + +export const filterOptions = [ + { label: 'Count', value: 'count' }, + { label: 'Average', value: 'avg' }, + { label: 'Minimum', value: 'min' }, + { label: 'Maxmimum', value: 'max' }, + { label: 'Sum', value: 'sum' }, +]; + +export const dateOptions = [ + { label: , value: '1day' }, + { + label: ( + + ), + value: '24hour', + }, + { + label: , + value: '-1day', + }, + { + label: , + value: '1week', + divider: true, + }, + { + label: ( + + ), + value: '7day', + }, + { + label: , + value: '1month', + divider: true, + }, + { + label: ( + + ), + value: '30day', + }, + { + label: ( + + ), + value: '90day', + }, + { label: , value: '1year' }, + { + label: , + value: 'custom', + divider: true, + }, +]; + +export default function EventDataForm({ websiteId, onClose, className }) { + const { post } = useApi(); + const [message, setMessage] = useState(); + const [columns, setColumns] = useState({}); + const [filters, setFilters] = useState({}); + const [data, setData] = useState([]); + const [dateRange, setDateRange] = useDateRange('report'); + const { startDate, endDate, value } = dateRange; + const [timezone] = useTimezone(); + const [isValid, setIsValid] = useState(false); + + useEffect(() => { + if (Object.keys(columns).length > 0) { + setIsValid(true); + } else { + setIsValid(false); + } + }, [columns]); + + const handleAddTag = (value, list, setState, resetForm) => { + setState({ ...list, [`${value.field}`]: value.value }); + resetForm(); + }; + + const handleRemoveTag = (value, list, setState) => { + const newList = { ...list }; + + delete newList[`${value}`]; + + setState(newList); + }; + + const handleSubmit = async () => { + const params = { + website_id: websiteId, + start_at: +startDate, + end_at: +endDate, + timezone, + columns, + filters, + }; + + const { ok, data } = await post(`/websites/${websiteId}/eventdata`, params); + + if (!ok) { + setMessage(); + setData([]); + } else { + setData(data); + setMessage(null); + } + }; + + return ( + <> + {message} +
+
+ +
+ + + + +
+
+ + handleAddTag(value, columns, setColumns, resetForm) + } + > + {({ values, setFieldValue }) => ( +
+ + +
+ + +
+
+ + +
+ setFieldValue('value', value)} + className={styles.dropdown} + name="value" + options={filterOptions} + /> + +
+
+ + + +
+ )} +
+ handleRemoveTag(value, columns, setColumns)} + /> +
+
+ + handleAddTag(value, filters, setFilters, resetForm) + } + > + {({ values }) => ( +
+ + +
+ + +
+
+ + +
+ + +
+
+ + + +
+ )} +
+ handleRemoveTag(value, filters, setFilters)} + /> +
+
+
+
+ +
+
+ + + + + + ); +} diff --git a/components/forms/EventDataForm.module.css b/components/forms/EventDataForm.module.css new file mode 100644 index 00000000..19d76f77 --- /dev/null +++ b/components/forms/EventDataForm.module.css @@ -0,0 +1,38 @@ +.container { + display: flex; +} + +.form { + border-right: 1px solid var(--gray300); + width: 420px; +} + +.filters { + padding: 10px 5px; +} + +.filters + .filters { + border-top: 1px solid var(--gray300); + min-height: 250px; +} + +.table { + padding: 10px; + min-height: 430px; + min-width: 400px; +} + +.formButtons { + justify-content: flex-start; + margin-left: 20px; +} + +.dropdown { + min-height: 39px; + min-width: 240px; +} + +.filterTag { + flex-wrap: wrap; + margin: 10px 5px 5px 5px; +} diff --git a/components/forms/ShareUrlForm.js b/components/forms/ShareUrlForm.js index a77d3fbc..9800e54b 100644 --- a/components/forms/ShareUrlForm.js +++ b/components/forms/ShareUrlForm.js @@ -8,7 +8,7 @@ import CopyButton from 'components/common/CopyButton'; export default function TrackingCodeForm({ values, onClose }) { const ref = useRef(); const { basePath } = useRouter(); - const { name, share_id } = values; + const { name, shareId } = values; return ( @@ -27,7 +27,7 @@ export default function TrackingCodeForm({ values, onClose }) { spellCheck={false} defaultValue={`${ document.location.origin - }${basePath}/share/${share_id}/${encodeURIComponent(name)}`} + }${basePath}/share/${shareId}/${encodeURIComponent(name)}`} readOnly /> diff --git a/components/forms/TrackingCodeForm.js b/components/forms/TrackingCodeForm.js index e75260f7..52df3bde 100644 --- a/components/forms/TrackingCodeForm.js +++ b/components/forms/TrackingCodeForm.js @@ -26,7 +26,7 @@ export default function TrackingCodeForm({ values, onClose }) { rows={3} cols={60} spellCheck={false} - defaultValue={``} readOnly diff --git a/components/forms/WebsiteEditForm.js b/components/forms/WebsiteEditForm.js index 80a6dfc0..b56e21c1 100644 --- a/components/forms/WebsiteEditForm.js +++ b/components/forms/WebsiteEditForm.js @@ -38,18 +38,17 @@ const validate = ({ name, domain }) => { }; const OwnerDropDown = ({ user, accounts }) => { - console.info(styles); const { setFieldValue, values } = useFormikContext(); useEffect(() => { - if (values.user_id != null && values.owner === '') { - setFieldValue('owner', values.user_id.toString()); - } else if (user?.user_id && values.owner === '') { - setFieldValue('owner', user.user_id.toString()); + if (values.userId != null && values.owner === '') { + setFieldValue('owner', values.userId.toString()); + } else if (user?.id && values.owner === '') { + setFieldValue('owner', user.id.toString()); } }, [accounts, setFieldValue, user, values]); - if (user?.is_admin) { + if (user?.isAdmin) { return (