mirror of
https://github.com/umami-software/umami.git
synced 2026-02-18 19:45:35 +01:00
Compare commits
4 commits
78c1fd8283
...
c3b62e3a74
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3b62e3a74 | ||
|
|
8c8e36c63b | ||
|
|
07665f4824 | ||
|
|
0f9669f886 |
57 changed files with 1318 additions and 1328 deletions
16
package.json
16
package.json
|
|
@ -81,11 +81,11 @@
|
|||
"@prisma/extension-read-replicas": "^0.4.1",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@umami/react-zen": "^0.164.0",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"@umami/react-zen": "^0.168.0",
|
||||
"@umami/redis-client": "^0.27.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"chalk": "^5.4.1",
|
||||
"chalk": "^5.6.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"classnames": "^2.3.1",
|
||||
|
|
@ -136,7 +136,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^4.2.29",
|
||||
"@netlify/plugin-nextjs": "^5.11.6",
|
||||
"@netlify/plugin-nextjs": "^5.12.1",
|
||||
"@rollup/plugin-alias": "^5.0.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.4",
|
||||
"@rollup/plugin-json": "^6.0.0",
|
||||
|
|
@ -145,17 +145,17 @@
|
|||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.16.5",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react": "^19.1.11",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.6.6",
|
||||
"esbuild": "^0.25.8",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-config-next": "^14.2.30",
|
||||
"eslint-config-next": "^14.2.32",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
|
|
|
|||
1926
pnpm-lock.yaml
generated
1926
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -70,7 +70,11 @@ export function SideNav(props: SidebarProps) {
|
|||
<Row height="100%" backgroundColor border="right">
|
||||
<Sidebar {...props} isCollapsed={isCollapsed || hasNav} muteItems={false} showBorder={false}>
|
||||
<SidebarSection onClick={() => setIsCollapsed(false)}>
|
||||
<SidebarHeader label="umami" icon={isCollapsed && !hasNav ? <PanelLeft /> : <Logo />}>
|
||||
<SidebarHeader
|
||||
label="umami"
|
||||
icon={isCollapsed && !hasNav ? <PanelLeft /> : <Logo />}
|
||||
style={{ maxHeight: 40 }}
|
||||
>
|
||||
{!isCollapsed && !hasNav && <PanelButton />}
|
||||
</SidebarHeader>
|
||||
</SidebarSection>
|
||||
|
|
@ -81,7 +85,12 @@ export function SideNav(props: SidebarProps) {
|
|||
{links.map(({ id, path, label, icon }) => {
|
||||
return (
|
||||
<Link key={id} href={renderUrl(path, false)} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={pathname.endsWith(path)} />
|
||||
<SidebarItem
|
||||
label={label}
|
||||
icon={icon}
|
||||
isSelected={pathname.endsWith(path)}
|
||||
role="button"
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
|
@ -90,7 +99,12 @@ export function SideNav(props: SidebarProps) {
|
|||
{bottomLinks.map(({ id, path, label, icon }) => {
|
||||
return (
|
||||
<Link key={id} href={path} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={pathname.includes(path)} />
|
||||
<SidebarItem
|
||||
label={label}
|
||||
icon={icon}
|
||||
isSelected={pathname.includes(path)}
|
||||
role="button"
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,11 @@ import {
|
|||
PasswordField,
|
||||
Button,
|
||||
} from '@umami/react-zen';
|
||||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
|
||||
export function UserAddForm({ onSave, onClose }) {
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post(`/users`, data),
|
||||
});
|
||||
const { mutate, error, isPending } = useUpdateQuery(`/users`);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { AlertDialog, Row } from '@umami/react-zen';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
|
||||
|
||||
export function UserDeleteForm({
|
||||
userId,
|
||||
|
|
@ -13,8 +13,7 @@ export function UserDeleteForm({
|
|||
onClose?: () => void;
|
||||
}) {
|
||||
const { messages, labels, formatMessage } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate } = useMutation({ mutationFn: () => del(`/users/${userId}`) });
|
||||
const { mutate } = useDeleteQuery(`/users/${userId}`);
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
|
|
|
|||
|
|
@ -7,30 +7,16 @@ import {
|
|||
TextField,
|
||||
FormSubmitButton,
|
||||
PasswordField,
|
||||
useToast,
|
||||
} from '@umami/react-zen';
|
||||
import { useApi, useLoginQuery, useMessages, useModified, useUser } from '@/components/hooks';
|
||||
import { useLoginQuery, useMessages, useUpdateQuery, useUser } from '@/components/hooks';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
|
||||
export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () => void }) {
|
||||
const { formatMessage, labels, messages, getMessage } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const user = useUser();
|
||||
const { user: login } = useLoginQuery();
|
||||
const { toast } = useToast();
|
||||
const { touch } = useModified();
|
||||
|
||||
const { mutate, error } = useMutation({
|
||||
mutationFn: ({
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
}: {
|
||||
username: string;
|
||||
password: string;
|
||||
role: string;
|
||||
}) => post(`/users/${userId}`, { username, password, role }),
|
||||
});
|
||||
const { mutate, error, toast, touch } = useUpdateQuery(`/users/${userId}`);
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Form, FormField, FormSubmitButton, Row, TextField, Button } from '@umami/react-zen';
|
||||
import { useApi } from '@/components/hooks';
|
||||
import { useUpdateQuery } from '@/components/hooks';
|
||||
import { DOMAIN_REGEX } from '@/lib/constants';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
|
|
@ -13,10 +13,7 @@ export function BoardAddForm({
|
|||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post('/websites', { ...data, teamId }),
|
||||
});
|
||||
const { mutate, error, isPending } = useUpdateQuery('/websites', { teamId });
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ActionButton } from '@/components/input/ActionButton';
|
|||
import { Trash } from '@/components/icons';
|
||||
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||
import { messages } from '@/components/messages';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { useDeleteQuery, useMessages } from '@/components/hooks';
|
||||
|
||||
export function LinkDeleteButton({
|
||||
linkId,
|
||||
|
|
@ -16,11 +16,7 @@ export function LinkDeleteButton({
|
|||
onSave?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: () => del(`/links/${linkId}`),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
const { mutate, isPending, error, touch } = useDeleteQuery(`/links/${linkId}`);
|
||||
|
||||
const handleConfirm = (close: () => void) => {
|
||||
mutate(null, {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import { ActionButton } from '@/components/input/ActionButton';
|
|||
import { Trash } from '@/components/icons';
|
||||
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||
import { messages } from '@/components/messages';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
|
||||
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
|
||||
export function PixelDeleteButton({
|
||||
pixelId,
|
||||
name,
|
||||
|
|
@ -15,10 +14,7 @@ export function PixelDeleteButton({
|
|||
onSave?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: () => del(`/pixels/${pixelId}`),
|
||||
});
|
||||
const { mutate, isPending, error } = useDeleteQuery(`/pixels/${pixelId}`);
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleConfirm = (close: () => void) => {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,14 @@ export function DateRangeSetting() {
|
|||
const { dateRange, saveDateRange } = useDateRange();
|
||||
const { value } = dateRange;
|
||||
|
||||
const handleChange = (value: string) => saveDateRange(value);
|
||||
const handleChange = (value: string) => {
|
||||
saveDateRange(value);
|
||||
};
|
||||
const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE_VALUE);
|
||||
|
||||
return (
|
||||
<Row gap="3">
|
||||
<DateFilter
|
||||
value={value}
|
||||
startDate={dateRange.startDate}
|
||||
endDate={dateRange.endDate}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<DateFilter value={value} onChange={handleChange} />
|
||||
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||
</Row>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,14 +6,11 @@ import {
|
|||
Button,
|
||||
FormSubmitButton,
|
||||
} from '@umami/react-zen';
|
||||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
|
||||
export function PasswordEditForm({ onSave, onClose }) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post('/me/password', data),
|
||||
});
|
||||
const { mutate, error, isPending } = useUpdateQuery('/me/password');
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
|
|
@ -10,10 +10,7 @@ import {
|
|||
|
||||
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post('/teams', data),
|
||||
});
|
||||
const { mutate, error, isPending } = useUpdateQuery('/teams');
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ import {
|
|||
Button,
|
||||
FormSubmitButton,
|
||||
} from '@umami/react-zen';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { useMessages, useModified, useUpdateQuery } from '@/components/hooks';
|
||||
|
||||
export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error } = useMutation({ mutationFn: (data: any) => post('/teams/join', data) });
|
||||
const { mutate, error } = useUpdateQuery('/teams/join');
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
|
||||
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||
|
||||
export function TeamLeaveForm({
|
||||
|
|
@ -15,10 +15,7 @@ export function TeamLeaveForm({
|
|||
onClose: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: () => del(`/teams/${teamId}/users/${userId}`),
|
||||
});
|
||||
const { mutate, error, isPending } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleConfirm = async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
|
||||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { useDeleteQuery, useMessages } from '@/components/hooks';
|
||||
|
||||
const CONFIRM_VALUE = 'DELETE';
|
||||
|
||||
|
|
@ -13,14 +13,12 @@ export function TeamDeleteForm({
|
|||
onClose?: () => void;
|
||||
}) {
|
||||
const { labels, formatMessage } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: () => del(`/teams/${teamId}`),
|
||||
});
|
||||
const { mutate, error, isPending, touch } = useDeleteQuery(`/teams/${teamId}`);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
mutate(null, {
|
||||
onSuccess: async () => {
|
||||
touch('teams');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ import {
|
|||
FormSubmitButton,
|
||||
TextField,
|
||||
Button,
|
||||
useToast,
|
||||
} from '@umami/react-zen';
|
||||
import { getRandomChars } from '@/lib/crypto';
|
||||
import { useApi, useMessages, useModified, useTeam } from '@/components/hooks';
|
||||
import { useMessages, useTeam, useUpdateQuery } from '@/components/hooks';
|
||||
|
||||
const generateId = () => `team_${getRandomChars(16)}`;
|
||||
|
||||
|
|
@ -23,20 +22,15 @@ export function TeamEditForm({
|
|||
}) {
|
||||
const team = useTeam();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { toast } = useToast();
|
||||
const { touch } = useModified();
|
||||
|
||||
const { mutate, error } = useMutation({
|
||||
mutationFn: (data: any) => post(`/teams/${teamId}`, data),
|
||||
});
|
||||
const { mutate, error, isPending, touch, toast } = useUpdateQuery(`/teams/${teamId}`);
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('teams');
|
||||
touch(`teams:${teamId}`);
|
||||
toast(formatMessage(messages.saved));
|
||||
onSave?.();
|
||||
},
|
||||
});
|
||||
|
|
@ -65,7 +59,9 @@ export function TeamEditForm({
|
|||
<Button onPress={() => setValue('accessCode', generateId(), { shouldDirty: true })}>
|
||||
{formatMessage(labels.regenerate)}
|
||||
</Button>
|
||||
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
|
||||
<FormSubmitButton variant="primary" isPending={isPending}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -23,10 +23,7 @@ export function TeamMemberEditForm({
|
|||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post(`/teams/${teamId}/users/${userId}`, data),
|
||||
});
|
||||
const { mutate, error, isPending } = useUpdateQuery(`/teams/${teamId}/users/${userId}`);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
|
||||
import { messages } from '@/components/messages';
|
||||
import { Trash } from '@/components/icons';
|
||||
import { Dialog } from '@umami/react-zen';
|
||||
|
|
@ -18,10 +18,7 @@ export function TeamMemberRemoveButton({
|
|||
onSave?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: () => del(`/teams/${teamId}/users/${userId}`),
|
||||
});
|
||||
const { mutate, isPending, error } = useDeleteQuery(`/teams/${teamId}/users/${userId}`);
|
||||
const { touch } = useModified();
|
||||
|
||||
const handleConfirm = (close: () => void) => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { useDeleteQuery, useMessages } from '@/components/hooks';
|
||||
import { Icon, LoadingButton, Text } from '@umami/react-zen';
|
||||
import { Close } from '@/components/icons';
|
||||
|
||||
export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: () => del(`/teams/${teamId}/websites/${websiteId}`),
|
||||
});
|
||||
const { mutate, isPending } = useDeleteQuery(`/teams/${teamId}/websites/${websiteId}`);
|
||||
|
||||
const handleRemoveTeamMember = async () => {
|
||||
mutate(null, {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Form, FormField, FormSubmitButton, Row, TextField, Button } from '@umami/react-zen';
|
||||
import { useApi } from '@/components/hooks';
|
||||
import { useUpdateQuery } from '@/components/hooks';
|
||||
import { DOMAIN_REGEX } from '@/lib/constants';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
|
|
@ -13,10 +13,7 @@ export function WebsiteAddForm({
|
|||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post('/websites', { ...data, teamId }),
|
||||
});
|
||||
const { mutate, error, isPending } = useUpdateQuery('/websites', { teamId });
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
Row,
|
||||
Loading,
|
||||
} from '@umami/react-zen';
|
||||
import { useApi, useMessages, useModified, useReportQuery } from '@/components/hooks';
|
||||
import { useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks';
|
||||
import { File, Lightning, Close, Plus } from '@/components/icons';
|
||||
|
||||
const FUNNEL_STEPS_MAX = 8;
|
||||
|
|
@ -32,12 +32,8 @@ export function FunnelEditForm({
|
|||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { touch } = useModified();
|
||||
const { post, useMutation } = useApi();
|
||||
const { data } = useReportQuery(id);
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (params: any) => post(`/reports${id ? `/${id}` : ''}`, params),
|
||||
});
|
||||
const { mutate, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`);
|
||||
|
||||
const handleSubmit = async ({ name, ...parameters }) => {
|
||||
mutate(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
Icon,
|
||||
Loading,
|
||||
} from '@umami/react-zen';
|
||||
import { useApi, useMessages, useModified, useReportQuery } from '@/components/hooks';
|
||||
import { useMessages, useReportQuery, useUpdateQuery } from '@/components/hooks';
|
||||
import { File, Lightning } from '@/components/icons';
|
||||
|
||||
export function GoalEditForm({
|
||||
|
|
@ -27,12 +27,9 @@ export function GoalEditForm({
|
|||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { touch } = useModified();
|
||||
const { post, useMutation } = useApi();
|
||||
|
||||
const { data } = useReportQuery(id);
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (params: any) => post(`/reports${id ? `/${id}` : ''}`, params),
|
||||
});
|
||||
const { mutate, error, isPending, touch } = useUpdateQuery(`/reports${id ? `/${id}` : ''}`);
|
||||
|
||||
const handleSubmit = async ({ name, ...parameters }) => {
|
||||
mutate(
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
|
|||
const { pathname, renderUrl, teamId } = useNavigation();
|
||||
|
||||
const renderPath = (path: string) =>
|
||||
renderUrl(`/websites/${websiteId}${path}`, { event: undefined });
|
||||
renderUrl(`/websites/${websiteId}${path}`, {
|
||||
event: undefined,
|
||||
compare: undefined,
|
||||
view: undefined,
|
||||
});
|
||||
|
||||
const items = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function CohortAddButton({ websiteId }: { websiteId: string }) {
|
|||
<Text>{formatMessage(labels.cohort)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.cohort)} style={{ width: 800, maxHeight: '90vh' }}>
|
||||
<Dialog title={formatMessage(labels.cohort)} style={{ width: 800, minHeight: 300 }}>
|
||||
{({ close }) => {
|
||||
return <CohortEditForm websiteId={websiteId} onClose={close} />;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ActionButton } from '@/components/input/ActionButton';
|
|||
import { Trash } from '@/components/icons';
|
||||
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||
import { messages } from '@/components/messages';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { useDeleteQuery, useMessages } from '@/components/hooks';
|
||||
|
||||
export function CohortDeleteButton({
|
||||
cohortId,
|
||||
|
|
@ -17,11 +17,9 @@ export function CohortDeleteButton({
|
|||
onSave?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: () => del(`/websites/${websiteId}/segments/${cohortId}`),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
const { mutate, isPending, error, touch } = useDeleteQuery(
|
||||
`/websites/${websiteId}/segments/${cohortId}`,
|
||||
);
|
||||
|
||||
const handleConfirm = (close: () => void) => {
|
||||
mutate(null, {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function CohortEditButton({
|
|||
|
||||
return (
|
||||
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
|
||||
<Dialog title={formatMessage(labels.cohort)} style={{ width: 800, maxHeight: '90vh' }}>
|
||||
<Dialog title={formatMessage(labels.cohort)} style={{ width: 800, minHeight: 300 }}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<CohortEditForm
|
||||
|
|
|
|||
|
|
@ -7,18 +7,29 @@ import {
|
|||
TextField,
|
||||
Label,
|
||||
Loading,
|
||||
Column,
|
||||
ComboBox,
|
||||
Select,
|
||||
ListItem,
|
||||
Grid,
|
||||
useDebounce,
|
||||
} from '@umami/react-zen';
|
||||
import { subMonths, endOfDay } from 'date-fns';
|
||||
import {
|
||||
useMessages,
|
||||
useUpdateQuery,
|
||||
useWebsiteCohortQuery,
|
||||
useWebsiteValuesQuery,
|
||||
} from '@/components/hooks';
|
||||
import { DateFilter } from '@/components/input/DateFilter';
|
||||
import { FieldFilters } from '@/components/input/FieldFilters';
|
||||
import { useState } from 'react';
|
||||
import { useApi, useMessages, useModified, useWebsiteCohortQuery } from '@/components/hooks';
|
||||
import { filtersArrayToObject } from '@/lib/params';
|
||||
import { SetStateAction, useMemo, useState } from 'react';
|
||||
import { endOfDay, subMonths } from 'date-fns';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
|
||||
export function CohortEditForm({
|
||||
cohortId,
|
||||
websiteId,
|
||||
filters = [],
|
||||
showFilters = true,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
|
|
@ -29,33 +40,46 @@ export function CohortEditForm({
|
|||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const [action, setAction] = useState('path');
|
||||
const [search, setSearch] = useState('');
|
||||
const searchValue = useDebounce(search, 300);
|
||||
const { data } = useWebsiteCohortQuery(websiteId, cohortId);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [currentFilters, setCurrentFilters] = useState(filters);
|
||||
const { touch } = useModified();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const startDate = subMonths(endOfDay(new Date()), 6);
|
||||
const endDate = endOfDay(new Date());
|
||||
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) =>
|
||||
post(`/websites/${websiteId}/cohorts${cohortId ? `/${cohortId}` : ''}`, {
|
||||
...data,
|
||||
type: 'cohort',
|
||||
}),
|
||||
const { data: searchResults, isLoading } = useWebsiteValuesQuery({
|
||||
websiteId,
|
||||
type: action,
|
||||
search: searchValue,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(
|
||||
{ ...data, parameters: filtersArrayToObject(currentFilters) },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
touch('cohorts');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
const { mutate, error, isPending, touch, toast } = useUpdateQuery(
|
||||
`/websites/${websiteId}/segments${cohortId ? `/${cohortId}` : ''}`,
|
||||
{
|
||||
type: 'cohort',
|
||||
},
|
||||
);
|
||||
|
||||
const items: string[] = useMemo(() => {
|
||||
return searchResults?.map(({ value }) => value) || [];
|
||||
}, [searchResults]);
|
||||
|
||||
const handleSubmit = async (formData: any) => {
|
||||
mutate(formData, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('cohorts');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = (value: SetStateAction<string>) => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
if (cohortId && !data) {
|
||||
|
|
@ -63,7 +87,11 @@ export function CohortEditForm({
|
|||
}
|
||||
|
||||
return (
|
||||
<Form error={error} onSubmit={handleSubmit} defaultValues={data}>
|
||||
<Form
|
||||
error={error}
|
||||
onSubmit={handleSubmit}
|
||||
defaultValues={data || { parameters: { filters, dateRange: '30day' } }}
|
||||
>
|
||||
<FormField
|
||||
name="name"
|
||||
label={formatMessage(labels.name)}
|
||||
|
|
@ -71,27 +99,74 @@ export function CohortEditForm({
|
|||
>
|
||||
<TextField autoFocus />
|
||||
</FormField>
|
||||
{showFilters && (
|
||||
<>
|
||||
<Label>{formatMessage(labels.filters)}</Label>
|
||||
<FieldFilters
|
||||
websiteId={websiteId}
|
||||
filters={currentFilters}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onSave={setCurrentFilters}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.action)}</Label>
|
||||
<Grid columns="260px 1fr" gap>
|
||||
<Column>
|
||||
<Select value={action} onChange={setAction}>
|
||||
<ListItem id="path">{formatMessage(labels.viewedPage)}</ListItem>
|
||||
<ListItem id="event">{formatMessage(labels.triggeredEvent)}</ListItem>
|
||||
</Select>
|
||||
</Column>
|
||||
<Column>
|
||||
<FormField
|
||||
name="parameters.action"
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
{({ field }) => {
|
||||
return (
|
||||
<ComboBox
|
||||
aria-label="action"
|
||||
items={items}
|
||||
inputValue={field?.value}
|
||||
onInputChange={value => {
|
||||
handleSearch(value);
|
||||
field?.onChange?.(value);
|
||||
}}
|
||||
formValue="text"
|
||||
allowsEmptyCollection
|
||||
allowsCustomValue
|
||||
renderEmptyState={() =>
|
||||
isLoading ? (
|
||||
<Loading position="center" icon="dots" />
|
||||
) : (
|
||||
<Empty message={formatMessage(messages.noResultsFound)} />
|
||||
)
|
||||
}
|
||||
>
|
||||
{items.map(item => (
|
||||
<ListItem key={item} id={item}>
|
||||
{item}
|
||||
</ListItem>
|
||||
))}
|
||||
</ComboBox>
|
||||
);
|
||||
}}
|
||||
</FormField>
|
||||
</Column>
|
||||
</Grid>
|
||||
</Column>
|
||||
|
||||
<Column width="260px">
|
||||
<Label>{formatMessage(labels.dateRange)}</Label>
|
||||
<FormField name="parameters.dateRange" rules={{ required: formatMessage(labels.required) }}>
|
||||
<DateFilter placement="bottom start" />
|
||||
</FormField>
|
||||
</Column>
|
||||
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.filters)}</Label>
|
||||
<FormField name="parameters.filters" rules={{ required: formatMessage(labels.required) }}>
|
||||
<FieldFilters websiteId={websiteId} exclude={['path', 'event']} />
|
||||
</FormField>
|
||||
</Column>
|
||||
|
||||
<FormButtons>
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
<FormSubmitButton
|
||||
variant="primary"
|
||||
data-test="button-submit"
|
||||
isDisabled={isPending || currentFilters.length === 0}
|
||||
>
|
||||
<FormSubmitButton variant="primary" data-test="button-submit" isDisabled={isPending}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
|
|
|
|||
|
|
@ -17,80 +17,98 @@ export function CompareTables({ websiteId }: { websiteId: string }) {
|
|||
updateParams,
|
||||
query: { view = 'path' },
|
||||
} = useNavigation();
|
||||
const { startDate, endDate } = getCompareDate(
|
||||
dateCompare,
|
||||
dateRange.startDate,
|
||||
dateRange.endDate,
|
||||
);
|
||||
|
||||
const params = {
|
||||
startAt: startDate.getTime(),
|
||||
endAt: endDate.getTime(),
|
||||
};
|
||||
|
||||
const renderPath = (view: string) => {
|
||||
return updateParams({ view });
|
||||
};
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 'path',
|
||||
label: formatMessage(labels.path),
|
||||
path: updateParams({ view: 'path' }),
|
||||
path: renderPath('path'),
|
||||
},
|
||||
{
|
||||
id: 'channel',
|
||||
label: formatMessage(labels.channels),
|
||||
path: renderPath('channel'),
|
||||
},
|
||||
{
|
||||
id: 'referrer',
|
||||
label: formatMessage(labels.referrers),
|
||||
path: updateParams({ view: 'referrer' }),
|
||||
path: renderPath('referrer'),
|
||||
},
|
||||
{
|
||||
id: 'browser',
|
||||
label: formatMessage(labels.browsers),
|
||||
path: updateParams({ view: 'browser' }),
|
||||
path: renderPath('browser'),
|
||||
},
|
||||
{
|
||||
id: 'os',
|
||||
label: formatMessage(labels.os),
|
||||
path: updateParams({ view: 'os' }),
|
||||
path: renderPath('os'),
|
||||
},
|
||||
{
|
||||
id: 'device',
|
||||
label: formatMessage(labels.devices),
|
||||
path: updateParams({ view: 'device' }),
|
||||
path: renderPath('device'),
|
||||
},
|
||||
{
|
||||
id: 'country',
|
||||
label: formatMessage(labels.countries),
|
||||
path: updateParams({ view: 'country' }),
|
||||
path: renderPath('country'),
|
||||
},
|
||||
{
|
||||
id: 'region',
|
||||
label: formatMessage(labels.regions),
|
||||
path: updateParams({ view: 'region' }),
|
||||
path: renderPath('region'),
|
||||
},
|
||||
{
|
||||
id: 'city',
|
||||
label: formatMessage(labels.cities),
|
||||
path: updateParams({ view: 'city' }),
|
||||
path: renderPath('city'),
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
label: formatMessage(labels.languages),
|
||||
path: updateParams({ view: 'language' }),
|
||||
path: renderPath('language'),
|
||||
},
|
||||
{
|
||||
id: 'screen',
|
||||
label: formatMessage(labels.screens),
|
||||
path: updateParams({ view: 'screen' }),
|
||||
path: renderPath('screen'),
|
||||
},
|
||||
{
|
||||
id: 'event',
|
||||
label: formatMessage(labels.events),
|
||||
path: updateParams({ view: 'event' }),
|
||||
path: renderPath('event'),
|
||||
},
|
||||
{
|
||||
id: 'hostname',
|
||||
label: formatMessage(labels.hostname),
|
||||
path: updateParams({ view: 'hostname' }),
|
||||
path: renderPath('hostname'),
|
||||
},
|
||||
{
|
||||
id: 'tag',
|
||||
label: formatMessage(labels.tags),
|
||||
path: updateParams({ view: 'tag' }),
|
||||
path: renderPath('tag'),
|
||||
},
|
||||
];
|
||||
|
||||
const renderChange = props => {
|
||||
const { label: x, count: y } = props;
|
||||
const prev = data.find(d => d.x === x)?.y;
|
||||
const value = y - prev;
|
||||
const change = Math.abs(((y - prev) / prev) * 100);
|
||||
const renderChange = ({ label, count }) => {
|
||||
const prev = data.find(d => d.x === label)?.y;
|
||||
const value = count - prev;
|
||||
const change = Math.abs(((count - prev) / prev) * 100);
|
||||
|
||||
return (
|
||||
!isNaN(change) && (
|
||||
|
|
@ -101,19 +119,8 @@ export function CompareTables({ websiteId }: { websiteId: string }) {
|
|||
);
|
||||
};
|
||||
|
||||
const handleChange = id => {
|
||||
router.push(updateParams({ view: id }));
|
||||
};
|
||||
|
||||
const { startDate, endDate } = getCompareDate(
|
||||
dateCompare,
|
||||
dateRange.startDate,
|
||||
dateRange.endDate,
|
||||
);
|
||||
|
||||
const params = {
|
||||
startAt: startDate.getTime(),
|
||||
endAt: endDate.getTime(),
|
||||
const handleChange = (id: any) => {
|
||||
router.push(renderPath(id));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -133,8 +140,8 @@ export function CompareTables({ websiteId }: { websiteId: string }) {
|
|||
))}
|
||||
</Select>
|
||||
</Row>
|
||||
<Panel>
|
||||
<Grid columns={{ xs: '1fr', lg: '1fr 1fr' }} gap="6">
|
||||
<Panel minHeight="300px">
|
||||
<Grid columns={{ xs: '1fr', lg: '1fr 1fr' }} gap="6" height="100%">
|
||||
<Column gap="6">
|
||||
<Row alignItems="center" justifyContent="space-between">
|
||||
<Heading size="2">{formatMessage(labels.previous)}</Heading>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function SegmentAddButton({ websiteId }: { websiteId: string }) {
|
|||
<Text>{formatMessage(labels.segment)}</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.segment)} style={{ width: 800, maxHeight: '90vh' }}>
|
||||
<Dialog title={formatMessage(labels.segment)} style={{ width: 800, minHeight: 300 }}>
|
||||
{({ close }) => {
|
||||
return <SegmentEditForm websiteId={websiteId} onClose={close} />;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ActionButton } from '@/components/input/ActionButton';
|
|||
import { Trash } from '@/components/icons';
|
||||
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||
import { messages } from '@/components/messages';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { useDeleteQuery, useMessages } from '@/components/hooks';
|
||||
|
||||
export function SegmentDeleteButton({
|
||||
segmentId,
|
||||
|
|
@ -17,11 +17,9 @@ export function SegmentDeleteButton({
|
|||
onSave?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: () => del(`/websites/${websiteId}/segments/${segmentId}`),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
const { mutate, isPending, error, touch } = useDeleteQuery(
|
||||
`/websites/${websiteId}/segments/${segmentId}`,
|
||||
);
|
||||
|
||||
const handleConfirm = (close: () => void) => {
|
||||
mutate(null, {
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ export function SegmentEditButton({
|
|||
}: {
|
||||
segmentId: string;
|
||||
websiteId: string;
|
||||
filters: any[];
|
||||
filters?: any[];
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<ActionButton title={formatMessage(labels.edit)} icon={<Edit />}>
|
||||
<Dialog title={formatMessage(labels.segment)} style={{ width: 800, maxHeight: '90vh' }}>
|
||||
<Dialog title={formatMessage(labels.segment)} style={{ width: 800, minHeight: 300 }}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<SegmentEditForm
|
||||
|
|
|
|||
|
|
@ -5,14 +5,11 @@ import {
|
|||
FormField,
|
||||
FormSubmitButton,
|
||||
TextField,
|
||||
Label,
|
||||
Loading,
|
||||
Label,
|
||||
} from '@umami/react-zen';
|
||||
import { subMonths, endOfDay } from 'date-fns';
|
||||
import { FieldFilters } from '@/components/input/FieldFilters';
|
||||
import { useState } from 'react';
|
||||
import { useMessages, useUpdateQuery, useWebsiteSegmentQuery } from '@/components/hooks';
|
||||
import { filtersArrayToObject } from '@/lib/params';
|
||||
import { messages } from '@/components/messages';
|
||||
|
||||
export function SegmentEditForm({
|
||||
|
|
@ -32,30 +29,23 @@ export function SegmentEditForm({
|
|||
}) {
|
||||
const { data } = useWebsiteSegmentQuery(websiteId, segmentId);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [currentFilters, setCurrentFilters] = useState(filters);
|
||||
const startDate = subMonths(endOfDay(new Date()), 6);
|
||||
const endDate = endOfDay(new Date());
|
||||
|
||||
const { mutate, error, isPending, touch, toast } = useUpdateQuery(
|
||||
`/websites/${websiteId}/segments${segmentId ? `/${segmentId}` : ''}`,
|
||||
{
|
||||
...data,
|
||||
type: 'segment',
|
||||
},
|
||||
);
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(
|
||||
{ ...data, parameters: filtersArrayToObject(currentFilters) },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('segments');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
const handleSubmit = async (formData: any) => {
|
||||
mutate(formData, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('segments');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (segmentId && !data) {
|
||||
|
|
@ -63,35 +53,27 @@ export function SegmentEditForm({
|
|||
}
|
||||
|
||||
return (
|
||||
<Form error={error} onSubmit={handleSubmit} defaultValues={data}>
|
||||
<Form error={error} onSubmit={handleSubmit} defaultValues={data || { parameters: { filters } }}>
|
||||
<FormField
|
||||
name="name"
|
||||
label={formatMessage(labels.name)}
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<TextField autoFocus />
|
||||
<TextField autoFocus={!segmentId} />
|
||||
</FormField>
|
||||
{showFilters && (
|
||||
<>
|
||||
<Label>{formatMessage(labels.filters)}</Label>
|
||||
<FieldFilters
|
||||
websiteId={websiteId}
|
||||
filters={currentFilters}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onSave={setCurrentFilters}
|
||||
/>
|
||||
<FormField name="parameters.filters" rules={{ required: formatMessage(labels.required) }}>
|
||||
<FieldFilters websiteId={websiteId} />
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
<FormButtons>
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
<FormSubmitButton
|
||||
variant="primary"
|
||||
data-test="button-submit"
|
||||
isDisabled={isPending || currentFilters.length === 0}
|
||||
>
|
||||
<FormSubmitButton variant="primary" data-test="button-submit" isDisabled={isPending}>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</FormButtons>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { DataTable, DataColumn, Row } from '@umami/react-zen';
|
|||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { DateDistance } from '@/components/common/DateDistance';
|
||||
import { filtersObjectToArray } from '@/lib/params';
|
||||
import { SegmentEditButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentEditButton';
|
||||
import { SegmentDeleteButton } from '@/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -27,15 +26,11 @@ export function SegmentsTable({ data = [] }) {
|
|||
</DataColumn>
|
||||
<DataColumn id="action" align="end" width="100px">
|
||||
{(row: any) => {
|
||||
const { id, name, parameters } = row;
|
||||
const { id, name } = row;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<SegmentEditButton
|
||||
segmentId={id}
|
||||
websiteId={websiteId}
|
||||
filters={filtersObjectToArray(parameters)}
|
||||
/>
|
||||
<SegmentEditButton segmentId={id} websiteId={websiteId} />
|
||||
<SegmentDeleteButton segmentId={id} websiteId={websiteId} name={name} />
|
||||
</Row>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { useDeleteQuery, useMessages } from '@/components/hooks';
|
||||
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
|
||||
|
||||
const CONFIRM_VALUE = 'DELETE';
|
||||
|
|
@ -13,11 +13,7 @@ export function WebsiteDeleteForm({
|
|||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: () => del(`/websites/${websiteId}`),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
const { mutate, isPending, error, touch } = useDeleteQuery(`/websites/${websiteId}`);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
mutate(null, {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,11 @@
|
|||
import {
|
||||
FormSubmitButton,
|
||||
Form,
|
||||
FormField,
|
||||
FormButtons,
|
||||
TextField,
|
||||
useToast,
|
||||
} from '@umami/react-zen';
|
||||
import { useApi, useMessages, useModified, useWebsite } from '@/components/hooks';
|
||||
import { FormSubmitButton, Form, FormField, FormButtons, TextField } from '@umami/react-zen';
|
||||
import { useMessages, useUpdateQuery, useWebsite } from '@/components/hooks';
|
||||
import { DOMAIN_REGEX } from '@/lib/constants';
|
||||
|
||||
export function WebsiteEditForm({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
|
||||
const website = useWebsite();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { toast } = useToast();
|
||||
const { touch } = useModified();
|
||||
|
||||
const { mutate, error } = useMutation({
|
||||
mutationFn: (data: any) => post(`/websites/${websiteId}`, data),
|
||||
});
|
||||
const { mutate, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import { TypeConfirmationForm } from '@/components/common/TypeConfirmationForm';
|
||||
|
||||
const CONFIRM_VALUE = 'RESET';
|
||||
|
|
@ -13,10 +13,7 @@ export function WebsiteResetForm({
|
|||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: (data: any) => post(`/websites/${websiteId}/reset`, data),
|
||||
});
|
||||
const { mutate, isPending, error } = useUpdateQuery(`/websites/${websiteId}/reset`);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
mutate(null, {
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ import {
|
|||
FormSubmitButton,
|
||||
Column,
|
||||
Label,
|
||||
useToast,
|
||||
Row,
|
||||
} from '@umami/react-zen';
|
||||
import { useState } from 'react';
|
||||
import { getRandomChars } from '@/lib/crypto';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
|
||||
const generateId = () => getRandomChars(16);
|
||||
|
||||
|
|
@ -26,12 +25,7 @@ export interface WebsiteShareFormProps {
|
|||
export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const [id, setId] = useState(shareId);
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post(`/websites/${websiteId}`, data),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
const { toast } = useToast();
|
||||
const { mutate, error, isPending, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
|
||||
|
||||
const url = `${window?.location.origin || ''}${process.env.basePath || ''}/share/${id}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
|
|||
import { badRequest, json, unauthorized } from '@/lib/response';
|
||||
import { getWebsiteSegments, getValues } from '@/queries';
|
||||
import { z } from 'zod';
|
||||
import { dateRangeParams, searchParams } from '@/lib/schema';
|
||||
import { dateRangeParams, fieldsParam, searchParams } from '@/lib/schema';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
type: z.string(),
|
||||
type: fieldsParam,
|
||||
...dateRangeParams,
|
||||
...searchParams,
|
||||
});
|
||||
|
|
@ -31,7 +31,7 @@ export async function GET(
|
|||
const { type } = query;
|
||||
|
||||
if (!SESSION_COLUMNS.includes(type) && !EVENT_COLUMNS.includes(type) && !FILTER_GROUPS[type]) {
|
||||
return badRequest('Invalid type.');
|
||||
return badRequest();
|
||||
}
|
||||
|
||||
let values;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
Heading,
|
||||
} from '@umami/react-zen';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useApi, useMessages } from '@/components/hooks';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import { setUser } from '@/store/app';
|
||||
import { setClientAuthToken } from '@/lib/client';
|
||||
import { Logo } from '@/components/icons';
|
||||
|
|
@ -18,10 +18,7 @@ import { Logo } from '@/components/icons';
|
|||
export function LoginForm() {
|
||||
const { formatMessage, labels, getMessage } = useMessages();
|
||||
const router = useRouter();
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, error, isPending } = useMutation({
|
||||
mutationFn: (data: any) => post('/auth/login', data),
|
||||
});
|
||||
const { mutate, error, isPending } = useUpdateQuery('/auth/login');
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
mutate(data, {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Grid, Column, TextField, Label, Select, Icon, Button, ListItem } from '
|
|||
import { useFilters, useFormat, useWebsiteValuesQuery } from '@/components/hooks';
|
||||
import { Close } from '@/components/icons';
|
||||
import { isSearchOperator } from '@/lib/params';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
|
||||
export interface FilterRecordProps {
|
||||
websiteId: string;
|
||||
|
|
@ -41,20 +42,25 @@ export function FilterRecord({
|
|||
endDate,
|
||||
});
|
||||
const isSearch = isSearchOperator(operator);
|
||||
const items = data?.filter(({ value }) => value) || [];
|
||||
|
||||
const handleSearch = value => {
|
||||
const handleSearch = (value: string) => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleSelectOperator = value => {
|
||||
const handleSelectOperator = (value: any) => {
|
||||
onSelect?.(name, value);
|
||||
};
|
||||
|
||||
const handleSelectValue = value => {
|
||||
const handleSelectValue = (value: string) => {
|
||||
setSelected(value);
|
||||
onChange?.(name, value);
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
return formatValue(selected, type);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Label>{fields.find(f => f.name === name)?.label}</Label>
|
||||
|
|
@ -78,15 +84,17 @@ export function FilterRecord({
|
|||
)}
|
||||
{!isSearch && (
|
||||
<Select
|
||||
items={data}
|
||||
items={items}
|
||||
value={selected}
|
||||
onChange={handleSelectValue}
|
||||
searchValue={search}
|
||||
renderValue={renderValue}
|
||||
onSearch={handleSearch}
|
||||
isLoading={isLoading}
|
||||
listProps={{ renderEmptyState: () => <Empty /> }}
|
||||
allowSearch
|
||||
>
|
||||
{data?.map(({ value }) => {
|
||||
{items?.map(({ value }) => {
|
||||
return (
|
||||
<ListItem key={value} id={value}>
|
||||
{formatValue(value, type)}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { useApi, useModified } from '@/components/hooks';
|
|||
|
||||
export function useDeleteQuery(path: string, params?: Record<string, any>) {
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
const query = useMutation({
|
||||
mutationFn: () => del(path, params),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
|
||||
return { mutate, isPending, error, touch };
|
||||
return { ...query, touch };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { useToast } from '@umami/react-zen';
|
|||
|
||||
export function useUpdateQuery(path: string, params?: Record<string, any>) {
|
||||
const { post, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
const query = useMutation({
|
||||
mutationFn: (data: Record<string, any>) => post(path, { ...data, ...params }),
|
||||
});
|
||||
const { touch } = useModified();
|
||||
const { toast } = useToast();
|
||||
|
||||
return { mutate, isPending, error, touch, toast };
|
||||
return { ...query, touch, toast };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function useWebsiteCohortQuery(
|
|||
|
||||
return useQuery({
|
||||
queryKey: ['website:cohorts', { websiteId, cohortId, modified }],
|
||||
queryFn: () => get(`/websites/${websiteId}/cohorts/${cohortId}`),
|
||||
queryFn: () => get(`/websites/${websiteId}/segments/${cohortId}`),
|
||||
enabled: !!(websiteId && cohortId),
|
||||
placeholderData: keepPreviousData,
|
||||
...options,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useMemo } from 'react';
|
||||
import { getMinimumUnit, parseDateRange, getOffsetDateRange } from '@/lib/date';
|
||||
import { setItem } from '@/lib/storage';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
|
||||
|
|
@ -6,10 +7,10 @@ import { setDateRangeValue, useApp } from '@/store/app';
|
|||
import { useLocale } from './useLocale';
|
||||
import { useApi } from './useApi';
|
||||
import { useNavigation } from './useNavigation';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export interface UseDateRangeOptions {
|
||||
ignoreOffset?: boolean;
|
||||
useQueryParameter?: boolean;
|
||||
}
|
||||
|
||||
export function useDateRange(websiteId?: string, options: UseDateRangeOptions = {}) {
|
||||
|
|
@ -20,7 +21,11 @@ export function useDateRange(websiteId?: string, options: UseDateRangeOptions =
|
|||
} = useNavigation();
|
||||
const websiteConfig = useWebsites(state => state[websiteId]?.dateRange);
|
||||
const globalConfig = useApp(state => state.dateRangeValue);
|
||||
const dateValue = date || websiteConfig?.value || globalConfig || DEFAULT_DATE_RANGE_VALUE;
|
||||
const dateValue =
|
||||
(options.useQueryParameter ? date : false) ||
|
||||
websiteConfig?.value ||
|
||||
globalConfig ||
|
||||
DEFAULT_DATE_RANGE_VALUE;
|
||||
|
||||
const dateRange = useMemo(() => {
|
||||
const dateRangeObject = parseDateRange(dateValue, locale);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useIntl } from 'react-intl';
|
||||
import { messages, labels } from '@/components/messages';
|
||||
|
||||
export function useMessages(): any {
|
||||
export function useMessages() {
|
||||
const intl = useIntl();
|
||||
|
||||
const getMessage = (id: string) => {
|
||||
|
|
|
|||
|
|
@ -4,31 +4,31 @@ import { endOfYear } from 'date-fns';
|
|||
import { DatePickerForm } from '@/components/metrics/DatePickerForm';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { DateDisplay } from '@/components/common/DateDisplay';
|
||||
import { parseDateRange } from '@/lib/date';
|
||||
|
||||
export interface DateFilterProps {
|
||||
value: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
showAllTime?: boolean;
|
||||
renderDate?: boolean;
|
||||
placement?: string;
|
||||
}
|
||||
|
||||
export function DateFilter({
|
||||
value,
|
||||
startDate,
|
||||
endDate,
|
||||
onChange,
|
||||
showAllTime,
|
||||
renderDate,
|
||||
placement = 'bottom',
|
||||
}: DateFilterProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const { startDate, endDate } = parseDateRange(value) || {};
|
||||
|
||||
const options = [
|
||||
{ label: formatMessage(labels.today), value: '0day' },
|
||||
{
|
||||
label: formatMessage(labels.lastHours, { x: 24 }),
|
||||
label: formatMessage(labels.lastHours, { x: '24' }),
|
||||
value: '24hour',
|
||||
},
|
||||
{
|
||||
|
|
@ -37,7 +37,7 @@ export function DateFilter({
|
|||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastDays, { x: 7 }),
|
||||
label: formatMessage(labels.lastDays, { x: '7' }),
|
||||
value: '7day',
|
||||
},
|
||||
{
|
||||
|
|
@ -46,21 +46,21 @@ export function DateFilter({
|
|||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastDays, { x: 30 }),
|
||||
label: formatMessage(labels.lastDays, { x: '30' }),
|
||||
value: '30day',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastDays, { x: 90 }),
|
||||
label: formatMessage(labels.lastDays, { x: '90' }),
|
||||
value: '90day',
|
||||
},
|
||||
{ label: formatMessage(labels.thisYear), value: '0year' },
|
||||
{
|
||||
label: formatMessage(labels.lastMonths, { x: 6 }),
|
||||
label: formatMessage(labels.lastMonths, { x: '6' }),
|
||||
value: '6month',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastMonths, { x: 12 }),
|
||||
label: formatMessage(labels.lastMonths, { x: '12' }),
|
||||
value: '12month',
|
||||
},
|
||||
showAllTime && {
|
||||
|
|
@ -105,7 +105,7 @@ export function DateFilter({
|
|||
placeholder={formatMessage(labels.selectDate)}
|
||||
onChange={handleChange}
|
||||
renderValue={renderValue}
|
||||
popoverProps={{ placement: 'bottom' }}
|
||||
popoverProps={{ placement: placement as any }}
|
||||
>
|
||||
{options.map(({ label, value, divider }: any) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Key } from 'react';
|
||||
import { subMonths, endOfDay } from 'date-fns';
|
||||
import { Grid, Column, List, ListItem } from '@umami/react-zen';
|
||||
import { useFields, useMessages } from '@/components/hooks';
|
||||
import { FilterRecord } from '@/components/common/FilterRecord';
|
||||
|
|
@ -6,28 +7,23 @@ import { Empty } from '@/components/common/Empty';
|
|||
|
||||
export interface FieldFiltersProps {
|
||||
websiteId: string;
|
||||
filters: { name: string; operator: string; value: string }[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
onSave?: (data: any) => void;
|
||||
value?: { name: string; operator: string; value: string }[];
|
||||
exclude?: string[];
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
export function FieldFilters({
|
||||
websiteId,
|
||||
filters,
|
||||
startDate,
|
||||
endDate,
|
||||
onSave,
|
||||
}: FieldFiltersProps) {
|
||||
export function FieldFilters({ websiteId, value, exclude = [], onChange }: FieldFiltersProps) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const { fields } = useFields();
|
||||
const startDate = subMonths(endOfDay(new Date()), 6);
|
||||
const endDate = endOfDay(new Date());
|
||||
|
||||
const updateFilter = (name: string, props: Record<string, any>) => {
|
||||
onSave(filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)));
|
||||
onChange(value.map(filter => (filter.name === name ? { ...filter, ...props } : filter)));
|
||||
};
|
||||
|
||||
const handleAdd = (name: Key) => {
|
||||
onSave(filters.concat({ name: name.toString(), operator: 'eq', value: '' }));
|
||||
onChange(value.concat({ name: name.toString(), operator: 'eq', value: '' }));
|
||||
};
|
||||
|
||||
const handleChange = (name: string, value: Key) => {
|
||||
|
|
@ -39,25 +35,27 @@ export function FieldFilters({
|
|||
};
|
||||
|
||||
const handleRemove = (name: string) => {
|
||||
onSave(filters.filter(filter => filter.name !== name));
|
||||
onChange(value.filter(filter => filter.name !== name));
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns="160px 1fr" overflow="hidden" gapY="6">
|
||||
<Column border="right" paddingRight="3">
|
||||
<List onAction={handleAdd}>
|
||||
{fields.map((field: any) => {
|
||||
const isDisabled = !!filters.find(({ name }) => name === field.name);
|
||||
return (
|
||||
<ListItem key={field.name} id={field.name} isDisabled={isDisabled}>
|
||||
{field.label}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
{fields
|
||||
.filter(({ name }) => !exclude.includes(name))
|
||||
.map(field => {
|
||||
const isDisabled = !!value.find(({ name }) => name === field.name);
|
||||
return (
|
||||
<ListItem key={field.name} id={field.name} isDisabled={isDisabled}>
|
||||
{field.label}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Column>
|
||||
<Column paddingLeft="6" overflow="auto" gapY="4" height="500px" style={{ contain: 'layout' }}>
|
||||
{filters.map(filter => {
|
||||
{value.map(filter => {
|
||||
return (
|
||||
<FilterRecord
|
||||
key={filter.name}
|
||||
|
|
@ -72,7 +70,7 @@ export function FieldFilters({
|
|||
/>
|
||||
);
|
||||
})}
|
||||
{!filters.length && <Empty message={formatMessage(messages.nothingSelected)} />}
|
||||
{!value.length && <Empty message={formatMessage(messages.nothingSelected)} />}
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
|||
label={label}
|
||||
operator={operatorLabels[operator]}
|
||||
value={paramValue}
|
||||
onRemove={name => handleCloseFilter(name)}
|
||||
onRemove={(name: string) => handleCloseFilter(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
@ -143,7 +143,7 @@ const FilterItem = ({ name, label, operator, value, onRemove }) => {
|
|||
{value}
|
||||
</Text>
|
||||
</Row>
|
||||
<Icon onClick={() => onRemove(name)} size="xs">
|
||||
<Icon onClick={() => onRemove(name)} size="xs" style={{ cursor: 'pointer' }}>
|
||||
<Close />
|
||||
</Icon>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function FilterEditForm({
|
|||
<TabPanel id="fields">
|
||||
<FieldFilters
|
||||
websiteId={websiteId}
|
||||
filters={currentFilters}
|
||||
value={currentFilters}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onSave={setCurrentFilters}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ export function TeamsButton({ showText = true }: { showText?: boolean }) {
|
|||
return (
|
||||
<MenuTrigger>
|
||||
<Pressable>
|
||||
<Row width="100%" backgroundColor="2" border borderRadius>
|
||||
<SidebarItem label={label} icon={teamId ? <Users /> : <User />}>
|
||||
<Row role="button" width="100%" backgroundColor="2" border borderRadius>
|
||||
<SidebarItem role="button" label={label} icon={teamId ? <Users /> : <User />}>
|
||||
{showText && (
|
||||
<Icon rotate={90} size="sm">
|
||||
<Chevron />
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function WebsiteDateFilter({
|
|||
allowCompare,
|
||||
}: WebsiteDateFilterProps) {
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { value, startDate, endDate } = dateRange;
|
||||
const { value, endDate } = dateRange;
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const {
|
||||
router,
|
||||
|
|
@ -61,8 +61,6 @@ export function WebsiteDateFilter({
|
|||
)}
|
||||
<DateFilter
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={handleChange}
|
||||
showAllTime={showAllTime}
|
||||
renderDate={+offset !== 0}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export const labels = defineMessages({
|
|||
data: { id: 'label.data', defaultMessage: 'Data' },
|
||||
trackingCode: { id: 'label.tracking-code', defaultMessage: 'Tracking code' },
|
||||
shareUrl: { id: 'label.share-url', defaultMessage: 'Share URL' },
|
||||
action: { id: 'label.action', defaultMessage: 'Action' },
|
||||
actions: { id: 'label.actions', defaultMessage: 'Actions' },
|
||||
domain: { id: 'label.domain', defaultMessage: 'Domain' },
|
||||
websiteId: { id: 'label.website-id', defaultMessage: 'Website ID' },
|
||||
|
|
@ -357,6 +358,7 @@ export const labels = defineMessages({
|
|||
audience: { id: 'label.audience', defaultMessage: 'Audience' },
|
||||
invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
|
||||
environment: { id: 'label.environment', defaultMessage: 'Environment' },
|
||||
criteria: { id: 'label.criteria', defaultMessage: 'Criteria' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -68,13 +68,7 @@ export function MetricsTable({
|
|||
};
|
||||
|
||||
return (
|
||||
<LoadingPanel
|
||||
data={data}
|
||||
isFetching={isFetching}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
height="100%"
|
||||
>
|
||||
<LoadingPanel data={data} isFetching={isFetching} isLoading={isLoading} error={error}>
|
||||
{data && <ListTable {...props} data={filteredData} renderLabel={renderLabel} />}
|
||||
{showMore && limit && (
|
||||
<Row justifyContent="center">
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export function PageviewsChart({ data, unit, minDate, maxDate, ...props }: Pagev
|
|||
__id: new Date().getTime(),
|
||||
datasets: [
|
||||
{
|
||||
type: data.compare ? 'line' : 'bar',
|
||||
label: formatMessage(labels.visitors),
|
||||
data: generateTimeSeries(data.sessions, minDate, maxDate, unit, dateLocale),
|
||||
borderWidth: 1,
|
||||
|
|
@ -40,6 +41,7 @@ export function PageviewsChart({ data, unit, minDate, maxDate, ...props }: Pagev
|
|||
order: 3,
|
||||
},
|
||||
{
|
||||
type: data.compare ? 'line' : 'bar',
|
||||
label: formatMessage(labels.views),
|
||||
data: generateTimeSeries(data.pageviews, minDate, maxDate, unit, dateLocale),
|
||||
barPercentage: 0.9,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export function WeeklyTraffic({ websiteId }: { websiteId: string }) {
|
|||
height="16px"
|
||||
borderRadius="full"
|
||||
style={{ margin: '0 auto' }}
|
||||
role="cell"
|
||||
>
|
||||
<Row
|
||||
backgroundColor="primary"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ export function isSearchOperator(operator: any) {
|
|||
}
|
||||
|
||||
export function filtersObjectToArray(filters: QueryFilters, options: QueryOptions = {}) {
|
||||
if (!filters) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(filters).reduce((arr, key) => {
|
||||
const filter = filters[key];
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export const fieldsParam = z.enum([
|
|||
'tag',
|
||||
'hostname',
|
||||
'language',
|
||||
'event',
|
||||
]);
|
||||
|
||||
export const reportTypeParam = z.enum([
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue