From 36cd5a393a5454f8e2fc023c7ee4451ef282c249 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 18 Sep 2020 19:53:50 -0700 Subject: [PATCH 1/3] Convert accounts dashboard button into a link. --- assets/external-link.svg | 1 + components/settings/AccountSettings.js | 33 +++++++++++-------- .../settings/AccountSettings.module.css | 1 + package.json | 2 +- 4 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 assets/external-link.svg diff --git a/assets/external-link.svg b/assets/external-link.svg new file mode 100644 index 00000000..ed09306f --- /dev/null +++ b/assets/external-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/settings/AccountSettings.js b/components/settings/AccountSettings.js index bc555a0f..2471ba21 100644 --- a/components/settings/AccountSettings.js +++ b/components/settings/AccountSettings.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useRouter } from 'next/router'; +import Link from 'next/link'; import classNames from 'classnames'; import PageHeader from 'components/layout/PageHeader'; import Button from 'components/common/Button'; @@ -16,11 +16,10 @@ import Pen from 'assets/pen.svg'; import Plus from 'assets/plus.svg'; import Trash from 'assets/trash.svg'; import Check from 'assets/check.svg'; -import List from 'assets/list-ul.svg'; +import LinkIcon from 'assets/external-link.svg'; import styles from './AccountSettings.module.css'; export default function AccountSettings() { - const router = useRouter(); const [addAccount, setAddAccount] = useState(); const [editAccount, setEditAccount] = useState(); const [deleteAccount, setDeleteAccount] = useState(); @@ -30,16 +29,18 @@ export default function AccountSettings() { const Checkmark = ({ is_admin }) => (is_admin ? } size="medium" /> : null); + const DashboardLink = row => + row.is_admin ? null : ( + + + } /> + + + ); + const Buttons = row => row.username !== 'admin' ? ( - + + ); +} diff --git a/components/settings/DateRangeSetting.module.css b/components/settings/DateRangeSetting.module.css new file mode 100644 index 00000000..230e7c97 --- /dev/null +++ b/components/settings/DateRangeSetting.module.css @@ -0,0 +1,3 @@ +.button { + margin-left: 10px; +} diff --git a/components/settings/ProfileSettings.js b/components/settings/ProfileSettings.js index 10386318..f28226c5 100644 --- a/components/settings/ProfileSettings.js +++ b/components/settings/ProfileSettings.js @@ -1,40 +1,27 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import PageHeader from 'components/layout/PageHeader'; import Button from 'components/common/Button'; import Modal from 'components/common/Modal'; import Toast from 'components/common/Toast'; import ChangePasswordForm from 'components/forms/ChangePasswordForm'; -import DateFilter from 'components/common/DateFilter'; +import TimezoneSetting from 'components/settings/TimezoneSetting'; import Dots from 'assets/ellipsis-h.svg'; -import { getTimezone } from 'lib/date'; -import { setItem } from 'lib/web'; -import useDateRange from 'hooks/useDateRange'; -import { setDateRange } from 'redux/actions/websites'; import styles from './ProfileSettings.module.css'; +import DateRangeSetting from './DateRangeSetting'; export default function ProfileSettings() { - const dispatch = useDispatch(); const user = useSelector(state => state.user); const [changePassword, setChangePassword] = useState(false); const [message, setMessage] = useState(); const { user_id } = user; - const timezone = getTimezone(); - const dateRange = useDateRange(0); - const { startDate, endDate, value } = dateRange; function handleSave() { setChangePassword(false); setMessage(); } - function handleDateChange(values) { - const { value } = values; - setItem(`umami.date-range`, value === 'custom' ? values : value); - dispatch(setDateRange(0, values)); - } - return ( <> @@ -47,7 +34,7 @@ export default function ProfileSettings() { -
+
@@ -55,17 +42,14 @@ export default function ProfileSettings() {
-
{timezone}
+
+ +
-
- +
+
{changePassword && ( diff --git a/components/settings/ProfileSettings.module.css b/components/settings/ProfileSettings.module.css index ea7711ac..fdd8252f 100644 --- a/components/settings/ProfileSettings.module.css +++ b/components/settings/ProfileSettings.module.css @@ -1,3 +1,3 @@ -.date { +.list dd { display: flex; } diff --git a/components/settings/TimezoneSetting.js b/components/settings/TimezoneSetting.js new file mode 100644 index 00000000..54751a45 --- /dev/null +++ b/components/settings/TimezoneSetting.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { listTimeZones } from 'timezone-support'; +import DropDown from '../common/DropDown'; +import Button from '../common/Button'; +import useTimezone from 'hooks/useTimezone'; +import { getTimezone } from 'lib/date'; +import styles from './TimezoneSetting.module.css'; + +export default function TimezoneSetting() { + const [timezone, saveTimezone] = useTimezone(); + const options = listTimeZones().map(n => ({ label: n, value: n })); + + function handleReset() { + saveTimezone(getTimezone()); + } + + return ( + <> + + + + ); +} diff --git a/components/settings/TimezoneSetting.module.css b/components/settings/TimezoneSetting.module.css new file mode 100644 index 00000000..9561111d --- /dev/null +++ b/components/settings/TimezoneSetting.module.css @@ -0,0 +1,8 @@ +.menu { + max-height: 300px; + overflow-y: auto; +} + +.button { + margin-left: 10px; +} diff --git a/hooks/useDateRange.js b/hooks/useDateRange.js index 13703d9f..77f892de 100644 --- a/hooks/useDateRange.js +++ b/hooks/useDateRange.js @@ -1,24 +1,41 @@ -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { parseISO } from 'date-fns'; import { getDateRange } from 'lib/date'; -import { getItem } from 'lib/web'; +import { getItem, setItem } from 'lib/web'; +import { setDateRange } from '../redux/actions/websites'; +import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants'; +import useForceUpdate from './useForceUpdate'; -export default function useDateRange(websiteId, defaultDateRange = '24hour') { - const globalDefault = getItem('umami.date-range'); +export default function useDateRange(websiteId, defaultDateRange = DEFAULT_DATE_RANGE) { + const dispatch = useDispatch(); + const dateRange = useSelector(state => state.websites[websiteId]?.dateRange); + const forceUpdate = useForceUpdate(); + + const globalDefault = getItem(DATE_RANGE_CONFIG); let globalDateRange; - if (typeof globalDefault === 'string') { - globalDateRange = getDateRange(globalDefault); - } else if (typeof globalDefault === 'object') { - globalDateRange = { - ...globalDefault, - startDate: parseISO(globalDefault.startDate), - endDate: parseISO(globalDefault.endDate), - }; + if (globalDefault) { + if (typeof globalDefault === 'string') { + globalDateRange = getDateRange(globalDefault); + } else if (typeof globalDefault === 'object') { + globalDateRange = { + ...globalDefault, + startDate: parseISO(globalDefault.startDate), + endDate: parseISO(globalDefault.endDate), + }; + } } - return useSelector( - state => - state.websites[websiteId]?.dateRange || globalDateRange || getDateRange(defaultDateRange), - ); + function saveDateRange(values) { + const { value } = values; + + if (websiteId) { + dispatch(setDateRange(websiteId, values)); + } else { + setItem(DATE_RANGE_CONFIG, value === 'custom' ? values : value); + forceUpdate(); + } + } + + return [dateRange || globalDateRange || getDateRange(defaultDateRange), saveDateRange]; } diff --git a/hooks/useForceUpdate.js b/hooks/useForceUpdate.js new file mode 100644 index 00000000..2b8d6101 --- /dev/null +++ b/hooks/useForceUpdate.js @@ -0,0 +1,9 @@ +import { useCallback, useState } from 'react'; + +export default function useForceUpdate() { + const [, update] = useState(Object.create(null)); + + return useCallback(() => { + update(Object.create(null)); + }, [update]); +} diff --git a/hooks/useLocale.js b/hooks/useLocale.js index 185c1f19..788b9b28 100644 --- a/hooks/useLocale.js +++ b/hooks/useLocale.js @@ -1,11 +1,14 @@ import { useDispatch, useSelector } from 'react-redux'; import { updateApp } from 'redux/actions/app'; +import { setItem } from 'lib/web'; +import { LOCALE_CONFIG } from 'lib/constants'; export default function useLocale() { const locale = useSelector(state => state.app.locale); const dispatch = useDispatch(); function setLocale(value) { + setItem(LOCALE_CONFIG, value); dispatch(updateApp({ locale: value })); } diff --git a/hooks/useTimezone.js b/hooks/useTimezone.js new file mode 100644 index 00000000..5de39f9a --- /dev/null +++ b/hooks/useTimezone.js @@ -0,0 +1,17 @@ +import { useState, useCallback } from 'react'; +import { getTimezone } from 'lib/date'; +import { getItem, setItem } from 'lib/web'; + +export default function useTimezone() { + const [timezone, setTimezone] = useState(getItem('umami.timezone') || getTimezone()); + + const saveTimezone = useCallback( + value => { + setItem('umami.timezone', value); + setTimezone(value); + }, + [setTimezone], + ); + + return [timezone, saveTimezone]; +} diff --git a/lang/de-DE.json b/lang/de-DE.json index 75d2b1ea..96ba0f99 100644 --- a/lang/de-DE.json +++ b/lang/de-DE.json @@ -11,6 +11,7 @@ "button.login": "Anmelden", "button.more": "Mehr", "button.refresh": "Aktualisieren", + "button.reset": "Reset", "button.save": "Speichern", "button.single-day": "Ein Tag", "button.view-details": "Details anzeigen", diff --git a/lang/en-US.json b/lang/en-US.json index a583e05c..54ee8cad 100644 --- a/lang/en-US.json +++ b/lang/en-US.json @@ -11,6 +11,7 @@ "button.login": "Login", "button.more": "More", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Save", "button.single-day": "Single day", "button.view-details": "View details", diff --git a/lang/es-MX.json b/lang/es-MX.json index 4a7cdd57..ed8cf1a2 100644 --- a/lang/es-MX.json +++ b/lang/es-MX.json @@ -11,6 +11,7 @@ "button.login": "Iniciar sesión", "button.more": "Más", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Guardar", "button.single-day": "Single day", "button.view-details": "Ver detalles", diff --git a/lang/fr-FR.json b/lang/fr-FR.json index 7536e839..8cfedb33 100644 --- a/lang/fr-FR.json +++ b/lang/fr-FR.json @@ -11,6 +11,7 @@ "button.login": "Connexion", "button.more": "Plus", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Sauvegarder", "button.single-day": "Single day", "button.view-details": "Voir les details", diff --git a/lang/ja-JP.json b/lang/ja-JP.json index a9318697..27d3ca34 100644 --- a/lang/ja-JP.json +++ b/lang/ja-JP.json @@ -11,6 +11,7 @@ "button.login": "ログイン", "button.more": "さらに表示", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "保存", "button.single-day": "Single day", "button.view-details": "詳細表示", diff --git a/lang/mn-MN.json b/lang/mn-MN.json index 2a354fe4..0dd543bf 100644 --- a/lang/mn-MN.json +++ b/lang/mn-MN.json @@ -11,6 +11,7 @@ "button.login": "Нэвтрэх", "button.more": "Цааш", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Хадгалах", "button.single-day": "Single day", "button.view-details": "Дэлгэрүүлж харах", diff --git a/lang/nl-NL.json b/lang/nl-NL.json index e26c5266..2c49098c 100644 --- a/lang/nl-NL.json +++ b/lang/nl-NL.json @@ -11,6 +11,7 @@ "button.login": "Inloggen", "button.more": "Toon meer", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Opslaan", "button.single-day": "Single day", "button.view-details": "Meer details", diff --git a/lang/ru-RU.json b/lang/ru-RU.json index 0fd1b449..5cbedf0b 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -11,6 +11,7 @@ "button.login": "Войти", "button.more": "Больше", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Сохранить", "button.single-day": "Single day", "button.view-details": "Посмотреть детали", diff --git a/lang/tr-TR.json b/lang/tr-TR.json index 36c3b58c..6a93d1dd 100644 --- a/lang/tr-TR.json +++ b/lang/tr-TR.json @@ -11,6 +11,7 @@ "button.login": "Giriş Yap", "button.more": "Detaylı göster", "button.refresh": "Refresh", + "button.reset": "Reset", "button.save": "Kaydet", "button.single-day": "Single day", "button.view-details": "Detayı incele", diff --git a/lang/zh-CN.json b/lang/zh-CN.json index cbcc6f1c..9c6ddda4 100644 --- a/lang/zh-CN.json +++ b/lang/zh-CN.json @@ -11,6 +11,7 @@ "button.login": "登录", "button.more": "更多", "button.refresh": "刷新", + "button.reset": "Reset", "button.save": "保存", "button.single-day": "单日", "button.view-details": "查看更多", diff --git a/lib/constants.js b/lib/constants.js index e9641c68..0e2468ab 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,4 +1,9 @@ export const AUTH_COOKIE_NAME = 'umami.auth'; +export const LOCALE_CONFIG = 'umami.locale'; +export const TIMEZONE_CONFIG = 'umami.timezone'; +export const DATE_RANGE_CONFIG = 'umami.date-range'; + +export const DEFAULT_DATE_RANGE = '24hour'; export const POSTGRESQL = 'postgresql'; export const MYSQL = 'mysql'; diff --git a/package.json b/package.json index 37209cea..547537f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.41.0", + "version": "0.42.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", @@ -83,6 +83,7 @@ "redux-thunk": "^2.3.0", "request-ip": "^2.1.3", "thenby": "^1.3.4", + "timezone-support": "^2.0.2", "tinycolor2": "^1.4.1", "unfetch": "^4.1.0", "uuid": "^8.3.0" diff --git a/redux/actions/app.js b/redux/actions/app.js index 72636a74..cbb8285b 100644 --- a/redux/actions/app.js +++ b/redux/actions/app.js @@ -1,9 +1,10 @@ import { createSlice } from '@reduxjs/toolkit'; import { getItem } from 'lib/web'; +import { LOCALE_CONFIG } from 'lib/constants'; const app = createSlice({ name: 'app', - initialState: { locale: getItem('umami.locale') || 'en-US' }, + initialState: { locale: getItem(LOCALE_CONFIG) || 'en-US' }, reducers: { updateApp(state, action) { state = action.payload; diff --git a/yarn.lock b/yarn.lock index da2fa5cb..201c7e5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2699,6 +2699,11 @@ commander@2, commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + commander@^6.0.0, commander@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc" @@ -8608,6 +8613,13 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" +timezone-support@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/timezone-support/-/timezone-support-2.0.2.tgz#801d6924478b1b60f09b90699ce1127a6044cbe7" + integrity sha512-J/1PyHCX76vOPuJzCyHMQMH2wTjXCJ30R5EXaS/QTi+xYsL0thS0pubDrHCWnfG4zU1jpPJtctnBBRCOpcJZeQ== + dependencies: + commander "2.20.0" + tiny-lru@7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24"