mirror of
https://github.com/umami-software/umami.git
synced 2026-02-08 14:47:14 +01:00
Merge branch 'dev' into analytics
This commit is contained in:
commit
f16d74e25d
27 changed files with 144 additions and 94 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics';
|
||||
import { useMessages, useFilters, useFormat } from 'components/hooks';
|
||||
import { useMessages, useFilters, useFormat, useLocale } from 'components/hooks';
|
||||
import styles from './FieldFilterForm.module.css';
|
||||
|
||||
export default function FieldFilterForm({
|
||||
|
|
@ -16,14 +16,30 @@ export default function FieldFilterForm({
|
|||
const [value, setValue] = useState();
|
||||
const { getFilters } = useFilters();
|
||||
const { formatValue } = useFormat();
|
||||
const { locale } = useLocale();
|
||||
const filters = getFilters(type);
|
||||
|
||||
const formattedValues = useMemo(() => {
|
||||
const formatted = {};
|
||||
const format = val => {
|
||||
formatted[val] = formatValue(val, name);
|
||||
return formatted[val];
|
||||
};
|
||||
if (values.length !== 1) {
|
||||
const { compare } = new Intl.Collator(locale, { numeric: true });
|
||||
values.sort((a, b) => compare(formatted[a] ?? format(a), formatted[b] ?? format(b)));
|
||||
} else {
|
||||
format(values[0]);
|
||||
}
|
||||
return formatted;
|
||||
}, [values]);
|
||||
|
||||
const renderFilterValue = value => {
|
||||
return filters.find(f => f.value === value)?.label;
|
||||
};
|
||||
|
||||
const renderValue = value => {
|
||||
return formatValue(value, name);
|
||||
return formattedValues[value];
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
|
|
@ -59,7 +75,7 @@ export default function FieldFilterForm({
|
|||
}}
|
||||
>
|
||||
{value => {
|
||||
return <Item key={value}>{formatValue(value, name)}</Item>;
|
||||
return <Item key={value}>{formattedValues[value]}</Item>;
|
||||
}}
|
||||
</Dropdown>
|
||||
</Flexbox>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { Button, Icon, Text, Modal, Icons, ModalTrigger } from 'react-basics';
|
||||
import { Button, Icon, Text, Modal, Icons, ModalTrigger, useToasts } from 'react-basics';
|
||||
import UserAddForm from './UserAddForm';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import { setValue } from 'store/cache';
|
||||
|
||||
export function UserAddButton({ onSave }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { showToast } = useToasts();
|
||||
|
||||
const handleSave = () => {
|
||||
onSave();
|
||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||
setValue('users', Date.now());
|
||||
onSave?.();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export function WebsiteSettings({ websiteId, openExternal = false, analyticsUrl
|
|||
|
||||
const handleReset = async value => {
|
||||
if (value === 'delete') {
|
||||
await router.push('/settings/websites');
|
||||
router.push('/settings/websites');
|
||||
} else if (value === 'reset') {
|
||||
showSuccess();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Text, Icon } from 'react-basics';
|
||||
import { Button, Text, Icon, Icons } from 'react-basics';
|
||||
import { useMemo } from 'react';
|
||||
import { firstBy } from 'thenby';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -7,7 +7,6 @@ import useDashboard from 'store/dashboard';
|
|||
import WebsiteHeader from './WebsiteHeader';
|
||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||
import { useMessages, useLocale } from 'components/hooks';
|
||||
import Icons from 'components/icons';
|
||||
|
||||
export default function WebsiteChartList({ websites, showCharts, limit }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export function ErrorMessage() {
|
|||
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
<Icon className={styles.icon} size="large">
|
||||
<Icon className={styles.icon} size="lg">
|
||||
<Icons.Alert />
|
||||
</Icon>
|
||||
<Text>{formatMessage(messages.error)}</Text>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import styles from './FilterLink.module.css';
|
|||
export interface FilterLinkProps {
|
||||
id: string;
|
||||
value: string;
|
||||
label: string;
|
||||
externalUrl: string;
|
||||
className: string;
|
||||
label?: string;
|
||||
externalUrl?: string;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Button, Icon } from 'react-basics';
|
||||
import { Button, Icon, Icons } from 'react-basics';
|
||||
import { useState } from 'react';
|
||||
import MobileMenu from './MobileMenu';
|
||||
import Icons from 'components/icons';
|
||||
|
||||
export function HamburgerButton({ menuItems }: { menuItems: any[] }) {
|
||||
const [active, setActive] = useState(false);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import User from 'assets/user.svg';
|
|||
import Users from 'assets/users.svg';
|
||||
import Visitor from 'assets/visitor.svg';
|
||||
|
||||
const icons: any = {
|
||||
const icons = {
|
||||
...Icons,
|
||||
AddUser,
|
||||
Bars,
|
||||
|
|
|
|||
|
|
@ -36,9 +36,4 @@
|
|||
.header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-basis: 100%;
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ import { safeDecodeURI } from 'next-basics';
|
|||
import { Button, Icon, Icons, Text } from 'react-basics';
|
||||
import useNavigation from 'components/hooks/useNavigation';
|
||||
import useMessages from 'components/hooks/useMessages';
|
||||
import useFormat from 'components/hooks/useFormat';
|
||||
import styles from './FilterTags.module.css';
|
||||
|
||||
export function FilterTags({ params }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
const {
|
||||
router,
|
||||
makeUrl,
|
||||
|
|
@ -34,7 +36,7 @@ export function FilterTags({ params }) {
|
|||
return (
|
||||
<div key={key} className={styles.tag} onClick={() => handleCloseFilter(key)}>
|
||||
<Text>
|
||||
<b>{`${key}`}</b> = {`${safeDecodeURI(params[key])}`}
|
||||
<b>{formatMessage(labels[key])}</b> = {formatValue(safeDecodeURI(params[key]), key)}
|
||||
</Text>
|
||||
<Icon>
|
||||
<Icons.Close />
|
||||
|
|
|
|||
|
|
@ -24,3 +24,7 @@
|
|||
.tag:hover {
|
||||
background: var(--blue200);
|
||||
}
|
||||
|
||||
.tag b {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ export function incrementDateRange(value, increment) {
|
|||
|
||||
const { num, unit } = selectedUnit;
|
||||
|
||||
const sub = num * increment;
|
||||
const sub = Math.abs(num) * increment;
|
||||
|
||||
switch (unit) {
|
||||
case 'hour':
|
||||
|
|
|
|||
|
|
@ -107,11 +107,16 @@ export async function getLocation(ip, req) {
|
|||
const result = lookup.get(ip);
|
||||
|
||||
if (result) {
|
||||
const country = result.country?.iso_code ?? result?.registered_country?.iso_code;
|
||||
const subdivision1 = result.subdivisions?.[0]?.iso_code;
|
||||
const subdivision2 = result.subdivisions?.[1]?.names?.en;
|
||||
const city = result.city?.names?.en;
|
||||
|
||||
return {
|
||||
country: result.country?.iso_code ?? result?.registered_country?.iso_code,
|
||||
subdivision1: result.subdivisions?.[0]?.iso_code,
|
||||
subdivision2: result.subdivisions?.[1]?.names?.en,
|
||||
city: result.city?.names?.en,
|
||||
country,
|
||||
subdivision1: getRegionCode(country, subdivision1),
|
||||
subdivision2,
|
||||
city,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,9 @@ export const useAuth = createMiddleware(async (req, res, next) => {
|
|||
} else if (redis.enabled && authKey) {
|
||||
const key = await redis.client.get(authKey);
|
||||
|
||||
user = await getUserById(key?.userId);
|
||||
if (key?.userId) {
|
||||
user = await getUserById(key.userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@ const POSTGRESQL_DATE_FORMATS = {
|
|||
year: 'YYYY-01-01',
|
||||
};
|
||||
|
||||
function getAddMinutesQuery(field: string, minutes: number): string {
|
||||
function getAddIntervalQuery(field: string, interval: string): string {
|
||||
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
return `${field} + interval '${minutes} minute'`;
|
||||
return `${field} + interval '${interval}'`;
|
||||
}
|
||||
|
||||
if (db === MYSQL) {
|
||||
return `DATE_ADD(${field}, interval ${minutes} minute)`;
|
||||
return `DATE_ADD(${field}, interval ${interval})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,15 +80,15 @@ function getDateQuery(field: string, unit: string, timezone?: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
function getTimestampIntervalQuery(field: string): string {
|
||||
function getTimestampDiffQuery(field1: string, field2: string): string {
|
||||
const db = getDatabaseType();
|
||||
|
||||
if (db === POSTGRESQL) {
|
||||
return `floor(extract(epoch from max(${field}) - min(${field})))`;
|
||||
return `floor(extract(epoch from (${field2} - ${field1})))`;
|
||||
}
|
||||
|
||||
if (db === MYSQL) {
|
||||
return `floor(unix_timestamp(max(${field})) - unix_timestamp(min(${field})))`;
|
||||
return `timestampdiff(second, ${field1}, ${field2})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -216,11 +216,11 @@ function getSearchMode(): { mode?: Prisma.QueryMode } {
|
|||
|
||||
export default {
|
||||
...prisma,
|
||||
getAddMinutesQuery,
|
||||
getAddIntervalQuery,
|
||||
getDayDiffQuery,
|
||||
getCastColumnQuery,
|
||||
getDateQuery,
|
||||
getTimestampIntervalQuery,
|
||||
getTimestampDiffQuery,
|
||||
getFilterQuery,
|
||||
parseFilters,
|
||||
getPageFilters,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import moment from 'moment';
|
||||
import moment from 'moment-timezone';
|
||||
import * as yup from 'yup';
|
||||
import { UNIT_TYPES } from './constants';
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
async function clickhouseQuery(
|
||||
websiteId: string,
|
||||
filters: QueryFilters,
|
||||
): Promise<{ events: number; fields: number; records: number }> {
|
||||
): Promise<{ events: number; fields: number; records: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, params } = await parseFilters(websiteId, filters);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ import { md5 } from 'next-basics';
|
|||
import { getSessions, getEvents } from 'queries/index';
|
||||
import { EVENT_TYPE } from 'lib/constants';
|
||||
|
||||
export async function getRealtimeData(websiteId, time) {
|
||||
export async function getRealtimeData(websiteId: string, startDate: Date) {
|
||||
const [pageviews, sessions, events] = await Promise.all([
|
||||
getEvents(websiteId, time, EVENT_TYPE.pageView),
|
||||
getSessions(websiteId, time),
|
||||
getEvents(websiteId, time, EVENT_TYPE.customEvent),
|
||||
getEvents(websiteId, startDate, EVENT_TYPE.pageView),
|
||||
getSessions(websiteId, startDate),
|
||||
getEvents(websiteId, startDate, EVENT_TYPE.customEvent),
|
||||
]);
|
||||
|
||||
const decorate = (id, data) => {
|
||||
return data.map(props => ({
|
||||
const decorate = (id: string, data: any[]) => {
|
||||
return data.map((props: { [key: string]: any }) => ({
|
||||
...props,
|
||||
__id: md5(id, ...Object.values(props)),
|
||||
__type: id,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ export async function getWebsiteStats(...args: [websiteId: string, filters: Quer
|
|||
}
|
||||
|
||||
async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
||||
const { getDateQuery, getTimestampIntervalQuery, parseFilters, rawQuery } = prisma;
|
||||
const { getDateQuery, getAddIntervalQuery, getTimestampDiffQuery, parseFilters, rawQuery } =
|
||||
prisma;
|
||||
const { filterQuery, joinSession, params } = await parseFilters(websiteId, {
|
||||
...filters,
|
||||
eventType: EVENT_TYPE.pageView,
|
||||
|
|
@ -24,13 +25,16 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
|
|||
sum(t.c) as "pageviews",
|
||||
count(distinct t.session_id) as "uniques",
|
||||
sum(case when t.c = 1 then 1 else 0 end) as "bounces",
|
||||
sum(t.time) as "totaltime"
|
||||
sum(case when t.max_time < ${getAddIntervalQuery('t.min_time', '1 hour')}
|
||||
then ${getTimestampDiffQuery('t.min_time', 't.max_time')}
|
||||
else 0 end) as "totaltime"
|
||||
from (
|
||||
select
|
||||
website_event.session_id,
|
||||
${getDateQuery('website_event.created_at', 'hour')},
|
||||
count(*) as c,
|
||||
${getTimestampIntervalQuery('website_event.created_at')} as "time"
|
||||
${getDateQuery('website_event.created_at', 'day')},
|
||||
count(*) as "c",
|
||||
min(website_event.created_at) as "min_time",
|
||||
max(website_event.created_at) as "max_time"
|
||||
from website_event
|
||||
join website
|
||||
on website_event.website_id = website.website_id
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ async function relationalQuery(
|
|||
}[]
|
||||
> {
|
||||
const { windowMinutes, startDate, endDate, urls } = criteria;
|
||||
const { rawQuery, getAddMinutesQuery } = prisma;
|
||||
const { rawQuery, getAddIntervalQuery } = prisma;
|
||||
const { levelQuery, sumQuery } = getFunnelQuery(urls, windowMinutes);
|
||||
|
||||
function getFunnelQuery(
|
||||
|
|
@ -58,9 +58,9 @@ async function relationalQuery(
|
|||
join website_event we
|
||||
on l.session_id = we.session_id
|
||||
where we.website_id = {{websiteId::uuid}}
|
||||
and we.created_at between l.created_at and ${getAddMinutesQuery(
|
||||
and we.created_at between l.created_at and ${getAddIntervalQuery(
|
||||
`l.created_at `,
|
||||
windowMinutes,
|
||||
`${windowMinutes} minute`,
|
||||
)}
|
||||
and we.referrer_path = {{${i - 1}}}
|
||||
and we.url_path = {{${i}}}
|
||||
|
|
|
|||
|
|
@ -83,10 +83,17 @@ async function clickhouseQuery(
|
|||
limit 500
|
||||
`,
|
||||
params,
|
||||
);
|
||||
).then(a => {
|
||||
return Object.values(a).map(a => {
|
||||
return {
|
||||
x: a.x,
|
||||
y: Number(a.y),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseFields(fields) {
|
||||
function parseFields(fields: any[]) {
|
||||
const query = fields.reduce(
|
||||
(arr, field) => {
|
||||
const { name } = field;
|
||||
|
|
@ -99,7 +106,7 @@ function parseFields(fields) {
|
|||
return query.join(',\n');
|
||||
}
|
||||
|
||||
function parseGroupBy(fields) {
|
||||
function parseGroupBy(fields: { name: any }[]) {
|
||||
if (!fields.length) {
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue