mirror of
https://github.com/umami-software/umami.git
synced 2026-02-18 03:25:40 +01:00
Compare commits
7 commits
128217c0f4
...
1498da2d02
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1498da2d02 | ||
|
|
4867492ca3 | ||
|
|
29a373467a | ||
|
|
5f316a79e5 | ||
|
|
7bb30443a8 | ||
|
|
7f43a0d41a | ||
|
|
14012ed68e |
20 changed files with 256 additions and 11 deletions
|
|
@ -3,14 +3,19 @@ import { Column } from '@umami/react-zen';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import { TeamsAddButton } from '../../teams/TeamsAddButton';
|
||||||
import { AdminTeamsDataTable } from './AdminTeamsDataTable';
|
import { AdminTeamsDataTable } from './AdminTeamsDataTable';
|
||||||
|
|
||||||
export function AdminTeamsPage() {
|
export function AdminTeamsPage() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
const handleSave = () => {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="6" margin="2">
|
<Column gap="6" margin="2">
|
||||||
<PageHeader title={formatMessage(labels.teams)} />
|
<PageHeader title={formatMessage(labels.teams)}>
|
||||||
|
<TeamsAddButton onSave={handleSave} isAdmin={true} />
|
||||||
|
</PageHeader>
|
||||||
<Panel>
|
<Panel>
|
||||||
<AdminTeamsDataTable />
|
<AdminTeamsDataTable />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,17 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||||
|
import { UserSelect } from '@/components/input/UserSelect';
|
||||||
|
|
||||||
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
|
export function TeamAddForm({
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
isAdmin,
|
||||||
|
}: {
|
||||||
|
onSave: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}) {
|
||||||
const { formatMessage, labels, getErrorMessage } = useMessages();
|
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||||
const { mutateAsync, error, isPending } = useUpdateQuery('/teams');
|
const { mutateAsync, error, isPending } = useUpdateQuery('/teams');
|
||||||
|
|
||||||
|
|
@ -26,6 +35,11 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose:
|
||||||
<FormField name="name" label={formatMessage(labels.name)}>
|
<FormField name="name" label={formatMessage(labels.name)}>
|
||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
{isAdmin && (
|
||||||
|
<FormField name="ownerId" label={formatMessage(labels.teamOwner)}>
|
||||||
|
<UserSelect buttonProps={{ style: { outline: 'none' } }} />
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<Button isDisabled={isPending} onPress={onClose}>
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
{formatMessage(labels.cancel)}
|
{formatMessage(labels.cancel)}
|
||||||
|
|
|
||||||
76
src/app/(main)/teams/TeamMemberAddForm.tsx
Normal file
76
src/app/(main)/teams/TeamMemberAddForm.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormButtons,
|
||||||
|
FormField,
|
||||||
|
FormSubmitButton,
|
||||||
|
ListItem,
|
||||||
|
Select,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||||
|
import { UserSelect } from '@/components/input/UserSelect';
|
||||||
|
import { ROLES } from '@/lib/constants';
|
||||||
|
|
||||||
|
const roles = [ROLES.teamManager, ROLES.teamMember, ROLES.teamViewOnly];
|
||||||
|
|
||||||
|
export function TeamMemberAddForm({
|
||||||
|
teamId,
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||||
|
const { mutateAsync, error, isPending } = useUpdateQuery(`/teams/${teamId}/users`);
|
||||||
|
|
||||||
|
const handleSubmit = async (data: any) => {
|
||||||
|
await mutateAsync(data, {
|
||||||
|
onSuccess: async () => {
|
||||||
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRole = role => {
|
||||||
|
switch (role) {
|
||||||
|
case ROLES.teamManager:
|
||||||
|
return formatMessage(labels.manager);
|
||||||
|
case ROLES.teamMember:
|
||||||
|
return formatMessage(labels.member);
|
||||||
|
case ROLES.teamViewOnly:
|
||||||
|
return formatMessage(labels.viewOnly);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}>
|
||||||
|
<FormField
|
||||||
|
name="userId"
|
||||||
|
label={formatMessage(labels.username)}
|
||||||
|
rules={{ required: 'Required' }}
|
||||||
|
>
|
||||||
|
<UserSelect teamId={teamId} />
|
||||||
|
</FormField>
|
||||||
|
<FormField name="role" label={formatMessage(labels.role)} rules={{ required: 'Required' }}>
|
||||||
|
<Select items={roles} renderValue={value => renderRole(value as any)}>
|
||||||
|
{roles.map(value => (
|
||||||
|
<ListItem key={value} id={value}>
|
||||||
|
{renderRole(value)}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormField>
|
||||||
|
<FormButtons>
|
||||||
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
|
{formatMessage(labels.cancel)}
|
||||||
|
</Button>
|
||||||
|
<FormSubmitButton variant="primary" isDisabled={isPending}>
|
||||||
|
{formatMessage(labels.save)}
|
||||||
|
</FormSubmitButton>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,13 @@ import { Plus } from '@/components/icons';
|
||||||
import { messages } from '@/components/messages';
|
import { messages } from '@/components/messages';
|
||||||
import { TeamAddForm } from './TeamAddForm';
|
import { TeamAddForm } from './TeamAddForm';
|
||||||
|
|
||||||
export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
|
export function TeamsAddButton({
|
||||||
|
onSave,
|
||||||
|
isAdmin = false,
|
||||||
|
}: {
|
||||||
|
onSave?: () => void;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { touch } = useModified();
|
const { touch } = useModified();
|
||||||
|
|
@ -25,7 +31,7 @@ export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
|
||||||
</Button>
|
</Button>
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog title={formatMessage(labels.createTeam)} style={{ width: 400 }}>
|
<Dialog title={formatMessage(labels.createTeam)} style={{ width: 400 }}>
|
||||||
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} />}
|
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} isAdmin={isAdmin} />}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
|
||||||
40
src/app/(main)/teams/TeamsMemberAddButton.tsx
Normal file
40
src/app/(main)/teams/TeamsMemberAddButton.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen';
|
||||||
|
import { useMessages, useModified } from '@/components/hooks';
|
||||||
|
import { Plus } from '@/components/icons';
|
||||||
|
import { messages } from '@/components/messages';
|
||||||
|
import { TeamMemberAddForm } from './TeamMemberAddForm';
|
||||||
|
|
||||||
|
export function TeamsMemberAddButton({
|
||||||
|
teamId,
|
||||||
|
onSave,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { touch } = useModified();
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
toast(formatMessage(messages.saved));
|
||||||
|
touch('teams:members');
|
||||||
|
onSave?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button>
|
||||||
|
<Icon>
|
||||||
|
<Plus />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.addMember)}</Text>
|
||||||
|
</Button>
|
||||||
|
<Modal>
|
||||||
|
<Dialog title={formatMessage(labels.addMember)} style={{ width: 400 }}>
|
||||||
|
{({ close }) => <TeamMemberAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</DialogTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column, Heading, Row } from '@umami/react-zen';
|
||||||
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
|
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useLoginQuery, useNavigation, useTeam } from '@/components/hooks';
|
import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks';
|
||||||
import { Users } from '@/components/icons';
|
import { Users } from '@/components/icons';
|
||||||
|
import { labels } from '@/components/messages';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
|
import { TeamsMemberAddButton } from '../TeamsMemberAddButton';
|
||||||
import { TeamEditForm } from './TeamEditForm';
|
import { TeamEditForm } from './TeamEditForm';
|
||||||
import { TeamManage } from './TeamManage';
|
import { TeamManage } from './TeamManage';
|
||||||
import { TeamMembersDataTable } from './TeamMembersDataTable';
|
import { TeamMembersDataTable } from './TeamMembersDataTable';
|
||||||
|
|
@ -13,6 +15,7 @@ export function TeamSettings({ teamId }: { teamId: string }) {
|
||||||
const team: any = useTeam();
|
const team: any = useTeam();
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
const { pathname } = useNavigation();
|
const { pathname } = useNavigation();
|
||||||
|
const { formatMessage } = useMessages();
|
||||||
|
|
||||||
const isAdmin = pathname.includes('/admin');
|
const isAdmin = pathname.includes('/admin');
|
||||||
|
|
||||||
|
|
@ -37,6 +40,10 @@ export function TeamSettings({ teamId }: { teamId: string }) {
|
||||||
<TeamEditForm teamId={teamId} allowEdit={canEdit} showAccessCode={canEdit} />
|
<TeamEditForm teamId={teamId} allowEdit={canEdit} showAccessCode={canEdit} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
|
<Row alignItems="center" justifyContent="space-between">
|
||||||
|
<Heading size="2">{formatMessage(labels.members)}</Heading>
|
||||||
|
{isAdmin && <TeamsMemberAddButton teamId={teamId} />}
|
||||||
|
</Row>
|
||||||
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
|
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
|
||||||
</Panel>
|
</Panel>
|
||||||
{isTeamOwner && (
|
{isTeamOwner && (
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column, Grid, ListItem, Row, SearchField, Select } from '@umami/react-zen';
|
import { Column, Grid, ListItem, Row, SearchField, Select } from '@umami/react-zen';
|
||||||
import { FilterButtons } from 'dist';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useDateRange, useMessages } from '@/components/hooks';
|
import { useDateRange, useMessages } from '@/components/hooks';
|
||||||
|
import { FilterButtons } from '@/components/input/FilterButtons';
|
||||||
import { Journey } from './Journey';
|
import { Journey } from './Journey';
|
||||||
|
|
||||||
const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];
|
const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,12 @@ export function WebsiteExpandedMenu({
|
||||||
path: updateParams({ view: 'hostname' }),
|
path: updateParams({ view: 'hostname' }),
|
||||||
icon: <Network />,
|
icon: <Network />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'distinctId',
|
||||||
|
label: formatMessage(labels.distinctId),
|
||||||
|
path: updateParams({ view: 'distinctId' }),
|
||||||
|
icon: <Tag />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tag',
|
id: 'tag',
|
||||||
label: formatMessage(labels.tag),
|
label: formatMessage(labels.tag),
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,11 @@ export function CompareTables({ websiteId }: { websiteId: string }) {
|
||||||
label: formatMessage(labels.hostname),
|
label: formatMessage(labels.hostname),
|
||||||
path: renderPath('hostname'),
|
path: renderPath('hostname'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'distinctId',
|
||||||
|
label: formatMessage(labels.distinctId),
|
||||||
|
path: renderPath('distinctId'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tag',
|
id: 'tag',
|
||||||
label: formatMessage(labels.tags),
|
label: formatMessage(labels.tags),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export async function GET(request: Request) {
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().max(50),
|
name: z.string().max(50),
|
||||||
|
ownerId: z.uuid().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
@ -40,7 +41,7 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name } = body;
|
const { name, ownerId } = body;
|
||||||
|
|
||||||
const team = await createTeam(
|
const team = await createTeam(
|
||||||
{
|
{
|
||||||
|
|
@ -48,7 +49,7 @@ export async function POST(request: Request) {
|
||||||
name,
|
name,
|
||||||
accessCode: `team_${getRandomChars(16)}`,
|
accessCode: `team_${getRandomChars(16)}`,
|
||||||
},
|
},
|
||||||
auth.user.id,
|
ownerId ?? auth.user.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return json(team);
|
return json(team);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export function useFields() {
|
||||||
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
||||||
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
||||||
{ name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
|
{ name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
|
||||||
|
{ name: 'distinctId', type: 'string', label: formatMessage(labels.distinctId) },
|
||||||
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
|
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
|
||||||
{ name: 'event', type: 'string', label: formatMessage(labels.event) },
|
{ name: 'event', type: 'string', label: formatMessage(labels.event) },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export function useFilterParameters() {
|
||||||
event,
|
event,
|
||||||
tag,
|
tag,
|
||||||
hostname,
|
hostname,
|
||||||
|
distinctId,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
search,
|
search,
|
||||||
|
|
@ -42,6 +43,7 @@ export function useFilterParameters() {
|
||||||
event,
|
event,
|
||||||
tag,
|
tag,
|
||||||
hostname,
|
hostname,
|
||||||
|
distinctId,
|
||||||
search,
|
search,
|
||||||
segment,
|
segment,
|
||||||
cohort,
|
cohort,
|
||||||
|
|
@ -61,6 +63,7 @@ export function useFilterParameters() {
|
||||||
event,
|
event,
|
||||||
tag,
|
tag,
|
||||||
hostname,
|
hostname,
|
||||||
|
distinctId,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
search,
|
search,
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@ export function FilterEditForm({ websiteId, onChange, onClose }: FilterEditFormP
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
value={currentFilters}
|
value={currentFilters}
|
||||||
onChange={setCurrentFilters}
|
onChange={setCurrentFilters}
|
||||||
exclude={excludeFilters ? ['path', 'title', 'hostname', 'tag', 'event'] : []}
|
exclude={
|
||||||
|
excludeFilters ? ['path', 'title', 'hostname', 'distinctId', 'tag', 'event'] : []
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="segments">
|
<TabPanel id="segments">
|
||||||
|
|
|
||||||
71
src/components/input/UserSelect.tsx
Normal file
71
src/components/input/UserSelect.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Empty } from '@/components/common/Empty';
|
||||||
|
import { useMessages, useTeamMembersQuery, useUsersQuery } from '@/components/hooks';
|
||||||
|
|
||||||
|
export function UserSelect({
|
||||||
|
teamId,
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
teamId?: string;
|
||||||
|
} & SelectProps) {
|
||||||
|
const { formatMessage, messages } = useMessages();
|
||||||
|
const { data: users, isLoading: usersLoading } = useUsersQuery();
|
||||||
|
const { data: teamMembers, isLoading: teamMembersLoading } = useTeamMembersQuery(teamId);
|
||||||
|
const [username, setUsername] = useState<string>();
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const listItems = useMemo(() => {
|
||||||
|
if (!users) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!teamId || !teamMembers) {
|
||||||
|
return users.data;
|
||||||
|
}
|
||||||
|
const teamMemberIds = teamMembers.data.map(({ userId }) => userId);
|
||||||
|
return users.data.filter(({ id }) => !teamMemberIds.includes(id));
|
||||||
|
}, [users, teamMembers, teamId]);
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = () => {
|
||||||
|
setSearch('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (id: string) => {
|
||||||
|
setUsername(listItems.find(item => item.id === id)?.username);
|
||||||
|
onChange(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderValue = () => {
|
||||||
|
return (
|
||||||
|
<Row maxWidth="160px">
|
||||||
|
<Text truncate>{username}</Text>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
{...props}
|
||||||
|
items={listItems}
|
||||||
|
value={username}
|
||||||
|
isLoading={usersLoading || (teamId && teamMembersLoading)}
|
||||||
|
allowSearch={true}
|
||||||
|
searchValue={search}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onChange={handleChange}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
renderValue={renderValue}
|
||||||
|
listProps={{
|
||||||
|
renderEmptyState: () => <Empty message={formatMessage(messages.noResultsFound)} />,
|
||||||
|
style: { maxHeight: 'calc(42vh - 65px)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ id, username }: any) => <ListItem key={id}>{username}</ListItem>}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -19,11 +19,13 @@ export * from '@/app/(main)/teams/TeamAddForm';
|
||||||
export * from '@/app/(main)/teams/TeamJoinForm';
|
export * from '@/app/(main)/teams/TeamJoinForm';
|
||||||
export * from '@/app/(main)/teams/TeamLeaveButton';
|
export * from '@/app/(main)/teams/TeamLeaveButton';
|
||||||
export * from '@/app/(main)/teams/TeamLeaveForm';
|
export * from '@/app/(main)/teams/TeamLeaveForm';
|
||||||
|
export * from '@/app/(main)/teams/TeamMemberAddForm';
|
||||||
export * from '@/app/(main)/teams/TeamProvider';
|
export * from '@/app/(main)/teams/TeamProvider';
|
||||||
export * from '@/app/(main)/teams/TeamsAddButton';
|
export * from '@/app/(main)/teams/TeamsAddButton';
|
||||||
export * from '@/app/(main)/teams/TeamsDataTable';
|
export * from '@/app/(main)/teams/TeamsDataTable';
|
||||||
export * from '@/app/(main)/teams/TeamsHeader';
|
export * from '@/app/(main)/teams/TeamsHeader';
|
||||||
export * from '@/app/(main)/teams/TeamsJoinButton';
|
export * from '@/app/(main)/teams/TeamsJoinButton';
|
||||||
|
export * from '@/app/(main)/teams/TeamsMemberAddButton';
|
||||||
export * from '@/app/(main)/teams/TeamsTable';
|
export * from '@/app/(main)/teams/TeamsTable';
|
||||||
export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteData';
|
export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteData';
|
||||||
export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm';
|
export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm';
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ export const SESSION_COLUMNS = [
|
||||||
'country',
|
'country',
|
||||||
'city',
|
'city',
|
||||||
'region',
|
'region',
|
||||||
|
'distinctId',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SEGMENT_TYPES = {
|
export const SEGMENT_TYPES = {
|
||||||
|
|
@ -69,6 +70,7 @@ export const FILTER_COLUMNS = {
|
||||||
referrer: 'referrer_domain',
|
referrer: 'referrer_domain',
|
||||||
domain: 'referrer_domain',
|
domain: 'referrer_domain',
|
||||||
hostname: 'hostname',
|
hostname: 'hostname',
|
||||||
|
distinctId: 'distinct_id',
|
||||||
title: 'page_title',
|
title: 'page_title',
|
||||||
query: 'url_query',
|
query: 'url_query',
|
||||||
os: 'os',
|
os: 'os',
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export const filterParams = {
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
tag: z.string().optional(),
|
tag: z.string().optional(),
|
||||||
hostname: z.string().optional(),
|
hostname: z.string().optional(),
|
||||||
|
distinctId: z.string().optional(),
|
||||||
language: z.string().optional(),
|
language: z.string().optional(),
|
||||||
event: z.string().optional(),
|
event: z.string().optional(),
|
||||||
segment: z.uuid().optional(),
|
segment: z.uuid().optional(),
|
||||||
|
|
@ -89,6 +90,7 @@ export const fieldsParam = z.enum([
|
||||||
'city',
|
'city',
|
||||||
'tag',
|
'tag',
|
||||||
'hostname',
|
'hostname',
|
||||||
|
'distinctId',
|
||||||
'language',
|
'language',
|
||||||
'event',
|
'event',
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ async function relationalQuery(
|
||||||
${filterQuery}
|
${filterQuery}
|
||||||
group by name, website_event.session_id, website_event.visit_id
|
group by name, website_event.session_id, website_event.visit_id
|
||||||
) as t
|
) as t
|
||||||
|
where name != ''
|
||||||
group by name
|
group by name
|
||||||
order by visitors desc, visits desc
|
order by visitors desc, visits desc
|
||||||
limit ${limit}
|
limit ${limit}
|
||||||
|
|
|
||||||
|
|
@ -131,5 +131,5 @@ function parseFields(fields: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFieldsByName(fields: string[]) {
|
function parseFieldsByName(fields: string[]) {
|
||||||
return `${fields.map(name => name).join(',')}`;
|
return `${fields.map(name => `"${name}"`).join(',')}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ async function relationalQuery(
|
||||||
group by name, website_event.session_id, website_event.visit_id
|
group by name, website_event.session_id, website_event.visit_id
|
||||||
${includeCountry ? ', country' : ''}
|
${includeCountry ? ', country' : ''}
|
||||||
) as t
|
) as t
|
||||||
|
where name != ''
|
||||||
group by name
|
group by name
|
||||||
${includeCountry ? ', country' : ''}
|
${includeCountry ? ', country' : ''}
|
||||||
order by visitors desc, visits desc
|
order by visitors desc, visits desc
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue