Updated edit forms and icons.
Some checks failed
Create docker images / Build, push, and deploy (push) Has been cancelled
Node.js CI / build (postgresql, 18.18, 10) (push) Has been cancelled

This commit is contained in:
Mike Cao 2025-09-26 01:46:36 -07:00
parent 554054d3a1
commit 257050f749
20 changed files with 120 additions and 123 deletions

View file

@ -9,7 +9,8 @@ import {
SidebarProps,
ThemeButton,
} from '@umami/react-zen';
import { Globe, LinkIcon, LogoSvg, Grid2x2, PanelLeft } from '@/components/icons';
import { Globe, LinkIcon, Grid2x2, PanelLeft } from '@/components/icons';
import { Logo } from '@/components/svg';
import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
import { NavButton } from '@/components/input/NavButton';
import { PanelButton } from '@/components/input/PanelButton';
@ -53,7 +54,7 @@ export function SideNav(props: SidebarProps) {
<SidebarSection onClick={() => setIsCollapsed(false)}>
<SidebarHeader
label="umami"
icon={isCollapsed && !hasNav ? <PanelLeft /> : <LogoSvg />}
icon={isCollapsed && !hasNav ? <PanelLeft /> : <Logo />}
style={{ maxHeight: 40 }}
>
{!isCollapsed && !hasNav && <PanelButton />}

View file

@ -3,7 +3,7 @@ import { SettingsLayout } from './SettingsLayout';
export default function ({ children }) {
if (process.env.cloudMode) {
return null;
//return null;
}
return <SettingsLayout>{children}</SettingsLayout>;

View file

@ -5,9 +5,12 @@ import {
FormSubmitButton,
TextField,
Button,
IconLabel,
Row,
} from '@umami/react-zen';
import { getRandomChars } from '@/lib/generate';
import { useMessages, useTeam, useUpdateQuery } from '@/components/hooks';
import { RefreshCw } from '@/components/icons';
const generateId = () => `team_${getRandomChars(16)}`;
@ -54,15 +57,25 @@ export function TeamEditForm({
<TextField isReadOnly={!allowEdit} />
</FormField>
{showAccessCode && (
<FormField name="accessCode" label={formatMessage(labels.accessCode)}>
<TextField isReadOnly allowCopy />
</FormField>
<Row alignItems="flex-end" gap>
<FormField
name="accessCode"
label={formatMessage(labels.accessCode)}
style={{ flex: 1 }}
>
<TextField isReadOnly allowCopy />
</FormField>
{allowEdit && (
<Button
onPress={() => setValue('accessCode', generateId(), { shouldDirty: true })}
>
<IconLabel icon={<RefreshCw />} label={formatMessage(labels.regenerate)} />
</Button>
)}
</Row>
)}
{allowEdit && (
<FormButtons justifyContent="space-between">
<Button onPress={() => setValue('accessCode', generateId(), { shouldDirty: true })}>
{formatMessage(labels.regenerate)}
</Button>
<FormButtons justifyContent="flex-end">
<FormSubmitButton variant="primary" isPending={isPending}>
{formatMessage(labels.save)}
</FormSubmitButton>

View file

@ -1,8 +1,7 @@
import Link from 'next/link';
import { Column, Icon, Text, Row } from '@umami/react-zen';
import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks';
import { Column } from '@umami/react-zen';
import { useLoginQuery, useNavigation, useTeam } from '@/components/hooks';
import { ROLES } from '@/lib/constants';
import { Users, ArrowRight } from '@/components/icons';
import { Users } from '@/components/icons';
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
import { TeamManage } from './TeamManage';
import { TeamEditForm } from './TeamEditForm';
@ -13,7 +12,6 @@ import { Panel } from '@/components/common/Panel';
export function TeamSettings({ teamId }: { teamId: string }) {
const team: any = useTeam();
const { user } = useLoginQuery();
const { formatMessage, labels } = useMessages();
const { pathname } = useNavigation();
const isAdmin = pathname.includes('/admin');
@ -31,32 +29,21 @@ export function TeamSettings({ teamId }: { teamId: string }) {
user.role !== ROLES.viewOnly);
return (
<>
<Link href="/settings/teams">
<Row marginTop="2" alignItems="center" gap>
<Icon rotate={180}>
<ArrowRight />
</Icon>
<Text>{formatMessage(labels.teams)}</Text>
</Row>
</Link>
<Column gap="6">
<PageHeader title={team?.name} icon={<Users />}>
{!isTeamOwner && !isAdmin && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
</PageHeader>
<Column gap="6">
<PageHeader title={team?.name} icon={<Users />}>
{!isTeamOwner && !isAdmin && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
</PageHeader>
<Panel>
<TeamEditForm teamId={teamId} allowEdit={canEdit} showAccessCode={canEdit} />
</Panel>
<Panel>
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
</Panel>
{isTeamOwner && (
<Panel>
<TeamEditForm teamId={teamId} allowEdit={canEdit} showAccessCode={canEdit} />
<TeamManage teamId={teamId} />
</Panel>
<Panel>
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
</Panel>
{isTeamOwner && (
<Panel>
<TeamManage teamId={teamId} />
</Panel>
)}
</Column>
</>
)}
</Column>
);
}

View file

@ -1,7 +1,8 @@
import { Grid, Column, Row, Text, Icon, ProgressBar, Dialog, Box } from '@umami/react-zen';
import { useMessages, useResultQuery } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { File, LightningSvg, User } from '@/components/icons';
import { File, User } from '@/components/icons';
import { Lightning } from '@/components/svg';
import { formatLongNumber } from '@/lib/format';
import { ReportEditButton } from '@/components/input/ReportEditButton';
import { FunnelEditForm } from './FunnelEditForm';
@ -92,7 +93,7 @@ export function Funnel({ id, name, type, parameters, websiteId }) {
</Row>
<Row alignItems="center" justifyContent="space-between" gap>
<Row alignItems="center" gap>
<Icon>{type === 'path' ? <File /> : <LightningSvg />}</Icon>
<Icon>{type === 'path' ? <File /> : <Lightning />}</Icon>
<Text>{value}</Text>
</Row>
<Row alignItems="center" gap>

View file

@ -1,7 +1,7 @@
import { Grid, Row, Column, Text, Icon, ProgressBar, Dialog } from '@umami/react-zen';
import { ReportEditButton } from '@/components/input/ReportEditButton';
import { useMessages, useResultQuery } from '@/components/hooks';
import { File, LightningSvg, User } from '@/components/icons';
import { File, Lightning, User } from '@/components/icons';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { formatLongNumber } from '@/lib/format';
import { GoalEditForm } from './GoalEditForm';
@ -68,7 +68,7 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
</Row>
<Row alignItems="center" justifyContent="space-between" gap>
<Row alignItems="center" gap>
<Icon>{parameters.type === 'path' ? <File /> : <LightningSvg />}</Icon>
<Icon>{parameters.type === 'path' ? <File /> : <Lightning />}</Icon>
<Text>{parameters.value}</Text>
</Row>
<Row alignItems="center" gap>

View file

@ -3,7 +3,7 @@ import { TooltipTrigger, Tooltip, Focusable, Icon, Text, Row, Column } from '@um
import { firstBy } from 'thenby';
import classNames from 'classnames';
import { useEscapeKey, useMessages, useResultQuery } from '@/components/hooks';
import { File, LightningSvg } from '@/components/icons';
import { File, Lightning } from '@/components/icons';
import { objectToArray } from '@/lib/data';
import { formatLongNumber } from '@/lib/format';
import { LoadingPanel } from '@/components/common/LoadingPanel';
@ -215,7 +215,7 @@ export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps)
onClick={() => handleClick(name, columnIndex, paths)}
>
<Row alignItems="center" className={styles.name} title={name} gap>
<Icon>{name.startsWith('/') ? <File /> : <LightningSvg />}</Icon>
<Icon>{name.startsWith('/') ? <File /> : <Lightning />}</Icon>
<Text truncate>{name}</Text>
</Row>
<div className={styles.count} title={nodeCount}>

View file

@ -19,10 +19,10 @@ import {
Languages,
Monitor,
Cpu,
LightningSvg,
Network,
Tag,
} from '@/components/icons';
import { Lightning } from '@/components/svg';
export function WebsiteExpandedView({
websiteId,
@ -161,7 +161,7 @@ export function WebsiteExpandedView({
id: 'event',
label: formatMessage(labels.event),
path: updateParams({ view: 'event' }),
icon: <LightningSvg />,
icon: <Lightning />,
},
{
id: 'hostname',

View file

@ -46,7 +46,7 @@ const ShareButton = ({ websiteId, shareId }) => {
<Text>Share</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.share)} style={{ width: 600 }}>
<Dialog title={formatMessage(labels.share)} style={{ width: 800 }}>
{({ close }) => {
return <WebsiteShareForm websiteId={websiteId} shareId={shareId} onClose={close} />;
}}

View file

@ -1,20 +1,5 @@
import {
Eye,
LightningSvg,
User,
Clock,
Sheet,
TargetSvg,
FunnelSvg,
PathSvg,
MagnetSvg,
Tag,
MoneySvg,
NetworkSvg,
ChartPie,
UserPlus,
CompareSvg,
} from '@/components/icons';
import { Eye, User, Clock, Sheet, Tag, ChartPie, UserPlus } from '@/components/icons';
import { Lightning, Path, Money, Compare, Target, Funnel, Magnet, Network } from '@/components/svg';
import { useMessages, useNavigation } from '@/components/hooks';
import { SideMenu } from '@/components/common/SideMenu';
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
@ -44,7 +29,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{
id: 'events',
label: formatMessage(labels.events),
icon: <LightningSvg />,
icon: <Lightning />,
path: renderPath('/events'),
},
{
@ -62,7 +47,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{
id: 'compare',
label: formatMessage(labels.compare),
icon: <CompareSvg />,
icon: <Compare />,
path: renderPath('/compare'),
},
{
@ -79,25 +64,25 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{
id: 'goals',
label: formatMessage(labels.goals),
icon: <TargetSvg />,
icon: <Target />,
path: renderPath('/goals'),
},
{
id: 'funnel',
label: formatMessage(labels.funnels),
icon: <FunnelSvg />,
icon: <Funnel />,
path: renderPath('/funnels'),
},
{
id: 'journeys',
label: formatMessage(labels.journeys),
icon: <PathSvg />,
icon: <Path />,
path: renderPath('/journeys'),
},
{
id: 'retention',
label: formatMessage(labels.retention),
icon: <MagnetSvg />,
icon: <Magnet />,
path: renderPath('/retention'),
},
],
@ -131,13 +116,13 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
{
id: 'revenue',
label: formatMessage(labels.revenue),
icon: <MoneySvg />,
icon: <Money />,
path: renderPath('/revenue'),
},
{
id: 'attribution',
label: formatMessage(labels.attribution),
icon: <NetworkSvg />,
icon: <Network />,
path: renderPath('/attribution'),
},
],

View file

@ -3,7 +3,8 @@ import { useFormat, useMessages, useNavigation } from '@/components/hooks';
import { Empty } from '@/components/common/Empty';
import { Avatar } from '@/components/common/Avatar';
import Link from 'next/link';
import { LightningSvg, Eye } from '@/components/icons';
import { Eye } from '@/components/icons';
import { Lightning } from '@/components/svg';
import { DateDistance } from '@/components/common/DateDistance';
import { TypeIcon } from '@/components/common/TypeIcon';
@ -25,7 +26,7 @@ export function EventsTable({ data = [] }) {
<Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
<Avatar seed={row.sessionId} size={32} />
</Link>
<Icon>{row.eventName ? <LightningSvg /> : <Eye />}</Icon>
<Icon>{row.eventName ? <Lightning /> : <Eye />}</Icon>
<Text>
{formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
</Text>

View file

@ -8,7 +8,7 @@ import {
useTimezone,
useWebsite,
} from '@/components/hooks';
import { Eye, User, LightningSvg } from '@/components/icons';
import { Eye, User, Lightning } from '@/components/icons';
import { BROWSERS, OS_NAMES } from '@/lib/constants';
import { stringToColor } from '@/lib/format';
import { useMemo, useState } from 'react';
@ -24,7 +24,7 @@ const TYPE_EVENT = 'event';
const icons = {
[TYPE_PAGEVIEW]: <Eye />,
[TYPE_SESSION]: <User />,
[TYPE_EVENT]: <LightningSvg />,
[TYPE_EVENT]: <Lightning />,
};
export function RealtimeLog({ data }: { data: any }) {

View file

@ -12,7 +12,7 @@ import {
Dialog,
} from '@umami/react-zen';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { LightningSvg, Eye, FileText } from '@/components/icons';
import { Lightning, Eye, FileText } from '@/components/icons';
import { useMessages, useSessionActivityQuery, useTimezone } from '@/components/hooks';
import { EventData } from '@/components/metrics/EventData';
@ -52,7 +52,7 @@ export function SessionActivity({
{formatTimezoneDate(createdAt, 'pp')}
</StatusLight>
<Row alignItems="center" gap="2">
<Icon>{eventName ? <LightningSvg /> : <Eye />}</Icon>
<Icon>{eventName ? <Lightning /> : <Eye />}</Icon>
<Text>
{eventName
? formatMessage(labels.triggeredEvent)

View file

@ -2,7 +2,8 @@ import { ReactNode } from 'react';
import { Icon, TextField, Column, Row, Label, Text } from '@umami/react-zen';
import { useFormat, useLocale, useMessages, useRegionNames } from '@/components/hooks';
import { TypeIcon } from '@/components/common/TypeIcon';
import { LocationSvg, KeyRound, Calendar } from '@/components/icons';
import { KeyRound, Calendar } from '@/components/icons';
import { Location } from '@/components/svg';
import { DateDistance } from '@/components/common/DateDistance';
export function SessionInfo({ data }) {
@ -36,11 +37,11 @@ export function SessionInfo({ data }) {
{formatValue(data?.country, 'country')}
</Info>
<Info label={formatMessage(labels.region)} icon={<LocationSvg />}>
<Info label={formatMessage(labels.region)} icon={<Location />}>
{getRegionName(data?.region)}
</Info>
<Info label={formatMessage(labels.city)} icon={<LocationSvg />}>
<Info label={formatMessage(labels.city)} icon={<Location />}>
{data?.city}
</Info>

View file

@ -8,10 +8,12 @@ import {
Column,
Label,
Row,
IconLabel,
} from '@umami/react-zen';
import { useState } from 'react';
import { getRandomChars } from '@/lib/generate';
import { useMessages, useUpdateQuery } from '@/components/hooks';
import { useMessages, useUpdateQuery, useConfig } from '@/components/hooks';
import { RefreshCcw } from 'lucide-react';
const generateId = () => getRandomChars(16);
@ -24,22 +26,31 @@ export interface WebsiteShareFormProps {
export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
const [id, setId] = useState(shareId);
const [currentId, setCurrentId] = useState(shareId);
const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
const { cloudMode } = useConfig();
const url = `${window?.location.origin || ''}${process.env.basePath || ''}/share/${id}`;
const getUrl = (shareId: string) => {
if (cloudMode) {
return `${process.env.cloudUrl}/share/${shareId}`;
}
return `${window?.location.origin}${process.env.basePath || ''}/share/${shareId}`;
};
const url = getUrl(currentId);
const handleGenerate = () => {
setId(generateId());
setCurrentId(generateId());
};
const handleSwitch = () => {
setId(id ? null : generateId());
setCurrentId(currentId ? null : generateId());
};
const handleSave = async () => {
const data = {
shareId: id,
shareId: currentId,
};
await mutateAsync(data, {
onSuccess: async () => {
@ -54,19 +65,23 @@ export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: Websit
return (
<Form onSubmit={handleSave} error={getErrorMessage(error)} values={{ url }}>
<Column gap>
<Switch isSelected={!!id} onChange={handleSwitch}>
<Switch isSelected={!!currentId} onChange={handleSwitch}>
{formatMessage(labels.enableShareUrl)}
</Switch>
{id && (
<Column>
<Label>{formatMessage(labels.shareUrl)}</Label>
<TextField value={url} isReadOnly allowCopy />
</Column>
)}
<FormButtons justifyContent="space-between">
<Row>
{id && <Button onPress={handleGenerate}>{formatMessage(labels.regenerate)}</Button>}
{currentId && (
<Row alignItems="flex-end" gap>
<Column flexGrow={1}>
<Label>{formatMessage(labels.shareUrl)}</Label>
<TextField value={url} isReadOnly allowCopy />
</Column>
<Column>
<Button onPress={handleGenerate}>
<IconLabel icon={<RefreshCcw />} label={formatMessage(labels.regenerate)} />
</Button>
</Column>
</Row>
)}
<FormButtons justifyContent="flex-end">
<Row alignItems="center" gap>
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
<FormSubmitButton isDisabled={false}>{formatMessage(labels.save)}</FormSubmitButton>

View file

@ -16,11 +16,17 @@ export function WebsiteTrackingCode({
const trackerScriptName =
config?.trackerScriptName?.split(',')?.map((n: string) => n.trim())?.[0] || SCRIPT_NAME;
const url = trackerScriptName?.startsWith('http')
? trackerScriptName
: `${hostUrl || window?.location.origin || ''}${
process.env.basePath || ''
}/${trackerScriptName}`;
const getUrl = () => {
if (config?.cloudMode) {
return `${process.env.cloudUrl}/${trackerScriptName}`;
}
return `${hostUrl || window?.location?.origin || ''}${
process.env.basePath || ''
}/${trackerScriptName}`;
};
const url = trackerScriptName?.startsWith('http') ? trackerScriptName : getUrl();
const code = `<script defer src="${url}" data-website-id="${websiteId}"></script>`;

View file

@ -13,7 +13,7 @@ import { useRouter } from 'next/navigation';
import { useMessages, useUpdateQuery } from '@/components/hooks';
import { setUser } from '@/store/app';
import { setClientAuthToken } from '@/lib/client';
import { LogoSvg } from '@/components/icons';
import { Logo } from '@/components/svg';
export function LoginForm() {
const { formatMessage, labels, getErrorMessage } = useMessages();
@ -34,7 +34,7 @@ export function LoginForm() {
return (
<Column justifyContent="center" alignItems="center" gap="6">
<Icon size="lg">
<LogoSvg />
<Logo />
</Icon>
<Heading>umami</Heading>
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}>

View file

@ -1,7 +1,7 @@
import { Row, Icon, Text, ThemeButton } from '@umami/react-zen';
import { LanguageButton } from '@/components/input/LanguageButton';
import { PreferencesButton } from '@/components/input/PreferencesButton';
import { LogoSvg } from '@/components/icons';
import { Logo } from '@/components/svg';
export function Header() {
return (
@ -9,7 +9,7 @@ export function Header() {
<a href="https://umami.is" target="_blank">
<Row alignItems="center" gap>
<Icon>
<LogoSvg />
<Logo />
</Icon>
<Text weight="bold">umami</Text>
</Row>

View file

@ -1,14 +1 @@
export * from 'lucide-react';
export {
Compare as CompareSvg,
Funnel as FunnelSvg,
Lightning as LightningSvg,
Location as LocationSvg,
Logo as LogoSvg,
Magnet as MagnetSvg,
Money as MoneySvg,
Network as NetworkSvg,
Path as PathSvg,
Switch as SwitchSvg,
Target as TargetSvg,
} from '@/components/svg';

View file

@ -24,8 +24,8 @@ import {
Settings,
User,
Users,
SwitchSvg,
} from '@/components/icons';
import { Switch } from '@/components/svg';
import { DOCS_URL } from '@/lib/constants';
import { ArrowRight } from 'lucide-react';
@ -79,7 +79,7 @@ export function NavButton({ showText = true }: TeamsButtonProps) {
<Menu autoFocus="last" onAction={handleAction}>
<SubmenuTrigger>
<MenuItem id="teams" showChecked={false} showSubMenuIcon>
<IconLabel icon={<SwitchSvg />} label={formatMessage(labels.switchAccount)} />
<IconLabel icon={<Switch />} label={formatMessage(labels.switchAccount)} />
</MenuItem>
<Popover placement="right top">
<Column minWidth="300px">