Updates for cloud mode.
Some checks failed
Node.js CI / build (postgresql, 18.18) (push) Has been cancelled

This commit is contained in:
Mike Cao 2025-09-04 20:27:42 -07:00
parent dc1736458b
commit f40e1b44f3
51 changed files with 251 additions and 173 deletions

View file

@ -1,15 +1,10 @@
import 'dotenv/config';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pkg = require('./package.json');
import pkg from './package.json' assert { type: 'json' };
const TRACKER_SCRIPT = '/script.js';
const basePath = process.env.BASE_PATH;
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT;
const cloudMode = process.env.CLOUD_MODE;
const cloudUrl = process.env.CLOUD_URL;
const corsMaxAge = process.env.CORS_MAX_AGE;
const defaultLocale = process.env.DEFAULT_LOCALE;
@ -162,7 +157,7 @@ if (trackerScriptName) {
}
}
if (cloudMode && cloudUrl) {
if (cloudUrl) {
redirects.push({
source: '/login',
destination: cloudUrl,
@ -175,7 +170,6 @@ export default {
reactStrictMode: false,
env: {
basePath,
cloudMode,
cloudUrl,
currentVersion: pkg.version,
defaultLocale,

View file

@ -1,6 +1,6 @@
{
"name": "@umami/components",
"version": "0.115.0",
"version": "0.116.0",
"description": "Umami React components.",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",

View file

@ -82,7 +82,7 @@
"@react-spring/web": "^10.0.1",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.85.5",
"@umami/react-zen": "^0.176.0",
"@umami/react-zen": "^0.179.0",
"@umami/redis-client": "^0.29.0",
"bcryptjs": "^3.0.2",
"chalk": "^5.6.0",

10
pnpm-lock.yaml generated
View file

@ -45,8 +45,8 @@ importers:
specifier: ^5.85.5
version: 5.85.5(react@19.1.1)
'@umami/react-zen':
specifier: ^0.176.0
version: 0.176.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))
specifier: ^0.179.0
version: 0.179.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))
'@umami/redis-client':
specifier: ^0.29.0
version: 0.29.0
@ -2735,8 +2735,8 @@ packages:
'@prisma/client': ^6.1.0
'@prisma/extension-read-replicas': ^0.4.1
'@umami/react-zen@0.176.0':
resolution: {integrity: sha512-GP+Df68w0Kfo09ZC5WgK1YCg/IbqOz7HWPw8PLYG3shYm+feuF8ND8DHjoTle4qXY4oRJFufXCEsDbTb8FITkg==}
'@umami/react-zen@0.179.0':
resolution: {integrity: sha512-OS6Xa1tMO2PfXYXEB98XyOBctvnw/gE2eO3gK5mkJF5P3Ati6Z9KpTpj28Fxi7PgJiWYqcQ5enFzQqHXjYy/3A==}
'@umami/redis-client@0.29.0':
resolution: {integrity: sha512-Jaqh++jskqDB7ny75pfC02OvKp1JTS4asGDsFrRL3qy8sxL3PAl9+/mybCJe4/6vWrXDJKqpgkSfUDJq2bFjyw==}
@ -10333,7 +10333,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@umami/react-zen@0.176.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))':
'@umami/react-zen@0.179.0(@babel/core@7.28.3)(@types/react@19.1.12)(babel-plugin-react-compiler@19.1.0-rc.2)(immer@10.1.1)(use-sync-external-store@1.5.0(react@19.1.1))':
dependencies:
'@fontsource/jetbrains-mono': 5.2.6
'@internationalized/date': 3.9.0

View file

@ -22,6 +22,13 @@ if (!process.env.SKIP_DB_CHECK && !process.env.DATABASE_TYPE) {
checkMissing(['DATABASE_URL']);
}
if (process.env.CLOUD_MODE) {
checkMissing(['CLOUD_URL', 'KAFKA_BROKER', 'KAFKA_URL', 'REDIS_URL', 'KAFKA_SASL_MECHANISM']);
if (process.env.CLOUD_URL) {
checkMissing([
'CLOUD_URL',
'CLICKHOUSE_URL',
'REDIS_URL',
'KAFKA_BROKER',
'KAFKA_URL',
'KAFKA_SASL_MECHANISM',
]);
}

View file

@ -11,8 +11,8 @@ export function App({ children }) {
const config = useConfig();
const pathname = usePathname();
if (isLoading) {
return <Loading position="page" />;
if (isLoading || !config) {
return <Loading placement="absolute" />;
}
if (error) {

View file

@ -14,15 +14,13 @@ import {
Link as LinkIcon,
Logo,
Pixel,
Settings,
PanelLeft,
} from '@/components/icons';
import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
import { TeamsButton } from '@/components/input/TeamsButton';
import { PanelButton } from '@/components/input/PanelButton';
import { ProfileButton } from '@/components/input/ProfileButton';
import { LanguageButton } from '@/components/input/LanguageButton';
import { Key } from 'react';
import { SettingsButton } from '@/components/input/SettingsButton';
export function SideNav(props: SidebarProps) {
const { formatMessage, labels } = useMessages();
@ -58,17 +56,7 @@ export function SideNav(props: SidebarProps) {
},
];
const bottomLinks = [
{
id: 'settings',
label: formatMessage(labels.settings),
path: renderUrl('/settings'),
icon: <Settings />,
},
];
const handleSelect = (id: Key) => {
console.log({ id });
router.push(id === 'user' ? '/websites' : `/teams/${id}/websites`);
};
@ -78,18 +66,15 @@ export function SideNav(props: SidebarProps) {
<SidebarSection onClick={() => setIsCollapsed(false)}>
<SidebarHeader
label="umami"
icon={
isCollapsed && !hasNav ? (
<PanelLeft />
) : (
<Logo onClick={() => (window.location.href = process.env.cloudUrl)} />
)
}
icon={isCollapsed && !hasNav ? <PanelLeft /> : <Logo />}
style={{ maxHeight: 40 }}
>
{!isCollapsed && !hasNav && <PanelButton />}
</SidebarHeader>
</SidebarSection>
<SidebarSection paddingTop="0" paddingBottom="0" justifyContent="center">
<TeamsButton showText={!hasNav && !isCollapsed} onAction={handleSelect} />
</SidebarSection>
<SidebarSection flexGrow={1}>
{links.map(({ id, path, label, icon }) => {
return (
@ -104,30 +89,10 @@ export function SideNav(props: SidebarProps) {
);
})}
</SidebarSection>
<SidebarSection style={{ paddingTop: 0, paddingBottom: 0 }}>
<TeamsButton showText={!hasNav && !isCollapsed} onAction={handleSelect} />
</SidebarSection>
<SidebarSection>
{bottomLinks.map(({ id, path, label, icon }) => {
return (
<Link key={id} href={path} role="button">
<SidebarItem
label={label}
icon={icon}
isSelected={pathname.includes(path)}
role="button"
/>
</Link>
);
})}
<Row alignItems="center" height="40px">
<ProfileButton />
{!isCollapsed && !hasNav && (
<Row>
<LanguageButton />
<ThemeButton />
</Row>
)}
<SidebarSection justifyContent="flex-start">
<Row>
<SettingsButton />
{!isCollapsed && !hasNav && <ThemeButton />}
</Row>
</SidebarSection>
</Sidebar>

View file

@ -18,7 +18,7 @@ export function UpdateNotice({ user, config }) {
!config?.updatesDisabled &&
!config?.privateMode &&
!pathname.includes('/share/') &&
!process.env.cloudMode &&
!process.env.cloudUrl &&
!dismissed;
const updateCheck = useCallback(() => {

View file

@ -11,7 +11,7 @@ export function AdminLayout({ children }: { children: ReactNode }) {
const { formatMessage, labels } = useMessages();
const { pathname } = useNavigation();
if (!user.isAdmin) {
if (!user.isAdmin || process.env.cloudUrl) {
return null;
}

View file

@ -2,7 +2,7 @@ import { Metadata } from 'next';
import { AdminLayout } from './AdminLayout';
export default function ({ children }) {
if (process.env.cloudMode) {
if (process.env.cloudUrl) {
return null;
}

View file

@ -9,7 +9,7 @@ export function UserProvider({ userId, children }: { userId: string; children: R
const { data: user, isFetching, isLoading } = useUserQuery(userId);
if (isFetching && isLoading) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
if (!user) {

View file

@ -78,7 +78,7 @@ export function LinkEditForm({
}, [data]);
if (linkId && isLoading) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
return (

View file

@ -10,7 +10,7 @@ export function LinkProvider({ linkId, children }: { linkId?: string; children:
const { data: link, isLoading, isFetching } = useLinkQuery(linkId);
if (isFetching && isLoading) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
if (!link) {

View file

@ -70,7 +70,7 @@ export function PixelEditForm({
}, [data]);
if (pixelId && isLoading) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
return (

View file

@ -10,7 +10,7 @@ export function PixelProvider({ pixelId, children }: { pixelId?: string; childre
const { data: pixel, isLoading, isFetching } = usePixelQuery(pixelId);
if (isFetching && isLoading) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
if (!pixel) {

View file

@ -2,6 +2,10 @@ import { Metadata } from 'next';
import { SettingsLayout } from './SettingsLayout';
export default function ({ children }) {
if (process.env.cloudUrl) {
return null;
}
return <SettingsLayout>{children}</SettingsLayout>;
}

View file

@ -15,7 +15,7 @@ export function DateRangeSetting() {
return (
<Row gap="3">
<DateFilter value={value} onChange={handleChange} />
<DateFilter value={value} onChange={handleChange} placement="bottom start" />
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
</Row>
);

View file

@ -26,7 +26,7 @@ export function LanguageSetting() {
};
return (
<Row gap="3">
<Row gap>
<Select
value={locale}
onChange={val => saveLocale(val as string)}

View file

@ -22,7 +22,7 @@ export function TimezoneSetting() {
};
return (
<Row gap="3">
<Row gap>
<Select
value={timezone}
onChange={(value: any) => saveTimezone(value)}

View file

@ -1,12 +1,12 @@
import { Row, Column, Label } from '@umami/react-zen';
import { useLoginQuery, useMessages } from '@/components/hooks';
import { useConfig, useLoginQuery, useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants';
import { PasswordChangeButton } from './PasswordChangeButton';
export function ProfileSettings() {
const { user } = useLoginQuery();
const { formatMessage, labels } = useMessages();
const cloudMode = !!process.env.cloudMode;
const { cloudMode } = useConfig();
if (!user) {
return null;

View file

@ -10,7 +10,7 @@ export function TeamProvider({ teamId, children }: { teamId?: string; children:
const { data: team, isLoading, isFetching } = useTeamQuery(teamId);
if (isFetching && isLoading) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
if (!team) {

View file

@ -1,10 +1,10 @@
import { ReactNode } from 'react';
import Link from 'next/link';
import { DataGrid } from '@/components/common/DataGrid';
import { TeamsTable } from './TeamsTable';
import { useLoginQuery, useUserTeamsQuery } from '@/components/hooks';
import { ReactNode } from 'react';
export function TeamsDataTable({
allowEdit,
showActions,
}: {
allowEdit?: boolean;
@ -14,10 +14,18 @@ export function TeamsDataTable({
const { user } = useLoginQuery();
const query = useUserTeamsQuery(user.id);
const renderLink = (row: any) => {
return (
<Link key={row.id} href={`/teams/${row.id}`}>
{row.name}
</Link>
);
};
return (
<DataGrid query={query}>
{({ data }) => {
return <TeamsTable data={data} allowEdit={allowEdit} showActions={showActions} />;
return <TeamsTable data={data} showActions={showActions} renderLink={renderLink} />;
}}
</DataGrid>
);

View file

@ -1,14 +1,14 @@
import { Row } from '@umami/react-zen';
import { PageHeader } from '@/components/common/PageHeader';
import { ROLES } from '@/lib/constants';
import { useLoginQuery, useMessages } from '@/components/hooks';
import { useConfig, useLoginQuery, useMessages } from '@/components/hooks';
import { TeamsJoinButton } from './TeamsJoinButton';
import { TeamsAddButton } from './TeamsAddButton';
export function TeamsHeader({ allowCreate = true }: { allowCreate?: boolean }) {
const { formatMessage, labels } = useMessages();
const { user } = useLoginQuery();
const cloudMode = !!process.env.cloudMode;
const { cloudMode } = useConfig();
return (
<PageHeader title={formatMessage(labels.teams)}>

View file

@ -1,25 +1,25 @@
import { DataColumn, DataTable, Icon, MenuItem, Text, Row } from '@umami/react-zen';
import { useMessages, useNavigation } from '@/components/hooks';
import { useMessages } from '@/components/hooks';
import { Eye, Edit } from '@/components/icons';
import { ROLES } from '@/lib/constants';
import { MenuButton } from '@/components/input/MenuButton';
import Link from 'next/link';
import { ReactNode } from 'react';
export function TeamsTable({
data = [],
showActions = false,
renderLink,
}: {
data: any[];
allowEdit?: boolean;
showActions?: boolean;
renderLink?: (row: any) => ReactNode;
}) {
const { formatMessage, labels } = useMessages();
const { renderUrl } = useNavigation();
return (
<DataTable data={data}>
<DataColumn id="name" label={formatMessage(labels.name)}>
{(row: any) => <Link href={renderUrl(`/settings/teams/${row.id}`)}>{row.name}</Link>}
{renderLink}
</DataColumn>
<DataColumn id="owner" label={formatMessage(labels.owner)}>
{(row: any) => row?.members?.find(({ role }) => role === ROLES.teamOwner)?.user?.username}

View file

@ -16,7 +16,7 @@ export function WebsiteProvider({
const { data: website, isFetching, isLoading } = useWebsiteQuery(websiteId);
if (isFetching && isLoading) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
if (!website) {

View file

@ -1,6 +1,7 @@
import Link from 'next/link';
import { WebsitesTable } from './WebsitesTable';
import { DataGrid } from '@/components/common/DataGrid';
import { useUserWebsitesQuery } from '@/components/hooks';
import { useNavigation, useUserWebsitesQuery } from '@/components/hooks';
export function WebsitesDataTable({
teamId,
@ -14,16 +15,21 @@ export function WebsitesDataTable({
showActions?: boolean;
}) {
const queryResult = useUserWebsitesQuery({ teamId });
const { renderUrl } = useNavigation();
const renderLink = (row: any) => (
<Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>
);
return (
<DataGrid query={queryResult} allowSearch allowPaging>
{({ data }) => (
<WebsitesTable
teamId={teamId}
data={data}
showActions={showActions}
allowEdit={allowEdit}
allowView={allowView}
renderLink={renderLink}
/>
)}
</DataGrid>

View file

@ -3,27 +3,24 @@ import { Row, Text, Icon, DataTable, DataColumn, MenuItem } from '@umami/react-z
import { useMessages, useNavigation } from '@/components/hooks';
import { MenuButton } from '@/components/input/MenuButton';
import { Eye, SquarePen } from '@/components/icons';
import Link from 'next/link';
export interface WebsitesTableProps {
data: Record<string, any>[];
showActions?: boolean;
allowEdit?: boolean;
allowView?: boolean;
teamId?: string;
children?: ReactNode;
}
export function WebsitesTable({
data = [],
showActions,
allowEdit,
allowView,
renderLink,
children,
}: WebsitesTableProps) {
}: {
data: Record<string, any>[];
showActions?: boolean;
allowEdit?: boolean;
allowView?: boolean;
renderLink?: (row: any) => ReactNode;
children?: ReactNode;
}) {
const { formatMessage, labels } = useMessages();
const { renderUrl, pathname } = useNavigation();
const isSettings = pathname.includes('/settings');
const { renderUrl } = useNavigation();
if (!data?.length) {
return children;
@ -32,11 +29,7 @@ export function WebsitesTable({
return (
<DataTable data={data}>
<DataColumn id="name" label={formatMessage(labels.name)}>
{(row: any) => (
<Link href={renderUrl(`${isSettings ? '/settings' : ''}/websites/${row.id}`, false)}>
{row.name}
</Link>
)}
{renderLink}
</DataColumn>
<DataColumn id="domain" label={formatMessage(labels.domain)} />
{showActions && (

View file

@ -50,7 +50,7 @@ export function FunnelEditForm({
};
if (id && !data) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
const defaultValues = {

View file

@ -44,7 +44,7 @@ export function GoalEditForm({
};
if (id && !data) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
const defaultValues = {

View file

@ -52,7 +52,7 @@ export function CohortEditForm({
};
if (cohortId && !data) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
const defaultValues = {

View file

@ -49,7 +49,7 @@ export function SegmentEditForm({
};
if (segmentId && !data) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
return (

View file

@ -64,7 +64,7 @@ export function WebsiteTransferForm({
};
if (isLoading) {
return <Loading icon="dots" position="center" />;
return <Loading icon="dots" placement="center" />;
}
return (

View file

@ -1,23 +1,27 @@
'use server';
export type Config = {
cloudMode: boolean;
cloudUrl?: string;
faviconUrl?: string;
linksUrl?: string;
pixelsUrl?: string;
privateMode: boolean;
telemetryDisabled: boolean;
trackerScriptName?: string;
updatesDisabled: boolean;
linksUrl?: string;
pixelsUrl?: string;
};
export async function getConfig(): Promise<Config> {
return {
cloudMode: !!process.env.CLOUD_URL,
cloudUrl: process.env.CLOUD_URL,
faviconUrl: process.env.FAVICON_URL,
linksUrl: process.env.LINKS_URL,
pixelsUrl: process.env.PIXELS_URL,
privateMode: !!process.env.PRIVATE_MODE,
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
updatesDisabled: !!process.env.DISABLE_UPDATES,
linksUrl: process.env.LINKS_URL,
pixelsUrl: process.env.PIXELS_URL,
};
}

View file

@ -8,7 +8,7 @@ import { removeClientAuthToken } from '@/lib/client';
export function LogoutPage() {
const router = useRouter();
const { post } = useApi();
const disabled = process.env.cloudMode;
const disabled = process.env.cloudUrl;
useEffect(() => {
async function logout() {

View file

@ -1,7 +1,7 @@
import { Row, Icon, Text, ThemeButton } from '@umami/react-zen';
import Link from 'next/link';
import { LanguageButton } from '@/components/input/LanguageButton';
import { SettingsButton } from '@/components/input/SettingsButton';
import { PreferencesButton } from '@/components/input/PreferencesButton';
import { Logo } from '@/components/icons';
export function Header() {
@ -18,7 +18,7 @@ export function Header() {
<Row alignItems="center" gap>
<ThemeButton />
<LanguageButton />
<SettingsButton />
<PreferencesButton />
</Row>
</Row>
);

View file

@ -18,5 +18,5 @@ export function SSOPage() {
}
}, [router, url, token]);
return <Loading position="page" />;
return <Loading placement="absolute" />;
}

View file

@ -10,7 +10,7 @@ export interface LoadingPanelProps extends ColumnProps {
isLoading?: boolean;
isFetching?: boolean;
loadingIcon?: 'dots' | 'spinner';
loadingPosition?: 'center' | 'page' | 'inline';
loadingPlacement?: 'center' | 'absolute' | 'inline';
renderEmpty?: () => ReactNode;
children: ReactNode;
}
@ -22,7 +22,7 @@ export function LoadingPanel({
isLoading,
isFetching,
loadingIcon = 'dots',
loadingPosition = 'page',
loadingPlacement = 'absolute',
renderEmpty = () => <Empty />,
children,
...props
@ -34,7 +34,7 @@ export function LoadingPanel({
{/* Show loading spinner only if no data exists */}
{(isLoading || isFetching) && (
<Column position="relative" height="100%" {...props}>
<Loading icon={loadingIcon} position={loadingPosition} />
<Loading icon={loadingIcon} placement={loadingPlacement} />
</Column>
)}

View file

@ -24,7 +24,7 @@ export function PageBody({
}
if (isLoading) {
return <Loading position="page" />;
return <Loading placement="absolute" />;
}
return (

View file

@ -1,6 +1,7 @@
export {
AlertTriangle as Alert,
ArrowRight as Arrow,
Bookmark,
Calendar,
ChartPie,
ChevronRight as Chevron,
@ -50,4 +51,20 @@ export {
UserPlus,
X as Close,
} from 'lucide-react';
export * from '@/components/svg';
export {
Logo,
Bolt,
Change,
Compare,
Funnel,
Lightbulb,
Lightning,
Magnet,
Money,
Network,
Path,
Tag,
Target,
AddUser,
Visitor,
} from '@/components/svg';

View file

@ -1,12 +1,12 @@
import { useState, Key, Fragment } from 'react';
import { Modal, Select, ListItem, ListSeparator, Dialog, Row } from '@umami/react-zen';
import { Modal, Select, ListItem, ListSeparator, Dialog, SelectProps } from '@umami/react-zen';
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 {
export interface DateFilterProps extends SelectProps {
value?: string;
onChange?: (value: string) => void;
showAllTime?: boolean;
@ -20,6 +20,7 @@ export function DateFilter({
showAllTime,
renderDate,
placement = 'bottom',
...props
}: DateFilterProps) {
const { formatMessage, labels } = useMessages();
const [showPicker, setShowPicker] = useState(false);
@ -99,8 +100,9 @@ export function DateFilter({
};
return (
<Row minWidth="200px">
<>
<Select
{...props}
value={value}
placeholder={formatMessage(labels.selectDate)}
onChange={handleChange}
@ -130,6 +132,6 @@ export function DateFilter({
</Dialog>
</Modal>
)}
</Row>
</>
);
}

View file

@ -49,7 +49,7 @@ export function LookupField({ websiteId, type, value, onChange, ...props }: Look
allowsCustomValue
renderEmptyState={() =>
isLoading ? (
<Loading position="center" icon="dots" />
<Loading placement="center" icon="dots" />
) : (
<Empty message={formatMessage(messages.noResultsFound)} />
)

View file

@ -0,0 +1,27 @@
import { Button, Icon, DialogTrigger, Popover, Column, Label } from '@umami/react-zen';
import { TimezoneSetting } from '@/app/(main)/settings/preferences/TimezoneSetting';
import { DateRangeSetting } from '@/app/(main)/settings/preferences/DateRangeSetting';
import { Settings } from '@/components/icons';
import { useMessages } from '@/components/hooks';
export function PreferencesButton() {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button variant="quiet">
<Icon>
<Settings />
</Icon>
</Button>
<Popover placement="bottom end">
<Column gap="3">
<Label>{formatMessage(labels.timezone)}</Label>
<TimezoneSetting />
<Label>{formatMessage(labels.defaultDateRange)}</Label>
<DateRangeSetting />
</Column>
</Popover>
</DialogTrigger>
);
}

View file

@ -11,31 +11,31 @@ import {
Text,
Row,
} from '@umami/react-zen';
import { useMessages, useLoginQuery, useNavigation } from '@/components/hooks';
import { useMessages, useLoginQuery, useNavigation, useConfig } from '@/components/hooks';
import { LogOut, UserCircle, LockKeyhole } from '@/components/icons';
export function ProfileButton() {
const { formatMessage, labels } = useMessages();
const { user } = useLoginQuery();
const { renderUrl } = useNavigation();
const cloudMode = !!process.env.cloudMode;
const { cloudUrl } = useConfig();
const items = [
{
id: 'profile',
id: 'settings',
label: formatMessage(labels.profile),
path: renderUrl('/settings/profile'),
icon: <UserCircle />,
},
user.isAdmin &&
!cloudMode && {
!cloudUrl && {
id: 'admin',
label: formatMessage(labels.admin),
path: '/admin',
icon: <LockKeyhole />,
},
{
id: 'LogOut',
id: 'logout',
label: formatMessage(labels.logout),
path: '/logout',
icon: <LogOut />,

View file

@ -1,27 +1,71 @@
import { Button, Icon, DialogTrigger, Popover, Column, Label } from '@umami/react-zen';
import { TimezoneSetting } from '@/app/(main)/settings/preferences/TimezoneSetting';
import { DateRangeSetting } from '@/app/(main)/settings/preferences/DateRangeSetting';
import { Gear } from '@/components/icons';
import { useMessages } from '@/components/hooks';
import { Key } from 'react';
import {
Icon,
Button,
MenuTrigger,
Popover,
Menu,
MenuItem,
MenuSeparator,
MenuSection,
Dialog,
SubMenuTrigger,
} from '@umami/react-zen';
import { useMessages, useLoginQuery, useNavigation, useConfig } from '@/components/hooks';
import { LogOut, LockKeyhole, Settings, Knobs } from '@/components/icons';
import { PreferenceSettings } from '@/app/(main)/settings/preferences/PreferenceSettings';
export function SettingsButton() {
const { formatMessage, labels } = useMessages();
const { user } = useLoginQuery();
const { router, renderUrl } = useNavigation();
const { cloudMode, cloudUrl } = useConfig();
const handleAction = (id: Key) => {
if (id === 'settings') {
if (cloudMode) {
window.location.href = `${cloudUrl}/dashboard`;
return;
}
}
router.push(renderUrl(`/${id}`));
};
return (
<DialogTrigger>
<Button variant="quiet">
<MenuTrigger>
<Button data-test="button-profile" variant="quiet" autoFocus={false}>
<Icon>
<Gear />
<Settings />
</Icon>
</Button>
<Popover placement="bottom end">
<Column gap="3">
<Label>{formatMessage(labels.timezone)}</Label>
<TimezoneSetting />
<Label>{formatMessage(labels.defaultDateRange)}</Label>
<DateRangeSetting />
</Column>
<Menu autoFocus="last" onAction={handleAction}>
<MenuSection title={user.username}>
<MenuSeparator />
<MenuItem id="settings" icon={<Settings />} label={formatMessage(labels.settings)} />
{cloudMode && (
<SubMenuTrigger>
<MenuItem
icon={<Knobs />}
label={formatMessage(labels.preferences)}
showSubMenuIcon
/>
<Popover placement="right bottom">
<Dialog>
<PreferenceSettings />
</Dialog>
</Popover>
</SubMenuTrigger>
)}
{!cloudMode && user.isAdmin && (
<MenuItem id="admin" icon={<LockKeyhole />} label={formatMessage(labels.admin)} />
)}
<MenuSeparator />
<MenuItem id="logout" icon={<LogOut />} label={formatMessage(labels.logout)} />
</MenuSection>
</Menu>
</Popover>
</DialogTrigger>
</MenuTrigger>
);
}

View file

@ -1,5 +1,3 @@
import { Key } from 'react';
import { useRouter } from 'next/navigation';
import {
Text,
Icon,
@ -10,8 +8,8 @@ import {
MenuSeparator,
Popover,
Row,
Box,
Button,
Column,
Pressable,
Loading,
} from '@umami/react-zen';
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks';
@ -32,7 +30,7 @@ export function TeamsButton({ showText = true, onAction }: TeamsButtonProps) {
const label = teamId ? team?.name : user.username;
if (isLoading) {
return <Loading icon="dots" position="center" />;
return <Loading icon="dots" size="sm" placement="center" />;
}
if (!data?.count) {
@ -41,8 +39,15 @@ export function TeamsButton({ showText = true, onAction }: TeamsButtonProps) {
return (
<MenuTrigger>
<Button variant="outline">
<Row alignItems="center" justifyContent="space-between" flexGrow={1}>
<Pressable>
<Row
alignItems="center"
justifyContent="space-between"
flexGrow={1}
padding
backgroundColor="2"
style={{ cursor: 'pointer', textWrap: 'nowrap', outline: 'none' }}
>
<Row alignItems="center" gap>
<Icon>{teamId ? <Users /> : <User />}</Icon>
{showText && <Text>{label}</Text>}
@ -53,9 +58,9 @@ export function TeamsButton({ showText = true, onAction }: TeamsButtonProps) {
</Icon>
)}
</Row>
</Button>
</Pressable>
<Popover placement="bottom start">
<Box minWidth="300px">
<Column minWidth="300px">
<Menu
selectionMode="single"
selectedKeys={selectedKeys}
@ -86,7 +91,7 @@ export function TeamsButton({ showText = true, onAction }: TeamsButtonProps) {
))}
</MenuSection>
</Menu>
</Box>
</Column>
</Popover>
</MenuTrigger>
);

View file

@ -59,12 +59,14 @@ export function WebsiteDateFilter({
</Button>
</Row>
)}
<DateFilter
value={value}
onChange={handleChange}
showAllTime={showAllTime}
renderDate={+offset !== 0}
/>
<Row width="200px">
<DateFilter
value={value}
onChange={handleChange}
showAllTime={showAllTime}
renderDate={+offset !== 0}
/>
</Row>
{allowCompare && !isAllTime && (
<Row alignItems="center" gap>
<Text weight="bold">VS</Text>

View file

@ -3,7 +3,7 @@ import { PERMISSIONS } from '@/lib/constants';
import { getTeamUser } from '@/queries';
import { hasPermission } from '@/lib/auth';
const cloudMode = !!process.env.CLOUD_MODE;
const cloudMode = !!process.env.CLOUD_URL;
export async function canViewTeam({ user }: Auth, teamId: string) {
if (user.isAdmin) {

View file

@ -3,7 +3,7 @@ import { PERMISSIONS } from '@/lib/constants';
import { hasPermission } from '@/lib/auth';
import { getTeamUser, getWebsite } from '@/queries';
const cloudMode = !!process.env.CLOUD_MODE;
const cloudMode = !!process.env.CLOUD_URL;
export async function canViewWebsite({ user, shareToken }: Auth, websiteId: string) {
if (user?.isAdmin) {

View file

@ -117,7 +117,7 @@ export async function deleteTeam(
teamId: string,
): Promise<Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Team]>> {
const { client, transaction } = prisma;
const cloudMode = process.env.CLOUD_MODE;
const cloudMode = !!process.env.CLOUD_URL;
if (cloudMode) {
return transaction([

View file

@ -122,7 +122,7 @@ export async function deleteUser(
]
> {
const { client, transaction } = prisma;
const cloudMode = process.env.CLOUD_MODE;
const cloudMode = !!process.env.CLOUD_URL;
const websites = await client.website.findMany({
where: { userId },

View file

@ -159,7 +159,7 @@ export async function resetWebsite(
websiteId: string,
): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> {
const { client, transaction } = prisma;
const cloudMode = !!process.env.cloudMode;
const cloudMode = !!process.env.CLOUD_URL;
return transaction([
client.eventData.deleteMany({
@ -193,7 +193,7 @@ export async function deleteWebsite(
websiteId: string,
): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> {
const { client, transaction } = prisma;
const cloudMode = !!process.env.CLOUD_MODE;
const cloudMode = !!process.env.CLOUD_URL;
return transaction([
client.eventData.deleteMany({