mirror of
				https://github.com/mikecao/umami.git
				synced 2021-05-17 18:46:32 +03:00 
			
		
		
		
	Renamed methods. Initial work on realtime dashboard.
This commit is contained in:
		| @@ -12,7 +12,7 @@ export default function RefreshButton({ websiteId }) { | |||||||
|   const dispatch = useDispatch(); |   const dispatch = useDispatch(); | ||||||
|   const [dateRange] = useDateRange(websiteId); |   const [dateRange] = useDateRange(websiteId); | ||||||
|   const [loading, setLoading] = useState(false); |   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() { |   function handleClick() { | ||||||
|     if (dateRange) { |     if (dateRange) { | ||||||
|   | |||||||
| @@ -30,6 +30,9 @@ export default function Header() { | |||||||
|               <Link href="/dashboard"> |               <Link href="/dashboard"> | ||||||
|                 <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" /> |                 <FormattedMessage id="label.dashboard" defaultMessage="Dashboard" /> | ||||||
|               </Link> |               </Link> | ||||||
|  |               <Link href="/realtime"> | ||||||
|  |                 <FormattedMessage id="label.realtime" defaultMessage="Realtime" /> | ||||||
|  |               </Link> | ||||||
|               <Link href="/settings"> |               <Link href="/settings"> | ||||||
|                 <FormattedMessage id="label.settings" defaultMessage="Settings" /> |                 <FormattedMessage id="label.settings" defaultMessage="Settings" /> | ||||||
|               </Link> |               </Link> | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ export default function MetricsBar({ websiteId, token, className }) { | |||||||
|   } = usePageQuery(); |   } = usePageQuery(); | ||||||
|  |  | ||||||
|   const { data, error, loading } = useFetch( |   const { data, error, loading } = useFetch( | ||||||
|     `/api/website/${websiteId}/metrics`, |     `/api/website/${websiteId}/stats`, | ||||||
|     { |     { | ||||||
|       start_at: +startDate, |       start_at: +startDate, | ||||||
|       end_at: +endDate, |       end_at: +endDate, | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ export default function MetricsTable({ | |||||||
|   } = usePageQuery(); |   } = usePageQuery(); | ||||||
|  |  | ||||||
|   const { data, loading, error } = useFetch( |   const { data, loading, error } = useFetch( | ||||||
|     `/api/website/${websiteId}/rankings`, |     `/api/website/${websiteId}/metrics`, | ||||||
|     { |     { | ||||||
|       type, |       type, | ||||||
|       start_at: +startDate, |       start_at: +startDate, | ||||||
|   | |||||||
							
								
								
									
										80
									
								
								components/metrics/RealtimeChart.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								components/metrics/RealtimeChart.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import { useIntl } from 'react-intl'; | ||||||
|  | import tinycolor from 'tinycolor2'; | ||||||
|  | import BarChart from './BarChart'; | ||||||
|  | import useTheme from 'hooks/useTheme'; | ||||||
|  | import { THEME_COLORS } from 'lib/constants'; | ||||||
|  |  | ||||||
|  | export default function RealtimeChart({ websiteId, data, unit, records, className, loading }) { | ||||||
|  |   const intl = useIntl(); | ||||||
|  |   const [theme] = useTheme(); | ||||||
|  |   const primaryColor = tinycolor(THEME_COLORS[theme].primary); | ||||||
|  |   const colors = { | ||||||
|  |     views: { | ||||||
|  |       background: primaryColor.setAlpha(0.4).toRgbString(), | ||||||
|  |       border: primaryColor.setAlpha(0.5).toRgbString(), | ||||||
|  |     }, | ||||||
|  |     visitors: { | ||||||
|  |       background: primaryColor.setAlpha(0.6).toRgbString(), | ||||||
|  |       border: primaryColor.setAlpha(0.7).toRgbString(), | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleUpdate = chart => { | ||||||
|  |     const { | ||||||
|  |       data: { datasets }, | ||||||
|  |     } = chart; | ||||||
|  |  | ||||||
|  |     datasets[0].data = data.uniques; | ||||||
|  |     datasets[0].label = intl.formatMessage({ | ||||||
|  |       id: 'metrics.unique-visitors', | ||||||
|  |       defaultMessage: 'Unique visitors', | ||||||
|  |     }); | ||||||
|  |     datasets[1].data = data.pageviews; | ||||||
|  |     datasets[1].label = intl.formatMessage({ | ||||||
|  |       id: 'metrics.page-views', | ||||||
|  |       defaultMessage: 'Page views', | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     chart.update(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   if (!data) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <BarChart | ||||||
|  |       className={className} | ||||||
|  |       chartId={`realtime-${websiteId}`} | ||||||
|  |       datasets={[ | ||||||
|  |         { | ||||||
|  |           label: intl.formatMessage({ | ||||||
|  |             id: 'metrics.unique-visitors', | ||||||
|  |             defaultMessage: 'Unique visitors', | ||||||
|  |           }), | ||||||
|  |           data: data.uniques, | ||||||
|  |           lineTension: 0, | ||||||
|  |           backgroundColor: colors.visitors.background, | ||||||
|  |           borderColor: colors.visitors.border, | ||||||
|  |           borderWidth: 1, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           label: intl.formatMessage({ | ||||||
|  |             id: 'metrics.page-views', | ||||||
|  |             defaultMessage: 'Page views', | ||||||
|  |           }), | ||||||
|  |           data: data.pageviews, | ||||||
|  |           lineTension: 0, | ||||||
|  |           backgroundColor: colors.views.background, | ||||||
|  |           borderColor: colors.views.border, | ||||||
|  |           borderWidth: 1, | ||||||
|  |         }, | ||||||
|  |       ]} | ||||||
|  |       unit={unit} | ||||||
|  |       records={records} | ||||||
|  |       onUpdate={handleUpdate} | ||||||
|  |       loading={loading} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										36
									
								
								components/pages/RealtimeDashboard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								components/pages/RealtimeDashboard.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | import React, { useState } from 'react'; | ||||||
|  | import { useSelector } from 'react-redux'; | ||||||
|  | import Page from '../layout/Page'; | ||||||
|  | import PageHeader from '../layout/PageHeader'; | ||||||
|  | import useFetch from '../../hooks/useFetch'; | ||||||
|  | import DropDown from '../common/DropDown'; | ||||||
|  | import RealtimeChart from '../metrics/RealtimeChart'; | ||||||
|  |  | ||||||
|  | export default function TestConsole() { | ||||||
|  |   const user = useSelector(state => state.user); | ||||||
|  |   const [website, setWebsite] = useState(); | ||||||
|  |   const { data } = useFetch('/api/websites'); | ||||||
|  |  | ||||||
|  |   if (!data || !user?.is_admin) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const options = [{ label: 'All websites', value: 0 }].concat( | ||||||
|  |     data.map(({ name, website_id }) => ({ label: name, value: website_id })), | ||||||
|  |   ); | ||||||
|  |   const selectedValue = options.find(({ value }) => value === website?.website_id)?.value || 0; | ||||||
|  |  | ||||||
|  |   function handleSelect(value) { | ||||||
|  |     setWebsite(data.find(({ website_id }) => website_id === value)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Page> | ||||||
|  |       <PageHeader> | ||||||
|  |         <div>Real time</div> | ||||||
|  |         <DropDown value={selectedValue} options={options} onChange={handleSelect} /> | ||||||
|  |       </PageHeader> | ||||||
|  |       <RealtimeChart websiteId={website?.website_id} /> | ||||||
|  |     </Page> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								components/pages/RealtimeDashboard.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								components/pages/RealtimeDashboard.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | .container { | ||||||
|  |   display: flex; | ||||||
|  | } | ||||||
| @@ -7,7 +7,7 @@ import Page from '../layout/Page'; | |||||||
| import PageHeader from '../layout/PageHeader'; | import PageHeader from '../layout/PageHeader'; | ||||||
| import useFetch from '../../hooks/useFetch'; | import useFetch from '../../hooks/useFetch'; | ||||||
| import DropDown from '../common/DropDown'; | import DropDown from '../common/DropDown'; | ||||||
| import styles from './Test.module.css'; | import styles from './TestConsole.module.css'; | ||||||
| import WebsiteChart from '../metrics/WebsiteChart'; | import WebsiteChart from '../metrics/WebsiteChart'; | ||||||
| import EventsChart from '../metrics/EventsChart'; | import EventsChart from '../metrics/EventsChart'; | ||||||
| import Button from '../common/Button'; | import Button from '../common/Button'; | ||||||
|   | |||||||
| @@ -285,7 +285,7 @@ export async function createAccount(data) { | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getMetrics(website_id, start_at, end_at, filters = {}) { | export function getWebsiteStats(website_id, start_at, end_at, filters = {}) { | ||||||
|   const params = [website_id, start_at, end_at]; |   const params = [website_id, start_at, end_at]; | ||||||
|   const { url } = filters; |   const { url } = filters; | ||||||
|   let urlFilter = ''; |   let urlFilter = ''; | ||||||
| @@ -317,7 +317,7 @@ export function getMetrics(website_id, start_at, end_at, filters = {}) { | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getPageviews( | export function getPageviewStats( | ||||||
|   website_id, |   website_id, | ||||||
|   start_at, |   start_at, | ||||||
|   end_at, |   end_at, | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								pages/api/realtime.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								pages/api/realtime.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | import { useAuth } from 'lib/middleware'; | ||||||
|  | import { ok, unauthorized, methodNotAllowed } from 'lib/response'; | ||||||
|  |  | ||||||
|  | export default async (req, res) => { | ||||||
|  |   await useAuth(req, res); | ||||||
|  |  | ||||||
|  |   const { is_admin } = req.auth; | ||||||
|  |  | ||||||
|  |   if (!is_admin) { | ||||||
|  |     return unauthorized(res); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (req.method === 'GET') { | ||||||
|  |     const [pageviews, sessions, events] = await Promise.all([[], [], []]); | ||||||
|  |  | ||||||
|  |     return ok(res, { pageviews, sessions, events }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return methodNotAllowed(res); | ||||||
|  | }; | ||||||
| @@ -1,27 +1,67 @@ | |||||||
| import { getMetrics } from 'lib/queries'; | import { getPageviewMetrics, getSessionMetrics } from 'lib/queries'; | ||||||
| import { methodNotAllowed, ok, unauthorized } from 'lib/response'; | import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; | ||||||
|  | import { DOMAIN_REGEX } from 'lib/constants'; | ||||||
| import { allowQuery } from 'lib/auth'; | import { allowQuery } from 'lib/auth'; | ||||||
|  |  | ||||||
|  | const sessionColumns = ['browser', 'os', 'device', 'country']; | ||||||
|  | const pageviewColumns = ['url', 'referrer']; | ||||||
|  |  | ||||||
|  | function getTable(type) { | ||||||
|  |   if (type === 'event') { | ||||||
|  |     return 'event'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (sessionColumns.includes(type)) { | ||||||
|  |     return 'session'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return 'pageview'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getColumn(type) { | ||||||
|  |   if (type === 'event') { | ||||||
|  |     return `concat(event_type, ':', event_value)`; | ||||||
|  |   } | ||||||
|  |   return type; | ||||||
|  | } | ||||||
|  |  | ||||||
| export default async (req, res) => { | export default async (req, res) => { | ||||||
|   if (req.method === 'GET') { |   if (req.method === 'GET') { | ||||||
|     if (!(await allowQuery(req))) { |     if (!(await allowQuery(req))) { | ||||||
|       return unauthorized(res); |       return unauthorized(res); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const { id, start_at, end_at, url } = req.query; |     const { id, type, start_at, end_at, domain, url } = req.query; | ||||||
|  |  | ||||||
|  |     if (domain && !DOMAIN_REGEX.test(domain)) { | ||||||
|  |       return badRequest(res); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const websiteId = +id; |     const websiteId = +id; | ||||||
|     const startDate = new Date(+start_at); |     const startDate = new Date(+start_at); | ||||||
|     const endDate = new Date(+end_at); |     const endDate = new Date(+end_at); | ||||||
|  |  | ||||||
|     const metrics = await getMetrics(websiteId, startDate, endDate, { url }); |     if (sessionColumns.includes(type)) { | ||||||
|  |       const data = await getSessionMetrics(websiteId, startDate, endDate, type, { url }); | ||||||
|  |  | ||||||
|     const stats = Object.keys(metrics[0]).reduce((obj, key) => { |       return ok(res, data); | ||||||
|       obj[key] = Number(metrics[0][key]) || 0; |     } | ||||||
|       return obj; |  | ||||||
|     }, {}); |  | ||||||
|  |  | ||||||
|     return ok(res, stats); |     if (type === 'event' || pageviewColumns.includes(type)) { | ||||||
|  |       const data = await getPageviewMetrics( | ||||||
|  |         websiteId, | ||||||
|  |         startDate, | ||||||
|  |         endDate, | ||||||
|  |         getColumn(type), | ||||||
|  |         getTable(type), | ||||||
|  |         { | ||||||
|  |           domain, | ||||||
|  |           url: type !== 'url' && url, | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       return ok(res, data); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return methodNotAllowed(res); |   return methodNotAllowed(res); | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import moment from 'moment-timezone'; | import moment from 'moment-timezone'; | ||||||
| import { getPageviews } from 'lib/queries'; | import { getPageviewStats } from 'lib/queries'; | ||||||
| import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; | import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; | ||||||
| import { allowQuery } from 'lib/auth'; | import { allowQuery } from 'lib/auth'; | ||||||
|  |  | ||||||
| @@ -22,8 +22,8 @@ export default async (req, res) => { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const [pageviews, uniques] = await Promise.all([ |     const [pageviews, uniques] = await Promise.all([ | ||||||
|       getPageviews(websiteId, startDate, endDate, tz, unit, '*', url), |       getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', url), | ||||||
|       getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url), |       getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url), | ||||||
|     ]); |     ]); | ||||||
|  |  | ||||||
|     return ok(res, { pageviews, uniques }); |     return ok(res, { pageviews, uniques }); | ||||||
|   | |||||||
| @@ -1,68 +0,0 @@ | |||||||
| import { getPageviewMetrics, getSessionMetrics } from 'lib/queries'; |  | ||||||
| import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; |  | ||||||
| import { DOMAIN_REGEX } from 'lib/constants'; |  | ||||||
| import { allowQuery } from 'lib/auth'; |  | ||||||
|  |  | ||||||
| const sessionColumns = ['browser', 'os', 'device', 'country']; |  | ||||||
| const pageviewColumns = ['url', 'referrer']; |  | ||||||
|  |  | ||||||
| function getTable(type) { |  | ||||||
|   if (type === 'event') { |  | ||||||
|     return 'event'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (sessionColumns.includes(type)) { |  | ||||||
|     return 'session'; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return 'pageview'; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function getColumn(type) { |  | ||||||
|   if (type === 'event') { |  | ||||||
|     return `concat(event_type, ':', event_value)`; |  | ||||||
|   } |  | ||||||
|   return type; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default async (req, res) => { |  | ||||||
|   if (req.method === 'GET') { |  | ||||||
|     if (!(await allowQuery(req))) { |  | ||||||
|       return unauthorized(res); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const { id, type, start_at, end_at, domain, url } = req.query; |  | ||||||
|  |  | ||||||
|     if (domain && !DOMAIN_REGEX.test(domain)) { |  | ||||||
|       return badRequest(res); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const websiteId = +id; |  | ||||||
|     const startDate = new Date(+start_at); |  | ||||||
|     const endDate = new Date(+end_at); |  | ||||||
|  |  | ||||||
|     if (sessionColumns.includes(type)) { |  | ||||||
|       const data = await getSessionMetrics(websiteId, startDate, endDate, type, { url }); |  | ||||||
|  |  | ||||||
|       return ok(res, data); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (type === 'event' || pageviewColumns.includes(type)) { |  | ||||||
|       const data = await getPageviewMetrics( |  | ||||||
|         websiteId, |  | ||||||
|         startDate, |  | ||||||
|         endDate, |  | ||||||
|         getColumn(type), |  | ||||||
|         getTable(type), |  | ||||||
|         { |  | ||||||
|           domain, |  | ||||||
|           url: type !== 'url' && url, |  | ||||||
|         }, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       return ok(res, data); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return methodNotAllowed(res); |  | ||||||
| }; |  | ||||||
							
								
								
									
										28
									
								
								pages/api/website/[id]/stats.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								pages/api/website/[id]/stats.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | import { getWebsiteStats } from 'lib/queries'; | ||||||
|  | import { methodNotAllowed, ok, unauthorized } from 'lib/response'; | ||||||
|  | import { allowQuery } from 'lib/auth'; | ||||||
|  |  | ||||||
|  | export default async (req, res) => { | ||||||
|  |   if (req.method === 'GET') { | ||||||
|  |     if (!(await allowQuery(req))) { | ||||||
|  |       return unauthorized(res); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const { id, start_at, end_at, url } = req.query; | ||||||
|  |  | ||||||
|  |     const websiteId = +id; | ||||||
|  |     const startDate = new Date(+start_at); | ||||||
|  |     const endDate = new Date(+end_at); | ||||||
|  |  | ||||||
|  |     const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url }); | ||||||
|  |  | ||||||
|  |     const stats = Object.keys(metrics[0]).reduce((obj, key) => { | ||||||
|  |       obj[key] = Number(metrics[0][key]) || 0; | ||||||
|  |       return obj; | ||||||
|  |     }, {}); | ||||||
|  |  | ||||||
|  |     return ok(res, stats); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return methodNotAllowed(res); | ||||||
|  | }; | ||||||
							
								
								
									
										18
									
								
								pages/realtime.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								pages/realtime.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import Layout from 'components/layout/Layout'; | ||||||
|  | import RealtimeDashboard from 'components/pages/RealtimeDashboard'; | ||||||
|  | import useRequireLogin from 'hooks/useRequireLogin'; | ||||||
|  |  | ||||||
|  | export default function RealtimePage() { | ||||||
|  |   const { loading } = useRequireLogin(); | ||||||
|  |  | ||||||
|  |   if (loading) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Layout> | ||||||
|  |       <RealtimeDashboard /> | ||||||
|  |     </Layout> | ||||||
|  |   ); | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Mike Cao
					Mike Cao