Merge branch 'jajaja' of https://github.com/umami-software/umami into jajaja

This commit is contained in:
Mike Cao
2025-03-08 08:09:18 -08:00
163 changed files with 1659 additions and 1733 deletions

View File

@@ -3,6 +3,7 @@
"browser": true, "browser": true,
"es2020": true, "es2020": true,
"node": true, "node": true,
"jquery": true,
"jest": true "jest": true
}, },
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
@@ -14,6 +15,7 @@
"sourceType": "module" "sourceType": "module"
}, },
"extends": [ "extends": [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"eslint:recommended", "eslint:recommended",
"plugin:prettier/recommended", "plugin:prettier/recommended",
@@ -39,7 +41,8 @@
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }] "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }],
"@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }]
}, },
"globals": { "globals": {
"React": "writable" "React": "writable"

29
cypress/e2e/api.cy.ts Normal file
View File

@@ -0,0 +1,29 @@
describe('Website tests', () => {
Cypress.session.clearAllSavedSessions();
beforeEach(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
});
//let userId;
it('creates a user.', () => {
cy.fixture('users').then(data => {
const userPost = data.userPost;
cy.request({
method: 'POST',
url: '/api/users',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: userPost,
}).then(response => {
//userId = response.body.id;
expect(response.status).to.eq(200);
expect(response.body).to.have.property('username', 'cypress1');
expect(response.body).to.have.property('role', 'User');
});
});
});
});

View File

@@ -1,22 +1,36 @@
describe('Login tests', () => { describe('Login tests', () => {
beforeEach(() => {
cy.visit('/login');
});
it( it(
'logs user in with correct credentials and logs user out', 'logs user in with correct credentials and logs user out',
{ {
defaultCommandTimeout: 10000, defaultCommandTimeout: 10000,
}, },
() => { () => {
cy.visit('/login'); cy.getDataTest('input-username').find('input').as('inputUsername').click();
cy.getDataTest('input-username').find('input').click(); cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
cy.getDataTest('input-username').find('input').type(Cypress.env('umami_user'), { delay: 50 }); cy.get('@inputUsername').click();
cy.getDataTest('input-password').find('input').click();
cy.getDataTest('input-password') cy.getDataTest('input-password')
.find('input') .find('input')
.type(Cypress.env('umami_password'), { delay: 50 }); .type(Cypress.env('umami_password'), { delay: 0 });
cy.getDataTest('button-submit').click(); cy.getDataTest('button-submit').click();
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard'); cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
cy.getDataTest('button-profile').click(); cy.logout();
cy.getDataTest('item-logout').click();
cy.url().should('eq', Cypress.config().baseUrl + '/login');
}, },
); );
it('login with blank inputs or incorrect credentials', () => {
cy.getDataTest('button-submit').click();
cy.contains(/Required/i).should('be.visible');
cy.getDataTest('input-username').find('input').as('inputUsername');
cy.get('@inputUsername').click();
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
cy.get('@inputUsername').click();
cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 });
cy.getDataTest('button-submit').click();
cy.contains(/Incorrect username and\/or password./i).should('be.visible');
});
}); });

65
cypress/e2e/user.cy.ts Normal file
View File

@@ -0,0 +1,65 @@
describe('Website tests', () => {
Cypress.session.clearAllSavedSessions();
beforeEach(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
cy.visit('/settings/users');
});
it('Add a User', () => {
// add user
cy.contains(/Create user/i).should('be.visible');
cy.getDataTest('button-create-user').click();
cy.getDataTest('input-username').find('input').as('inputName').click();
cy.get('@inputName').type('Test-user', { delay: 0 });
cy.getDataTest('input-password').find('input').as('inputPassword').click();
cy.get('@inputPassword').type('testPasswordCypress', { delay: 0 });
cy.getDataTest('dropdown-role').click();
cy.getDataTest('dropdown-item-user').click();
cy.getDataTest('button-submit').click();
cy.get('td[label="Username"]').should('contain.text', 'Test-user');
cy.get('td[label="Role"]').should('contain.text', 'User');
});
it('Edit a User role and password', () => {
// edit user
cy.get('table tbody tr')
.contains('td', /Test-user/i)
.parent()
.within(() => {
cy.getDataTest('link-button-edit').click(); // Clicks the button inside the row
});
cy.getDataTest('input-password').find('input').as('inputPassword').click();
cy.get('@inputPassword').type('newPassword', { delay: 0 });
cy.getDataTest('dropdown-role').click();
cy.getDataTest('dropdown-item-viewOnly').click();
cy.getDataTest('button-submit').click();
cy.visit('/settings/users');
cy.get('table tbody tr')
.contains('td', /Test-user/i)
.parent()
.should('contain.text', 'View only');
cy.logout();
cy.url().should('eq', Cypress.config().baseUrl + '/login');
cy.getDataTest('input-username').find('input').as('inputUsername').click();
cy.get('@inputUsername').type('Test-user', { delay: 0 });
cy.get('@inputUsername').click();
cy.getDataTest('input-password').find('input').type('newPassword', { delay: 0 });
cy.getDataTest('button-submit').click();
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
});
it('Delete a website', () => {
// delete user
cy.get('table tbody tr')
.contains('td', /Test-user/i)
.parent()
.within(() => {
cy.getDataTest('button-delete').click(); // Clicks the button inside the row
});
cy.contains(/Are you sure you want to delete Test-user?/i).should('be.visible');
cy.getDataTest('button-confirm').click();
});
});

View File

@@ -10,10 +10,10 @@ describe('Website tests', () => {
cy.visit('/settings/websites'); cy.visit('/settings/websites');
cy.getDataTest('button-website-add').click(); cy.getDataTest('button-website-add').click();
cy.contains(/Add website/i).should('be.visible'); cy.contains(/Add website/i).should('be.visible');
cy.getDataTest('input-name').find('input').click(); cy.getDataTest('input-name').find('input').as('inputUsername').click();
cy.getDataTest('input-name').find('input').type('Add test', { delay: 50 }); cy.getDataTest('input-name').find('input').type('Add test', { delay: 0 });
cy.getDataTest('input-domain').find('input').click(); cy.getDataTest('input-domain').find('input').click();
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 50 }); cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 0 });
cy.getDataTest('button-submit').click(); cy.getDataTest('button-submit').click();
cy.get('td[label="Name"]').should('contain.text', 'Add test'); cy.get('td[label="Name"]').should('contain.text', 'Add test');
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com'); cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
@@ -41,10 +41,10 @@ describe('Website tests', () => {
cy.contains(/Details/i).should('be.visible'); cy.contains(/Details/i).should('be.visible');
cy.getDataTest('input-name').find('input').click(); cy.getDataTest('input-name').find('input').click();
cy.getDataTest('input-name').find('input').clear(); cy.getDataTest('input-name').find('input').clear();
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 50 }); cy.getDataTest('input-name').find('input').type('Updated website', { delay: 0 });
cy.getDataTest('input-domain').find('input').click(); cy.getDataTest('input-domain').find('input').click();
cy.getDataTest('input-domain').find('input').clear(); cy.getDataTest('input-domain').find('input').clear();
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 50 }); cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 0 });
cy.getDataTest('button-submit').click({ force: true }); cy.getDataTest('button-submit').click({ force: true });
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website'); cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com'); cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');

View File

@@ -0,0 +1,17 @@
{
"userGet": {
"name": "cypress",
"email": "password",
"role": "User"
},
"userPost": {
"username": "cypress1",
"password": "password",
"role": "User"
},
"userDelete": {
"name": "Charlie",
"email": "charlie@example.com",
"age": 35
}
}

View File

@@ -5,6 +5,12 @@ Cypress.Commands.add('getDataTest', (value: string) => {
return cy.get(`[data-test=${value}]`); return cy.get(`[data-test=${value}]`);
}); });
Cypress.Commands.add('logout', () => {
cy.getDataTest('button-profile').click();
cy.getDataTest('item-logout').click();
cy.url().should('eq', Cypress.config().baseUrl + '/login');
});
Cypress.Commands.add('login', (username: string, password: string) => { Cypress.Commands.add('login', (username: string, password: string) => {
cy.session([username, password], () => { cy.session([username, password], () => {
cy.request({ cy.request({

View File

@@ -1,4 +1,5 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
/* global JQuery */
declare namespace Cypress { declare namespace Cypress {
interface Chainable { interface Chainable {
@@ -7,6 +8,11 @@ declare namespace Cypress {
* @example cy.getDataTest('greeting') * @example cy.getDataTest('greeting')
*/ */
getDataTest(value: string): Chainable<JQuery<HTMLElement>>; getDataTest(value: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to logout through UI.
* @example cy.logout()
*/
logout(): Chainable<JQuery<HTMLElement>>;
/** /**
* Custom command to login user into the app. * Custom command to login user into the app.
* @example cy.login('admin', 'password) * @example cy.login('admin', 'password)

View File

@@ -200,24 +200,6 @@ const config = {
typescript: { typescript: {
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
experimental: {
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
},
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
issuer: /\.(js|ts)x?$/,
use: ['@svgr/webpack'],
});
return config;
},
async headers() { async headers() {
return headers; return headers;
}, },

View File

@@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "2.16.1", "version": "2.17.0",
"description": "A simple, fast, privacy-focused alternative to Google Analytics.", "description": "A simple, fast, privacy-focused alternative to Google Analytics.",
"author": "Umami Software, Inc. <hello@umami.is>", "author": "Umami Software, Inc. <hello@umami.is>",
"license": "MIT", "license": "MIT",
@@ -10,7 +10,7 @@
"url": "https://github.com/umami-software/umami.git" "url": "https://github.com/umami-software/umami.git"
}, },
"scripts": { "scripts": {
"dev": "next dev -p 3000 --turbo", "dev": "next dev -p 3000",
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
"start": "next start", "start": "next start",
"build-docker": "npm-run-all build-db build-tracker build-geo build-app", "build-docker": "npm-run-all build-db build-tracker build-geo build-app",
@@ -71,12 +71,13 @@
"@dicebear/core": "^9.2.2", "@dicebear/core": "^9.2.2",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
"@hello-pangea/dnd": "^17.0.0", "@hello-pangea/dnd": "^17.0.0",
"@prisma/client": "6.1.0", "@internationalized/date": "^3.7.0",
"@prisma/client": "6.4.1",
"@prisma/extension-read-replicas": "^0.4.0", "@prisma/extension-read-replicas": "^0.4.0",
"@react-spring/web": "^9.7.5", "@react-spring/web": "^9.7.5",
"@tanstack/react-query": "^5.66.11", "@tanstack/react-query": "^5.66.11",
"@umami/prisma-client": "^0.14.0", "@umami/prisma-client": "^0.14.0",
"@umami/react-zen": "^0.62.0", "@umami/react-zen": "link:C:/Users/mike/AppData/Local/pnpm/global/5/node_modules/@umami/react-zen",
"@umami/redis-client": "^0.26.0", "@umami/redis-client": "^0.26.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"chalk": "^4.1.2", "chalk": "^4.1.2",
@@ -105,10 +106,10 @@
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"maxmind": "^4.3.24", "maxmind": "^4.3.24",
"md5": "^2.3.0", "md5": "^2.3.0",
"next": "15.2.0", "next": "15.2.1",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prisma": "6.1.0", "prisma": "6.4.1",
"pure-rand": "^6.1.0", "pure-rand": "^6.1.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-aria-components": "^1.6.0", "react-aria-components": "^1.6.0",

564
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ import { UpdateNotice } from './UpdateNotice';
import { NavBar } from '@/app/(main)/NavBar'; import { NavBar } from '@/app/(main)/NavBar';
import { Page } from '@/components/layout/Page'; import { Page } from '@/components/layout/Page';
import { useLogin, useConfig } from '@/components/hooks'; import { useLogin, useConfig } from '@/components/hooks';
import { SideNav } from '@/app/(main)/SideNav'; import { Nav } from '@/app/(main)/Nav';
export function App({ children }) { export function App({ children }) {
const { user, isLoading, error } = useLogin(); const { user, isLoading, error } = useLogin();
@@ -30,8 +30,8 @@ export function App({ children }) {
} }
return ( return (
<Grid> <Grid height="100vh" width="100%" columns="auto 1fr">
<SideNav /> <Nav />
<Grid rows="auto 1fr"> <Grid rows="auto 1fr">
<NavBar /> <NavBar />
<Page> <Page>

View File

@@ -1,11 +1,21 @@
import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { SideNav as Nav, SideNavHeader, SideNavSection, SideNavItem } from '@umami/react-zen'; import {
SideNav,
SideNavHeader,
SideNavSection,
SideNavItem,
Button,
Icon,
Row,
} from '@umami/react-zen';
import { Lucide, Icons } from '@/components/icons'; import { Lucide, Icons } from '@/components/icons';
import { useMessages, useTeamUrl } from '@/components/hooks'; import { useMessages, useTeamUrl } from '@/components/hooks';
export function SideNav() { export function Nav() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { renderTeamUrl } = useTeamUrl(); const { renderTeamUrl } = useTeamUrl();
const [isCollapsed, setCollapsed] = useState(false);
const links = [ const links = [
{ {
@@ -36,7 +46,7 @@ export function SideNav() {
].filter(n => n); ].filter(n => n);
return ( return (
<Nav> <SideNav isCollapsed={isCollapsed}>
<SideNavSection> <SideNavSection>
<SideNavHeader name="umami" icon={<Icons.Logo />} /> <SideNavHeader name="umami" icon={<Icons.Logo />} />
</SideNavSection> </SideNavSection>
@@ -49,6 +59,15 @@ export function SideNav() {
); );
})} })}
</SideNavSection> </SideNavSection>
</Nav> <SideNavSection>
<Row justifyContent="flex-start">
<Button onPress={() => setCollapsed(!isCollapsed)} variant="quiet">
<Icon>
<Lucide.PanelLeft />
</Icon>
</Button>
</Row>
</SideNavSection>
</SideNav>
); );
} }

View File

@@ -1,60 +0,0 @@
.navbar {
display: grid;
grid-template-columns: max-content 1fr 1fr;
position: relative;
align-items: center;
height: 60px;
background: var(--base75);
border-bottom: 1px solid var(--base300);
padding: 0 20px;
z-index: 200;
}
.links {
display: flex;
flex-direction: row;
gap: 30px;
padding: 0 40px;
font-weight: 700;
max-height: 60px;
align-items: center;
}
.links a,
.links a:active,
.links a:visited {
color: var(--font-color200);
line-height: 60px;
border-bottom: 2px solid transparent;
}
.links a:hover {
color: var(--font-color100);
border-bottom: 2px solid var(--primary400);
}
.links a.selected {
color: var(--font-color100);
border-bottom: 2px solid var(--primary400);
}
.mobile {
flex-direction: row;
align-items: center;
justify-content: flex-end;
display: none;
}
@media only screen and (max-width: 768px) {
.navbar {
grid-template-columns: repeat(2, 1fr);
}
.links {
display: none;
}
.mobile {
display: flex;
}
}

View File

@@ -1,46 +0,0 @@
.sidenav {
position: fixed;
top: 0;
width: 200px;
height: 100vh;
background-color: var(--layer-color-1);
border-right: 1px solid var(--border-color);
}
.header {
display: flex;
align-items: center;
gap: var(--gap);
padding: var(--padding);
}
.name {
font-weight: var(--font-weight-bold);
}
.section {
padding: var(--spacing-3);
gap: var(--spacing-9);
}
.title {
font-weight: var(--font-weight-bold);
}
.items {
display: grid;
gap: var(--gap);
}
.item {
color: var(--font-color-muted) !important;
display: flex;
align-items: center;
gap: var(--gap);
padding: var(--padding);
}
.item:hover {
color: var(--font-color) !important;
background-color: var(--layer-color-2);
}

View File

@@ -1,4 +1,4 @@
import { Button } from 'react-basics'; import { Button } from '@umami/react-zen';
import Link from 'next/link'; import Link from 'next/link';
import Script from 'next/script'; import Script from 'next/script';
import { WebsiteSelect } from '@/components/input/WebsiteSelect'; import { WebsiteSelect } from '@/components/input/WebsiteSelect';

View File

@@ -1,7 +1,7 @@
import { useState, useMemo, useEffect } from 'react'; import { useState, useMemo, useEffect } from 'react';
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import classNames from 'classnames'; import classNames from 'classnames';
import { Button, Loading, Toggle, SearchField } from 'react-basics'; import { Button, Loading, Toggle, SearchField } from '@umami/react-zen';
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
import { useDashboard, saveDashboard } from '@/store/dashboard'; import { useDashboard, saveDashboard } from '@/store/dashboard';
import { useMessages, useWebsites } from '@/components/hooks'; import { useMessages, useWebsites } from '@/components/hooks';

View File

@@ -1,5 +1,5 @@
'use client'; 'use client';
import { Icon, Icons, Loading, Text } from 'react-basics'; import { Icon, Icons, Loading, Text } from '@umami/react-zen';
import { PageHeader } from '@/components/layout/PageHeader'; import { PageHeader } from '@/components/layout/PageHeader';
import { Pager } from '@/components/common/Pager'; import { Pager } from '@/components/common/Pager';
import { WebsiteChartList } from '../websites/[websiteId]/WebsiteChartList'; import { WebsiteChartList } from '../websites/[websiteId]/WebsiteChartList';

View File

@@ -1,4 +1,4 @@
import { TooltipPopup, Icon, Text, Flexbox, Button } from 'react-basics'; import { Row, TooltipTrigger, Tooltip, Icon, Text, Button } from '@umami/react-zen';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { saveDashboard } from '@/store/dashboard'; import { saveDashboard } from '@/store/dashboard';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
@@ -15,20 +15,21 @@ export function DashboardSettingsButton() {
}; };
return ( return (
<Flexbox gap={10}> <Row gap="3">
<TooltipPopup label={formatMessage(labels.toggleCharts)} position="bottom"> <TooltipTrigger>
<Button onClick={handleToggleCharts}> <Button onPress={handleToggleCharts}>
<Icon> <Icon>
<Icons.BarChart /> <Icons.BarChart />
</Icon> </Icon>
</Button> </Button>
</TooltipPopup> <Tooltip placement="bottom">{formatMessage(labels.toggleCharts)}</Tooltip>
<Button onClick={handleEdit}> </TooltipTrigger>
<Button onPress={handleEdit}>
<Icon> <Icon>
<Icons.Edit /> <Icons.Edit />
</Icon> </Icon>
<Text>{formatMessage(labels.edit)}</Text> <Text>{formatMessage(labels.edit)}</Text>
</Button> </Button>
</Flexbox> </Row>
); );
} }

View File

@@ -1,5 +1,5 @@
import { DateFilter } from '@/components/input/DateFilter'; import { DateFilter } from '@/components/input/DateFilter';
import { Button, Flexbox } from 'react-basics'; import { Button, Flexbox } from '@umami/react-zen';
import { useDateRange, useMessages } from '@/components/hooks'; import { useDateRange, useMessages } from '@/components/hooks';
import { DEFAULT_DATE_RANGE } from '@/lib/constants'; import { DEFAULT_DATE_RANGE } from '@/lib/constants';
import { DateRange } from '@/lib/types'; import { DateRange } from '@/lib/types';

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, Dropdown, Item, Flexbox } from 'react-basics'; import { Button, Select, ListItem, Flexbox } from '@umami/react-zen';
import { useLocale, useMessages } from '@/components/hooks'; import { useLocale, useMessages } from '@/components/hooks';
import { DEFAULT_LOCALE } from '@/lib/constants'; import { DEFAULT_LOCALE } from '@/lib/constants';
import { languages } from '@/lib/lang'; import { languages } from '@/lib/lang';
@@ -20,22 +20,19 @@ export function LanguageSetting() {
const handleReset = () => saveLocale(DEFAULT_LOCALE); const handleReset = () => saveLocale(DEFAULT_LOCALE);
const renderValue = (value: string | number) => languages?.[value]?.label;
return ( return (
<Flexbox gap={10}> <Flexbox gap={10}>
<Dropdown <Select
items={options} items={options}
value={locale} value={locale}
renderValue={renderValue}
onChange={val => saveLocale(val as string)} onChange={val => saveLocale(val as string)}
allowSearch={true} allowSearch={true}
onSearch={setSearch} onSearch={setSearch}
menuProps={{ className: styles.menu }} menuProps={{ className: styles.menu }}
> >
{item => <Item key={item}>{languages[item].label}</Item>} {item => <ListItem key={item}>{languages[item].label}</ListItem>}
</Dropdown> </Select>
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button> <Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
</Flexbox> </Flexbox>
); );
} }

View File

@@ -1,29 +1,29 @@
import { Button, Icon, Text, useToasts, ModalTrigger, Modal } from 'react-basics'; import { Button, Icon, Text, useToast, DialogTrigger, Dialog, Modal } from '@umami/react-zen';
import { PasswordEditForm } from '@/app/(main)/profile/PasswordEditForm'; import { PasswordEditForm } from '@/app/(main)/profile/PasswordEditForm';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
export function PasswordChangeButton() { export function PasswordChangeButton() {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { showToast } = useToasts(); const { toast } = useToast();
const handleSave = () => { const handleSave = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); toast(formatMessage(messages.saved));
}; };
return ( return (
<> <DialogTrigger>
<ModalTrigger> <Button>
<Button> <Icon>
<Icon> <Icons.Lock />
<Icons.Lock /> </Icon>
</Icon> <Text>{formatMessage(labels.changePassword)}</Text>
<Text>{formatMessage(labels.changePassword)}</Text> </Button>
</Button> <Modal>
<Modal title={formatMessage(labels.changePassword)}> <Dialog title={formatMessage(labels.changePassword)}>
{close => <PasswordEditForm onSave={handleSave} onClose={close} />} {({ close }) => <PasswordEditForm onSave={handleSave} onClose={close} />}
</Modal> </Dialog>
</ModalTrigger> </Modal>
</> </DialogTrigger>
); );
} }

View File

@@ -1,5 +1,5 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from 'react-basics'; import { Form, FormRow, FormInput, FormButtons, PasswordField, Button } from '@umami/react-zen';
import { useApi, useMessages } from '@/components/hooks'; import { useApi, useMessages } from '@/components/hooks';
export function PasswordEditForm({ onSave, onClose }) { export function PasswordEditForm({ onSave, onClose }) {

View File

@@ -1,4 +1,4 @@
import { Form, FormRow } from 'react-basics'; import { Form, FormRow } from '@umami/react-zen';
import { TimezoneSetting } from '@/app/(main)/profile/TimezoneSetting'; import { TimezoneSetting } from '@/app/(main)/profile/TimezoneSetting';
import { DateRangeSetting } from '@/app/(main)/profile/DateRangeSetting'; import { DateRangeSetting } from '@/app/(main)/profile/DateRangeSetting';
import { LanguageSetting } from '@/app/(main)/profile/LanguageSetting'; import { LanguageSetting } from '@/app/(main)/profile/LanguageSetting';

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { Button, Icon } from 'react-basics'; import { Button, Icon } from '@umami/react-zen';
import { useTheme } from '@/components/hooks'; import { useTheme } from '@/components/hooks';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import styles from './ThemeSetting.module.css'; import styles from './ThemeSetting.module.css';

View File

@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Dropdown, Item, Button, Flexbox } from 'react-basics'; import { Row, Select, ListItem, Button } from '@umami/react-zen';
import { useTimezone, useMessages } from '@/components/hooks'; import { useTimezone, useMessages } from '@/components/hooks';
import { getTimezone } from '@/lib/date'; import { getTimezone } from '@/lib/date';
import styles from './TimezoneSetting.module.css'; import styles from './TimezoneSetting.module.css';
@@ -17,19 +17,22 @@ export function TimezoneSetting() {
const handleReset = () => saveTimezone(getTimezone()); const handleReset = () => saveTimezone(getTimezone());
return ( return (
<Flexbox gap={10}> <Row gap="3">
<Dropdown <Select
className={styles.dropdown} className={styles.dropdown}
items={options} items={options}
value={timezone} value={timezone}
onChange={(value: any) => saveTimezone(value)} onChange={(value: any) => saveTimezone(value)}
menuProps={{ className: styles.menu }}
allowSearch={true} allowSearch={true}
onSearch={setSearch} onSearch={setSearch}
> >
{item => <Item key={item}>{item}</Item>} {item => (
</Dropdown> <ListItem key={item} id={item}>
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button> {item}
</Flexbox> </ListItem>
)}
</Select>
<Button onPress={handleReset}>{formatMessage(labels.reset)}</Button>
</Row>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; import { Button, Icon, Icons, Modal, DialogTrigger, Dialog, Text } from '@umami/react-zen';
import { useApi, useMessages, useModified } from '@/components/hooks'; import { useApi, useMessages, useModified } from '@/components/hooks';
import { ConfirmationForm } from '@/components/common/ConfirmationForm'; import { ConfirmationForm } from '@/components/common/ConfirmationForm';
@@ -29,25 +29,29 @@ export function ReportDeleteButton({
}; };
return ( return (
<ModalTrigger> <DialogTrigger>
<Button> <Button>
<Icon> <Icon>
<Icons.Trash /> <Icons.Trash />
</Icon> </Icon>
<Text>{formatMessage(labels.delete)}</Text> <Text>{formatMessage(labels.delete)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.deleteReport)}> <Modal>
{(close: () => void) => ( <Dialog title={formatMessage(labels.deleteReport)}>
<ConfirmationForm {({ close }) => (
message={formatMessage(messages.confirmDelete, { target: <b>{reportName}</b> })} <ConfirmationForm
isLoading={isPending} message={formatMessage(messages.confirmDelete, {
error={error} target: <b key={messages.confirmDelete.id}>{reportName}</b>,
onConfirm={handleConfirm.bind(null, close)} })}
onClose={close} isLoading={isPending}
buttonLabel={formatMessage(labels.delete)} error={error}
/> onConfirm={handleConfirm.bind(null, close)}
)} onClose={close}
buttonLabel={formatMessage(labels.delete)}
/>
)}
</Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
); );
} }

View File

@@ -1,7 +1,7 @@
import { useReports } from '@/components/hooks';
import { ReportsTable } from './ReportsTable';
import { DataGrid } from '@/components/common/DataGrid';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useReports } from '@/components/hooks';
import { DataGrid } from '@/components/common/DataGrid';
import { ReportsTable } from './ReportsTable';
export function ReportsDataTable({ export function ReportsDataTable({
websiteId, websiteId,

View File

@@ -1,5 +1,5 @@
import { PageHeader } from '@/components/layout/PageHeader'; import { PageHeader } from '@/components/layout/PageHeader';
import { Icon, Icons, Text } from 'react-basics'; import { Icon, Icons, Text } from '@umami/react-zen';
import { useLogin, useMessages, useTeamUrl } from '@/components/hooks'; import { useLogin, useMessages, useTeamUrl } from '@/components/hooks';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';

View File

@@ -1,4 +1,4 @@
import { GridColumn, GridTable, Icon, Icons, Text } from 'react-basics'; import { Icon, Icons, Text, DataTable, DataColumn, Row } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
import { useMessages, useLogin, useTeamUrl } from '@/components/hooks'; import { useMessages, useLogin, useTeamUrl } from '@/components/hooks';
import { REPORT_TYPES } from '@/lib/constants'; import { REPORT_TYPES } from '@/lib/constants';
@@ -10,39 +10,39 @@ export function ReportsTable({ data = [], showDomain }: { data: any[]; showDomai
const { renderTeamUrl } = useTeamUrl(); const { renderTeamUrl } = useTeamUrl();
return ( return (
<GridTable data={data}> <DataTable data={data}>
<GridColumn name="name" label={formatMessage(labels.name)} /> <DataColumn id="name" label={formatMessage(labels.name)} />
<GridColumn name="description" label={formatMessage(labels.description)} /> <DataColumn id="description" label={formatMessage(labels.description)} />
<GridColumn name="type" label={formatMessage(labels.type)}> <DataColumn id="type" label={formatMessage(labels.type)}>
{row => { {(row: any) => {
return formatMessage( return formatMessage(
labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)], labels[Object.keys(REPORT_TYPES).find(key => REPORT_TYPES[key] === row.type)],
); );
}} }}
</GridColumn> </DataColumn>
{showDomain && ( {showDomain && (
<GridColumn name="domain" label={formatMessage(labels.domain)}> <DataColumn id="domain" label={formatMessage(labels.domain)}>
{row => row?.website?.domain} {(row: any) => row?.website?.domain}
</GridColumn> </DataColumn>
)} )}
<GridColumn name="action" label="" alignment="end"> <DataColumn id="action" label="" align="end">
{row => { {(row: any) => {
const { id, name, userId, website } = row; const { id, name, userId, website } = row;
return ( return (
<> <Row gap="3">
{(user.id === userId || user.id === website?.userId) && ( {(user.id === userId || user.id === website?.userId) && (
<ReportDeleteButton reportId={id} reportName={name} /> <ReportDeleteButton reportId={id} reportName={name} />
)} )}
<LinkButton href={renderTeamUrl(`/reports/${id}`)}> <LinkButton href={renderTeamUrl(`/reports/${id}`)}>
<Icon> <Icon>
<Icons.ArrowRight /> <Icons.Arrow />
</Icon> </Icon>
<Text>{formatMessage(labels.view)}</Text> <Text>{formatMessage(labels.view)}</Text>
</LinkButton> </LinkButton>
</> </Row>
); );
}} }}
</GridColumn> </DataColumn>
</GridTable> </DataTable>
); );
} }

View File

@@ -1,5 +1,5 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { FormRow } from 'react-basics'; import { Column, Label } from '@umami/react-zen';
import { parseDateRange } from '@/lib/date'; import { parseDateRange } from '@/lib/date';
import { DateFilter } from '@/components/input/DateFilter'; import { DateFilter } from '@/components/input/DateFilter';
import { WebsiteSelect } from '@/components/input/WebsiteSelect'; import { WebsiteSelect } from '@/components/input/WebsiteSelect';
@@ -40,16 +40,18 @@ export function BaseParameters({
return ( return (
<> <>
{showWebsiteSelect && ( {showWebsiteSelect && (
<FormRow label={formatMessage(labels.website)}> <Column>
<Label>{formatMessage(labels.website)}</Label>
{allowWebsiteSelect ? ( {allowWebsiteSelect ? (
<WebsiteSelect teamId={teamId} websiteId={websiteId} onSelect={handleWebsiteSelect} /> <WebsiteSelect teamId={teamId} websiteId={websiteId} onSelect={handleWebsiteSelect} />
) : ( ) : (
name name
)} )}
</FormRow> </Column>
)} )}
{showDateSelect && ( {showDateSelect && (
<FormRow label={formatMessage(labels.dateRange)} className={styles.dropdown}> <Column className={styles.dropdown}>
<Label>{formatMessage(labels.dateRange)}</Label>
{allowDateSelect && ( {allowDateSelect && (
<DateFilter <DateFilter
value={value} value={value}
@@ -58,7 +60,7 @@ export function BaseParameters({
onChange={handleDateChange} onChange={handleDateChange}
/> />
)} )}
</FormRow> </Column>
)} )}
</> </>
); );

View File

@@ -1,4 +1,4 @@
import { Form, FormRow, Menu, Item } from 'react-basics'; import { Form, FormRow, Menu, Item } from '@umami/react-zen';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
export function FieldAggregateForm({ export function FieldAggregateForm({

View File

@@ -4,19 +4,21 @@ import { OPERATORS } from '@/lib/constants';
import { isEqualsOperator } from '@/lib/params'; import { isEqualsOperator } from '@/lib/params';
import { import {
Button, Button,
Dropdown, Column,
Row,
Select,
Flexbox, Flexbox,
Form,
FormRow,
Icon, Icon,
Icons, Icons,
Item,
Loading, Loading,
Menu, Menu,
MenuItem,
ListItem,
SearchField, SearchField,
Text, Text,
TextField, TextField,
} from 'react-basics'; Label,
} from '@umami/react-zen';
import styles from './FieldFilterEditForm.module.css'; import styles from './FieldFilterEditForm.module.css';
export interface FieldFilterFormProps { export interface FieldFilterFormProps {
@@ -94,10 +96,6 @@ export function FieldFilterEditForm({
: values; : values;
}, [value, formattedValues]); }, [value, formattedValues]);
const renderFilterValue = (value: any) => {
return filters.find((filter: { value: any }) => filter.value === value)?.label;
};
const handleAdd = () => { const handleAdd = () => {
onChange({ name, type, operator, value: isEquals ? selected : value }); onChange({ name, type, operator, value: isEquals ? selected : value });
}; };
@@ -133,21 +131,21 @@ export function FieldFilterEditForm({
}; };
return ( return (
<Form> <Column>
<FormRow label={label} className={styles.filter}> <Row className={styles.filter}>
<Label>{label}</Label>
<Flexbox gap={10}> <Flexbox gap={10}>
{allowFilterSelect && ( {allowFilterSelect && (
<Dropdown <Select
className={styles.dropdown} className={styles.dropdown}
items={filterDropdownItems(name)} items={filterDropdownItems(name)}
value={operator} value={operator}
renderValue={renderFilterValue}
onChange={handleOperatorChange} onChange={handleOperatorChange}
> >
{({ value, label }) => { {({ value, label }: any) => {
return <Item key={value}>{label}</Item>; return <ListItem key={value}>{label}</ListItem>;
}} }}
</Dropdown> </Select>
)} )}
{selected && isEquals && ( {selected && isEquals && (
<div className={styles.selected} onClick={handleReset}> <div className={styles.selected} onClick={handleReset}>
@@ -163,7 +161,6 @@ export function FieldFilterEditForm({
className={styles.text} className={styles.text}
value={value} value={value}
placeholder={formatMessage(labels.enter)} placeholder={formatMessage(labels.enter)}
onChange={e => setValue(e.target.value)}
onSearch={handleSearch} onSearch={handleSearch}
delay={500} delay={500}
onFocus={() => setShowMenu(true)} onFocus={() => setShowMenu(true)}
@@ -187,11 +184,11 @@ export function FieldFilterEditForm({
/> />
)} )}
</Flexbox> </Flexbox>
<Button variant="primary" onClick={handleAdd} disabled={isDisabled}> <Button variant="primary" onPress={handleAdd} isDisabled={isDisabled}>
{formatMessage(isNew ? labels.add : labels.update)} {formatMessage(isNew ? labels.add : labels.update)}
</Button> </Button>
</FormRow> </Row>
</Form> </Column>
); );
} }
@@ -199,10 +196,10 @@ const ResultsMenu = ({ values, type, isLoading, onSelect }) => {
const { formatValue } = useFormat(); const { formatValue } = useFormat();
if (isLoading) { if (isLoading) {
return ( return (
<Menu className={styles.menu} variant="popup"> <Menu className={styles.menu}>
<Item> <MenuItem>
<Loading icon="dots" position="center" /> <Loading icon="dots" position="center" />
</Item> </MenuItem>
</Menu> </Menu>
); );
} }
@@ -212,9 +209,9 @@ const ResultsMenu = ({ values, type, isLoading, onSelect }) => {
} }
return ( return (
<Menu className={styles.menu} variant="popup" onSelect={onSelect}> <Menu className={styles.menu} onSelectionChange={onSelect}>
{values?.map(({ value }) => { {values?.map(({ value }) => {
return <Item key={value}>{formatValue(value, type)}</Item>; return <MenuItem key={value}>{formatValue(value, type)}</MenuItem>;
})} })}
</Menu> </Menu>
); );

View File

@@ -1,7 +1,7 @@
import { useFields, useMessages } from '@/components/hooks'; import { useFields, useMessages } from '@/components/hooks';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { useContext } from 'react'; import { useContext } from 'react';
import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics'; import { Button, Row, Label, Icon, Popover, MenuTrigger } from '@umami/react-zen';
import { FieldSelectForm } from '../[reportId]/FieldSelectForm'; import { FieldSelectForm } from '../[reportId]/FieldSelectForm';
import { ParameterList } from '../[reportId]/ParameterList'; import { ParameterList } from '../[reportId]/ParameterList';
import { PopupForm } from '../[reportId]/PopupForm'; import { PopupForm } from '../[reportId]/PopupForm';
@@ -26,27 +26,26 @@ export function FieldParameters() {
const AddButton = () => { const AddButton = () => {
return ( return (
<PopupTrigger> <MenuTrigger>
<Button size="sm"> <Button size="sm">
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>
</Button> </Button>
<Popup position="bottom" alignment="start"> <Popover placement="start">
<PopupForm> <FieldSelectForm
<FieldSelectForm fields={fieldOptions.filter(({ name }) => !fields.find(f => f.name === name))}
fields={fieldOptions.filter(({ name }) => !fields.find(f => f.name === name))} onSelect={handleAdd}
onSelect={handleAdd} showType={false}
showType={false} />
/> </Popover>
</PopupForm> </MenuTrigger>
</Popup>
</PopupTrigger>
); );
}; };
return ( return (
<FormRow label={formatMessage(labels.fields)} action={<AddButton />}> <Row>
<Label>{formatMessage(labels.fields)}</Label>
<ParameterList> <ParameterList>
{fields.map(({ name }) => { {fields.map(({ name }) => {
return ( return (
@@ -56,6 +55,7 @@ export function FieldParameters() {
); );
})} })}
</ParameterList> </ParameterList>
</FormRow> <AddButton />
</Row>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Menu, Item, Form, FormRow } from 'react-basics'; import { List, ListItem, Label, Column } from '@umami/react-zen';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import styles from './FieldSelectForm.module.css'; import styles from './FieldSelectForm.module.css';
import { Key } from 'react'; import { Key } from 'react';
@@ -13,19 +13,18 @@ export function FieldSelectForm({ fields = [], onSelect, showType = true }: Fiel
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
<Form> <Column>
<FormRow label={formatMessage(labels.fields)}> <Label>{formatMessage(labels.fields)}</Label>
<Menu className={styles.menu} onSelect={key => onSelect(fields[key as any])}> <List onSelectionChange={key => onSelect(fields[key as any])}>
{fields.map(({ name, label, type }: any, index: Key) => { {fields.map(({ name, label, type }: any, index: Key) => {
return ( return (
<Item key={index} className={styles.item}> <ListItem key={index} className={styles.item}>
<div>{label || name}</div> <div>{label || name}</div>
{showType && type && <div className={styles.type}>{type}</div>} {showType && type && <div className={styles.type}>{type}</div>}
</Item> </ListItem>
); );
})} })}
</Menu> </List>
</FormRow> </Column>
</Form>
); );
} }

View File

@@ -1,7 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useMessages, useFormat, useFilters, useFields } from '@/components/hooks'; import { useMessages, useFormat, useFilters, useFields } from '@/components/hooks';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics'; import { Button, Text, Row, Label, Icon, Popover, MenuTrigger } from '@umami/react-zen';
import { FilterSelectForm } from '../[reportId]/FilterSelectForm'; import { FilterSelectForm } from '../[reportId]/FilterSelectForm';
import { ParameterList } from '../[reportId]/ParameterList'; import { ParameterList } from '../[reportId]/ParameterList';
import { PopupForm } from '../[reportId]/PopupForm'; import { PopupForm } from '../[reportId]/PopupForm';
@@ -44,13 +44,13 @@ export function FilterParameters() {
const AddButton = () => { const AddButton = () => {
return ( return (
<PopupTrigger> <MenuTrigger>
<Button size="sm"> <Button size="sm">
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>
</Button> </Button>
<Popup position="bottom" alignment="start"> <Popover placement="bottom start">
<PopupForm> <PopupForm>
<FilterSelectForm <FilterSelectForm
websiteId={websiteId} websiteId={websiteId}
@@ -60,13 +60,17 @@ export function FilterParameters() {
onChange={handleAdd} onChange={handleAdd}
/> />
</PopupForm> </PopupForm>
</Popup> </Popover>
</PopupTrigger> </MenuTrigger>
); );
}; };
return ( return (
<FormRow label={formatMessage(labels.filters)} action={<AddButton />}> <>
<Row justifyContent="space-between">
<Label>{formatMessage(labels.filters)}</Label>
<AddButton />
</Row>
<ParameterList> <ParameterList>
{filters.map( {filters.map(
({ name, operator, value }: { name: string; operator: string; value: string }) => { ({ name, operator, value }: { name: string; operator: string; value: string }) => {
@@ -90,7 +94,7 @@ export function FilterParameters() {
}, },
)} )}
</ParameterList> </ParameterList>
</FormRow> </>
); );
} }
@@ -108,29 +112,27 @@ const FilterParameter = ({
const { operatorLabels } = useFilters(); const { operatorLabels } = useFilters();
return ( return (
<PopupTrigger> <MenuTrigger>
<div className={styles.item}> <div className={styles.item}>
<div className={styles.label}>{label}</div> <div className={styles.label}>{label}</div>
<div className={styles.op}>{operatorLabels[operator]}</div> <div className={styles.op}>{operatorLabels[operator]}</div>
<div className={styles.value}>{value}</div> <div className={styles.value}>{value}</div>
</div> </div>
<Popup className={styles.edit} alignment="start"> <Popover className={styles.edit} placement="right top">
{(close: any) => ( {(close: any) => (
<PopupForm> <FieldFilterEditForm
<FieldFilterEditForm websiteId={websiteId}
websiteId={websiteId} name={name}
name={name} label={label}
label={label} type={type}
type={type} startDate={startDate}
startDate={startDate} endDate={endDate}
endDate={endDate} operator={operator}
operator={operator} defaultValue={value}
defaultValue={value} onChange={onChange.bind(null, close)}
onChange={onChange.bind(null, close)} />
/>
</PopupForm>
)} )}
</Popup> </Popover>
</PopupTrigger> </MenuTrigger>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Icon } from 'react-basics'; import { Icon } from '@umami/react-zen';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';

View File

@@ -1,5 +1,5 @@
import { createContext, ReactNode } from 'react'; import { createContext, ReactNode } from 'react';
import { Loading } from 'react-basics'; import { Loading } from '@umami/react-zen';
import classNames from 'classnames'; import classNames from 'classnames';
import { useReport } from '@/components/hooks'; import { useReport } from '@/components/hooks';
import styles from './Report.module.css'; import styles from './Report.module.css';

View File

@@ -1,5 +1,5 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { Icon, LoadingButton, InlineEditField, useToasts } from 'react-basics'; import { Icon, LoadingButton, InlineEditField, useToast } from '@umami/react-zen';
import { useMessages, useApi, useNavigation, useTeamUrl } from '@/components/hooks'; import { useMessages, useApi, useNavigation, useTeamUrl } from '@/components/hooks';
import { ReportContext } from './Report'; import { ReportContext } from './Report';
import styles from './ReportHeader.module.css'; import styles from './ReportHeader.module.css';
@@ -9,7 +9,7 @@ import { Breadcrumb } from '@/components/common/Breadcrumb';
export function ReportHeader({ icon }) { export function ReportHeader({ icon }) {
const { report, updateReport } = useContext(ReportContext); const { report, updateReport } = useContext(ReportContext);
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { showToast } = useToasts(); const { toast } = useToast();
const { router } = useNavigation(); const { router } = useNavigation();
const { renderTeamUrl } = useTeamUrl(); const { renderTeamUrl } = useTeamUrl();
@@ -29,14 +29,14 @@ export function ReportHeader({ icon }) {
if (!report.id) { if (!report.id) {
create(report, { create(report, {
onSuccess: async ({ id }) => { onSuccess: async ({ id }) => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); toast(formatMessage(messages.saved));
router.push(renderTeamUrl(`/reports/${id}`)); router.push(renderTeamUrl(`/reports/${id}`));
}, },
}); });
} else { } else {
update(report, { update(report, {
onSuccess: async () => { onSuccess: async () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); toast(formatMessage(messages.saved));
}, },
}); });
} }
@@ -94,7 +94,7 @@ export function ReportHeader({ icon }) {
variant="primary" variant="primary"
isLoading={isCreating || isUpdating} isLoading={isCreating || isUpdating}
disabled={!websiteId || !dateRange?.value || !name} disabled={!websiteId || !dateRange?.value || !name}
onClick={handleSave} onPress={handleSave}
> >
{formatMessage(labels.save)} {formatMessage(labels.save)}
</LoadingButton> </LoadingButton>

View File

@@ -1,8 +1,8 @@
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { Icon, Icons } from '@umami/react-zen';
import classNames from 'classnames';
import { ReportContext } from './Report'; import { ReportContext } from './Report';
import styles from './ReportMenu.module.css'; import styles from './ReportMenu.module.css';
import { Icon, Icons } from 'react-basics';
import classNames from 'classnames';
export function ReportMenu({ children }) { export function ReportMenu({ children }) {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
@@ -16,7 +16,7 @@ export function ReportMenu({ children }) {
<div className={classNames(styles.menu, collapsed && styles.collapsed)}> <div className={classNames(styles.menu, collapsed && styles.collapsed)}>
<div className={styles.button} onClick={() => setCollapsed(!collapsed)}> <div className={styles.button} onClick={() => setCollapsed(!collapsed)}>
<Icon rotate={collapsed ? -90 : 90}> <Icon rotate={collapsed ? -90 : 90}>
<Icons.ChevronDown /> <Icons.Chevron />
</Icon> </Icon>
</div> </div>
{!collapsed && children} {!collapsed && children}

View File

@@ -1,32 +0,0 @@
.reports {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
gap: 20px;
}
.report {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
border: 1px solid var(--base500);
border-radius: var(--border-radius);
}
.title {
display: flex;
gap: 10px;
align-items: center;
font-size: var(--font-size-lg);
font-weight: 700;
}
.description {
flex: 1;
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,9 +1,8 @@
import { Button, Icon, Text } from '@umami/react-zen'; import { Icon, Text, Row, Column, Grid } from '@umami/react-zen';
import { useMessages, useTeamUrl } from '@/components/hooks'; import { useMessages, useTeamUrl } from '@/components/hooks';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { PageHeader } from '@/components/layout/PageHeader'; import { PageHeader } from '@/components/layout/PageHeader';
import Link from 'next/link'; import { LinkButton } from '@/components/common/LinkButton';
import styles from './ReportTemplates.module.css';
export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) { export function ReportTemplates({ showHeader = true }: { showHeader?: boolean }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@@ -57,13 +56,13 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
return ( return (
<> <>
{showHeader && <PageHeader title={formatMessage(labels.reports)} />} {showHeader && <PageHeader title={formatMessage(labels.reports)} />}
<div className={styles.reports}> <Grid columns="repeat(3, minmax(200px, 1fr))" gap="3">
{reports.map(({ title, description, url, icon }) => { {reports.map(({ title, description, url, icon }) => {
return ( return (
<ReportItem key={title} icon={icon} title={title} description={description} url={url} /> <ReportItem key={title} icon={icon} title={title} description={description} url={url} />
); );
})} })}
</div> </Grid>
</> </>
); );
} }
@@ -72,22 +71,22 @@ function ReportItem({ title, description, url, icon }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
<div className={styles.report}> <Column gap="6" padding="6" borderSize="1" borderRadius="3" justifyContent="space-between">
<div className={styles.title}> <Row gap="3" alignItems="center">
<Icon size="lg">{icon}</Icon> <Icon size="md">{icon}</Icon>
<Text>{title}</Text> <Text size="5" weight="bold">
</div> {title}
<div className={styles.description}>{description}</div> </Text>
<div className={styles.buttons}> </Row>
<Link href={url}> <Text>{description}</Text>
<Button variant="primary"> <Row justifyContent="flex-end">
<Icon> <LinkButton href={url} variant="primary">
<Icons.Plus /> <Icon fillColor="currentColor">
</Icon> <Icons.Plus />
<Text>{formatMessage(labels.create)}</Text> </Icon>
</Button> <Text>{formatMessage(labels.create)}</Text>
</Link> </LinkButton>
</div> </Row>
</div> </Column>
); );
} }

View File

@@ -1,5 +1,13 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; import {
Form,
FormRow,
FormButtons,
SubmitButton,
PopupTrigger,
Icon,
Popup,
} from '@umami/react-zen';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { useApi, useMessages } from '@/components/hooks'; import { useApi, useMessages } from '@/components/hooks';

View File

@@ -1,5 +1,5 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { GridTable, GridColumn } from 'react-basics'; import { GridTable, GridColumn } from '@umami/react-zen';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';

View File

@@ -11,7 +11,7 @@ import {
SubmitButton, SubmitButton,
TextField, TextField,
Button, Button,
} from 'react-basics'; } from '@umami/react-zen';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { FunnelStepAddForm } from './FunnelStepAddForm'; import { FunnelStepAddForm } from './FunnelStepAddForm';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';

View File

@@ -1,6 +1,15 @@
import { useState } from 'react'; import { useState } from 'react';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics'; import {
Button,
Column,
Row,
TextField,
Label,
Select,
ListItem,
FormButtons,
} from '@umami/react-zen';
import styles from './FunnelStepAddForm.module.css'; import styles from './FunnelStepAddForm.module.css';
export interface FunnelStepAddFormProps { export interface FunnelStepAddFormProps {
@@ -39,40 +48,34 @@ export function FunnelStepAddForm({
} }
}; };
const renderTypeValue = (value: any) => {
return items.find(item => item.value === value)?.label;
};
return ( return (
<Flexbox direction="column" gap={10}> <Column gap="3">
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}> <Label>{formatMessage(defaultValue ? labels.update : labels.add)}</Label>
<Flexbox gap={10}> <Row gap="3">
<Dropdown <Select
className={styles.dropdown} className={styles.dropdown}
items={items} items={items}
value={type} value={type}
renderValue={renderTypeValue} onChange={(value: any) => setType(value)}
onChange={(value: any) => setType(value)} >
> {({ value, label }: any) => {
{({ value, label }) => { return <ListItem key={value}>{label}</ListItem>;
return <Item key={value}>{label}</Item>; }}
}} </Select>
</Dropdown> <TextField
<TextField className={styles.input}
className={styles.input} value={value}
value={value} onChange={handleChange}
onChange={handleChange} autoFocus={true}
autoFocus={true} autoComplete="off"
autoComplete="off" onKeyDown={handleKeyDown}
onKeyDown={handleKeyDown} />
/> </Row>
</Flexbox> <FormButtons>
</FormRow> <Button variant="primary" onPress={handleSave} isDisabled={isDisabled}>
<FormRow>
<Button variant="primary" onClick={handleSave} disabled={isDisabled}>
{formatMessage(defaultValue ? labels.update : labels.add)} {formatMessage(defaultValue ? labels.update : labels.add)}
</Button> </Button>
</FormRow> </FormButtons>
</Flexbox> </Column>
); );
} }

View File

@@ -1,6 +1,15 @@
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { useState } from 'react'; import { useState } from 'react';
import { Button, Dropdown, Flexbox, FormRow, Item, TextField } from 'react-basics'; import {
Button,
Row,
Column,
Select,
Label,
ListItem,
TextField,
FormButtons,
} from '@umami/react-zen';
import styles from './GoalsAddForm.module.css'; import styles from './GoalsAddForm.module.css';
export function GoalsAddForm({ export function GoalsAddForm({
@@ -71,44 +80,43 @@ export function GoalsAddForm({
}; };
return ( return (
<Flexbox direction="column" gap={10}> <Column gap="3">
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}> <Label>{formatMessage(defaultValue ? labels.update : labels.add)}</Label>
<Flexbox gap={10}> <Row gap="3">
<Dropdown <Select
className={styles.dropdown} className={styles.dropdown}
items={items} items={items}
value={type} value={type}
renderValue={renderTypeValue} renderValue={renderTypeValue}
onChange={(value: any) => setType(value)} onChange={(value: any) => setType(value)}
> >
{({ value, label }) => { {({ value, label }: any) => {
return <Item key={value}>{label}</Item>; return <ListItem key={value}>{label}</ListItem>;
}} }}
</Dropdown> </Select>
<TextField <TextField
className={styles.input} className={styles.input}
value={value} value={value}
onChange={e => handleChange(e, setValue)} onChange={e => handleChange(e, setValue)}
autoFocus={true} autoFocus={true}
autoComplete="off" autoComplete="off"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
</Flexbox> </Row>
</FormRow>
{type === 'event-data' && ( {type === 'event-data' && (
<FormRow label={formatMessage(labels.property)}> <Column>
<Flexbox gap={10}> <Label>label={formatMessage(labels.property)}</Label>
<Dropdown <Row gap="3">
<Select
className={styles.dropdown} className={styles.dropdown}
items={operators} items={operators}
value={operator} value={operator}
renderValue={renderoperatorValue}
onChange={(value: any) => setOperator(value)} onChange={(value: any) => setOperator(value)}
> >
{({ value, label }) => { {({ value, label }: any) => {
return <Item key={value}>{label}</Item>; return <ListItem key={value}>{label}</ListItem>;
}} }}
</Dropdown> </Select>
<TextField <TextField
className={styles.input} className={styles.input}
value={property} value={property}
@@ -117,11 +125,12 @@ export function GoalsAddForm({
autoComplete="off" autoComplete="off"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
</Flexbox> </Row>
</FormRow> </Column>
)} )}
<FormRow label={formatMessage(labels.goal)}> <Column>
<Flexbox gap={10}> <Label>{formatMessage(labels.goal)}</Label>
<Row gap="3">
<TextField <TextField
className={styles.input} className={styles.input}
value={goal?.toString()} value={goal?.toString()}
@@ -129,13 +138,13 @@ export function GoalsAddForm({
autoComplete="off" autoComplete="off"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
</Flexbox> </Row>
</FormRow> </Column>
<FormRow> <FormButtons>
<Button variant="primary" onClick={handleSave} disabled={isDisabled}> <Button variant="primary" onPress={handleSave} isDisabled={isDisabled}>
{formatMessage(defaultValue ? labels.update : labels.add)} {formatMessage(defaultValue ? labels.update : labels.add)}
</Button> </Button>
</FormRow> </FormButtons>
</Flexbox> </Column>
); );
} }

View File

@@ -4,18 +4,17 @@ import { formatNumber } from '@/lib/format';
import { useContext } from 'react'; import { useContext } from 'react';
import { import {
Button, Button,
Flexbox,
Form, Form,
FormButtons, FormButtons,
FormRow, FormField,
Icon, Icon,
Popup, Popover,
PopupTrigger, MenuTrigger,
SubmitButton, FormSubmitButton,
} from 'react-basics'; Column,
} from '@umami/react-zen';
import { BaseParameters } from '../[reportId]/BaseParameters'; import { BaseParameters } from '../[reportId]/BaseParameters';
import { ParameterList } from '../[reportId]/ParameterList'; import { ParameterList } from '../[reportId]/ParameterList';
import { PopupForm } from '../[reportId]/PopupForm';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';
import { GoalsAddForm } from './GoalsAddForm'; import { GoalsAddForm } from './GoalsAddForm';
import styles from './GoalsParameters.module.css'; import styles from './GoalsParameters.module.css';
@@ -60,25 +59,24 @@ export function GoalsParameters() {
const AddGoalsButton = () => { const AddGoalsButton = () => {
return ( return (
<PopupTrigger> <MenuTrigger>
<Button> <Button>
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>
</Button> </Button>
<Popup alignment="start"> <Popover placement="start">
<PopupForm> <GoalsAddForm onChange={handleAddGoals} />
<GoalsAddForm onChange={handleAddGoals} /> </Popover>
</PopupForm> </MenuTrigger>
</Popup>
</PopupTrigger>
); );
}; };
return ( return (
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}> <Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters allowWebsiteSelect={!id} /> <BaseParameters allowWebsiteSelect={!id} />
<FormRow label={formatMessage(labels.goals)} action={<AddGoalsButton />}> <AddGoalsButton />
<FormField label={formatMessage(labels.goals)}>
<ParameterList> <ParameterList>
{goals.map( {goals.map(
( (
@@ -92,12 +90,12 @@ export function GoalsParameters() {
index: number, index: number,
) => { ) => {
return ( return (
<PopupTrigger key={index}> <MenuTrigger key={index}>
<ParameterList.Item <ParameterList.Item
icon={goal.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />} icon={goal.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}
onRemove={() => handleRemoveGoals(index)} onRemove={() => handleRemoveGoals(index)}
> >
<Flexbox direction="column" gap={5}> <Column>
<div className={styles.value}>{goal.value}</div> <div className={styles.value}>{goal.value}</div>
{goal.type === 'event-data' && ( {goal.type === 'event-data' && (
<div className={styles.eventData}> <div className={styles.eventData}>
@@ -107,32 +105,28 @@ export function GoalsParameters() {
<div className={styles.goal}> <div className={styles.goal}>
{formatMessage(labels.goal)}: {formatNumber(goal.goal)} {formatMessage(labels.goal)}: {formatNumber(goal.goal)}
</div> </div>
</Flexbox> </Column>
</ParameterList.Item> </ParameterList.Item>
<Popup alignment="start"> <Popover placement="start">
{(close: () => void) => ( <GoalsAddForm
<PopupForm> type={goal.type}
<GoalsAddForm value={goal.value}
type={goal.type} goal={goal.goal}
value={goal.value} operator={goal.operator}
goal={goal.goal} property={goal.property}
operator={goal.operator} onChange={handleUpdateGoals.bind(null, () => {}, index)}
property={goal.property} />
onChange={handleUpdateGoals.bind(null, close, index)} </Popover>
/> </MenuTrigger>
</PopupForm>
)}
</Popup>
</PopupTrigger>
); );
}, },
)} )}
</ParameterList> </ParameterList>
</FormRow> </FormField>
<FormButtons> <FormButtons>
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}> <FormSubmitButton variant="primary" isDisabled={queryDisabled} isLoading={isRunning}>
{formatMessage(labels.runQuery)} {formatMessage(labels.runQuery)}
</SubmitButton> </FormSubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { GoalReport } from './GoalsReport'; import { GoalsReport } from './GoalsReport';
export function GoalReportPage() { export function GoalsReportPage() {
return <GoalReport />; return <GoalsReport />;
} }

View File

@@ -1,6 +1,6 @@
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { useContext } from 'react'; import { useContext } from 'react';
import { Form, FormButtons, SubmitButton } from 'react-basics'; import { Form, FormButtons, FormSubmitButton } from '@umami/react-zen';
import { BaseParameters } from '../[reportId]/BaseParameters'; import { BaseParameters } from '../[reportId]/BaseParameters';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';
import { FieldParameters } from '../[reportId]/FieldParameters'; import { FieldParameters } from '../[reportId]/FieldParameters';
@@ -25,9 +25,9 @@ export function InsightsParameters() {
{parametersSelected && <FieldParameters />} {parametersSelected && <FieldParameters />}
{parametersSelected && <FilterParameters />} {parametersSelected && <FilterParameters />}
<FormButtons> <FormButtons>
<SubmitButton variant="primary" disabled={!queryEnabled} isLoading={isRunning}> <FormSubmitButton variant="primary" isDisabled={!queryEnabled} isLoading={isRunning}>
{formatMessage(labels.runQuery)} {formatMessage(labels.runQuery)}
</SubmitButton> </FormSubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@@ -1,5 +1,5 @@
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { GridTable, GridColumn } from 'react-basics'; import { DataTable, DataColumn } from '@umami/react-zen';
import { useFormat, useMessages } from '@/components/hooks'; import { useFormat, useMessages } from '@/components/hooks';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder'; import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
@@ -24,50 +24,35 @@ export function InsightsTable() {
} }
return ( return (
<GridTable data={report?.data || []}> <DataTable data={report?.data || []}>
{fields.map(({ name, label }) => { {fields.map(({ name, label }) => {
return ( return (
<GridColumn key={name} name={name} label={label}> <DataColumn key={name} id={name} label={label}>
{row => formatValue(row[name], name)} {row => formatValue(row[name], name)}
</GridColumn> </DataColumn>
); );
})} })}
<GridColumn name="views" label={formatMessage(labels.views)} width="100px" alignment="end"> <DataColumn id="views" label={formatMessage(labels.views)} align="end">
{row => row?.views?.toLocaleString()} {(row: any) => row?.views?.toLocaleString()}
</GridColumn> </DataColumn>
<GridColumn name="visits" label={formatMessage(labels.visits)} width="100px" alignment="end"> <DataColumn id="visits" label={formatMessage(labels.visits)} align="end">
{row => row?.visits?.toLocaleString()} {(row: any) => row?.visits?.toLocaleString()}
</GridColumn> </DataColumn>
<GridColumn <DataColumn id="visitors" label={formatMessage(labels.visitors)} align="end">
name="visitors" {(row: any) => row?.visitors?.toLocaleString()}
label={formatMessage(labels.visitors)} </DataColumn>
width="100px" <DataColumn id="bounceRate" label={formatMessage(labels.bounceRate)} align="end">
alignment="end" {(row: any) => {
>
{row => row?.visitors?.toLocaleString()}
</GridColumn>
<GridColumn
name="bounceRate"
label={formatMessage(labels.bounceRate)}
width="100px"
alignment="end"
>
{row => {
const n = (Math.min(row?.visits, row?.bounces) / row?.visits) * 100; const n = (Math.min(row?.visits, row?.bounces) / row?.visits) * 100;
return Math.round(+n) + '%'; return Math.round(+n) + '%';
}} }}
</GridColumn> </DataColumn>
<GridColumn <DataColumn id="visitDuration" label={formatMessage(labels.visitDuration)} align="end">
name="visitDuration" {(row: any) => {
label={formatMessage(labels.visitDuration)}
width="100px"
alignment="end"
>
{row => {
const n = row?.totaltime / row?.visits; const n = row?.totaltime / row?.visits;
return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`; return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
}} }}
</GridColumn> </DataColumn>
</GridTable> </DataTable>
); );
} }

View File

@@ -1,15 +1,14 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { import {
Dropdown, Select,
Form, Form,
FormButtons, FormButtons,
FormInput, FormField,
FormRow, ListItem,
Item, FormSubmitButton,
SubmitButton,
TextField, TextField,
} from 'react-basics'; } from '@umami/react-zen';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';
import { BaseParameters } from '../[reportId]/BaseParameters'; import { BaseParameters } from '../[reportId]/BaseParameters';
@@ -33,28 +32,29 @@ export function JourneyParameters() {
return ( return (
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}> <Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} /> <BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
<FormRow label={formatMessage(labels.steps)}> <FormField
<FormInput label={formatMessage(labels.steps)}
name="steps" name="steps"
rules={{ required: formatMessage(labels.required), pattern: /[0-9]+/, min: 3, max: 7 }} rules={{ required: formatMessage(labels.required), pattern: /[0-9]+/, min: 3, max: 7 }}
> >
<Dropdown items={[3, 4, 5, 6, 7]}>{item => <Item key={item}>{item}</Item>}</Dropdown> <Select items={[3, 4, 5, 6, 7]}>
</FormInput> {(item: any) => (
</FormRow> <ListItem key={item.toString()} id={item.toString()}>
<FormRow label={formatMessage(labels.startStep)}> {item}
<FormInput name="startStep"> </ListItem>
<TextField autoComplete="off" /> )}
</FormInput> </Select>
</FormRow> </FormField>
<FormRow label={formatMessage(labels.endStep)}> <FormField label={formatMessage(labels.startStep)} name="startStep">
<FormInput name="endStep"> <TextField autoComplete="off" />
<TextField autoComplete="off" /> </FormField>
</FormInput> <FormField label={formatMessage(labels.endStep)} name="endStep">
</FormRow> <TextField autoComplete="off" />
</FormField>
<FormButtons> <FormButtons>
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}> <FormSubmitButton variant="primary" isDisabled={queryDisabled} isLoading={isRunning}>
{formatMessage(labels.runQuery)} {formatMessage(labels.runQuery)}
</SubmitButton> </FormSubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@@ -1,5 +1,5 @@
import { useContext, useMemo, useState } from 'react'; import { useContext, useMemo, useState } from 'react';
import { TextOverflow, TooltipPopup } from 'react-basics'; import { TextOverflow, TooltipPopup } from '@umami/react-zen';
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEscapeKey, useMessages } from '@/components/hooks'; import { useEscapeKey, useMessages } from '@/components/hooks';

View File

@@ -1,8 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Form, FormButtons, FormRow, SubmitButton } from 'react-basics'; import { Form, FormButtons, FormSubmitButton } from '@umami/react-zen';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';
import { MonthSelect } from '@/components/input/MonthSelect';
import { BaseParameters } from '../[reportId]/BaseParameters'; import { BaseParameters } from '../[reportId]/BaseParameters';
import { parseDateRange } from '@/lib/date'; import { parseDateRange } from '@/lib/date';
@@ -12,7 +11,6 @@ export function RetentionParameters() {
const { id, parameters } = report || {}; const { id, parameters } = report || {};
const { websiteId, dateRange } = parameters || {}; const { websiteId, dateRange } = parameters || {};
const { startDate } = dateRange || {};
const queryDisabled = !websiteId || !dateRange; const queryDisabled = !websiteId || !dateRange;
const handleSubmit = (data: any, e: any) => { const handleSubmit = (data: any, e: any) => {
@@ -31,13 +29,11 @@ export function RetentionParameters() {
return ( return (
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}> <Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters showDateSelect={false} allowWebsiteSelect={!id} /> <BaseParameters showDateSelect={false} allowWebsiteSelect={!id} />
<FormRow label={formatMessage(labels.date)}>
<MonthSelect date={startDate} onChange={handleDateChange} />
</FormRow>
<FormButtons> <FormButtons>
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}> <FormSubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
{formatMessage(labels.runQuery)} {formatMessage(labels.runQuery)}
</SubmitButton> </FormSubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@@ -1,7 +1,7 @@
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { useRevenueValues } from '@/components/hooks/queries/useRevenueValues'; import { useRevenueValues } from '@/components/hooks/queries/useRevenueValues';
import { useContext } from 'react'; import { useContext } from 'react';
import { Dropdown, Form, FormButtons, FormInput, FormRow, Item, SubmitButton } from 'react-basics'; import { Select, Form, FormButtons, FormField, ListItem, FormSubmitButton } from '@umami/react-zen';
import { BaseParameters } from '../[reportId]/BaseParameters'; import { BaseParameters } from '../[reportId]/BaseParameters';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';
@@ -27,17 +27,20 @@ export function RevenueParameters() {
return ( return (
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}> <Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters showDateSelect={true} allowWebsiteSelect={!id} /> <BaseParameters showDateSelect={true} allowWebsiteSelect={!id} />
<FormRow label={formatMessage(labels.currency)}>
<FormInput name="currency" rules={{ required: formatMessage(labels.required) }}> <FormField
<Dropdown items={values.map(item => item.currency)}> label={formatMessage(labels.currency)}
{item => <Item key={item}>{item}</Item>} name="currency"
</Dropdown> rules={{ required: formatMessage(labels.required) }}
</FormInput> >
</FormRow> <Select items={values.map(item => item.currency)}>
{item => <ListItem key={item}>{item}</ListItem>}
</Select>
</FormField>
<FormButtons> <FormButtons>
<SubmitButton variant="primary" disabled={!queryEnabled} isLoading={isRunning}> <FormSubmitButton variant="primary" isDisabled={!queryEnabled} isLoading={isRunning}>
{formatMessage(labels.runQuery)} {formatMessage(labels.runQuery)}
</SubmitButton> </FormSubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@@ -1,7 +1,7 @@
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder'; import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { useContext } from 'react'; import { useContext } from 'react';
import { GridColumn, GridTable } from 'react-basics'; import { GridColumn, GridTable } from '@umami/react-zen';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';
import { formatLongCurrency } from '@/lib/format'; import { formatLongCurrency } from '@/lib/format';

View File

@@ -1,6 +1,6 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Form, FormButtons, SubmitButton } from 'react-basics'; import { Form, FormButtons, SubmitButton } from '@umami/react-zen';
import { ReportContext } from '../[reportId]/Report'; import { ReportContext } from '../[reportId]/Report';
import { BaseParameters } from '../[reportId]/BaseParameters'; import { BaseParameters } from '../[reportId]/BaseParameters';

View File

@@ -3,11 +3,10 @@ import {
Button, Button,
Form, Form,
FormButtons, FormButtons,
FormInput, FormField,
FormRow, FormSubmitButton,
SubmitButton,
TextField, TextField,
} from 'react-basics'; } from '@umami/react-zen';
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@@ -27,16 +26,14 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose:
return ( return (
<Form onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(labels.name)}> <FormField name="name" label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}> <TextField autoComplete="off" />
<TextField autoComplete="off" /> </FormField>
</FormInput> <FormButtons>
</FormRow> <FormSubmitButton variant="primary" disabled={isPending}>
<FormButtons flex>
<SubmitButton variant="primary" disabled={isPending}>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</SubmitButton> </FormSubmitButton>
<Button disabled={isPending} onClick={onClose}> <Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
</FormButtons> </FormButtons>

View File

@@ -1,20 +1,17 @@
import { useRef } from 'react';
import { import {
Form, Form,
FormRow, FormField,
FormInput,
FormButtons, FormButtons,
TextField, TextField,
Button, Button,
SubmitButton, FormSubmitButton,
} from 'react-basics'; } from '@umami/react-zen';
import { useApi, useMessages, useModified } from '@/components/hooks'; import { useApi, useMessages, useModified } from '@/components/hooks';
export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) { export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, error } = useMutation({ mutationFn: (data: any) => post('/teams/join', data) }); const { mutate, error } = useMutation({ mutationFn: (data: any) => post('/teams/join', data) });
const ref = useRef(null);
const { touch } = useModified(); const { touch } = useModified();
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
@@ -28,15 +25,17 @@ export function TeamJoinForm({ onSave, onClose }: { onSave: () => void; onClose:
}; };
return ( return (
<Form ref={ref} onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(labels.accessCode)}> <FormField
<FormInput name="accessCode" rules={{ required: formatMessage(labels.required) }}> label={formatMessage(labels.accessCode)}
<TextField autoComplete="off" /> name="accessCode"
</FormInput> rules={{ required: formatMessage(labels.required) }}
</FormRow> >
<FormButtons flex> <TextField autoComplete="off" />
<SubmitButton variant="primary">{formatMessage(labels.join)}</SubmitButton> </FormField>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button> <FormButtons>
<FormSubmitButton variant="primary">{formatMessage(labels.join)}</FormSubmitButton>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@@ -1,6 +1,6 @@
import { useLocale, useLogin, useMessages, useModified } from '@/components/hooks'; import { useLocale, useLogin, useMessages, useModified } from '@/components/hooks';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; import { Button, Icon, Icons, Modal, DialogTrigger, Dialog, Text } from '@umami/react-zen';
import { TeamLeaveForm } from './TeamLeaveForm'; import { TeamLeaveForm } from './TeamLeaveForm';
export function TeamLeaveButton({ teamId, teamName }: { teamId: string; teamName: string }) { export function TeamLeaveButton({ teamId, teamName }: { teamId: string; teamName: string }) {
@@ -16,24 +16,26 @@ export function TeamLeaveButton({ teamId, teamName }: { teamId: string; teamName
}; };
return ( return (
<ModalTrigger> <DialogTrigger>
<Button variant="secondary"> <Button variant="secondary">
<Icon rotate={dir === 'rtl' ? 180 : 0}> <Icon rotate={dir === 'rtl' ? 180 : 0}>
<Icons.Logout /> <Icons.Logout />
</Icon> </Icon>
<Text>{formatMessage(labels.leave)}</Text> <Text>{formatMessage(labels.leave)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.leaveTeam)}> <Modal>
{(close: () => void) => ( <Dialog title={formatMessage(labels.leaveTeam)}>
<TeamLeaveForm {({ close }) => (
teamId={teamId} <TeamLeaveForm
userId={user.id} teamId={teamId}
teamName={teamName} userId={user.id}
onSave={handleLeave} teamName={teamName}
onClose={close} onSave={handleLeave}
/> onClose={close}
)} />
)}
</Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
); );
} }

View File

@@ -34,7 +34,9 @@ export function TeamLeaveForm({
return ( return (
<ConfirmationForm <ConfirmationForm
buttonLabel={formatMessage(labels.leave)} buttonLabel={formatMessage(labels.leave)}
message={formatMessage(messages.confirmLeave, { target: <b>{teamName}</b> })} message={formatMessage(messages.confirmLeave, {
target: <b key={messages.confirmLeave.id}>{teamName}</b>,
})}
onConfirm={handleConfirm} onConfirm={handleConfirm}
onClose={onClose} onClose={onClose}
isLoading={isPending} isLoading={isPending}

View File

@@ -1,4 +1,4 @@
import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; import { Button, Icon, Modal, DialogTrigger, Dialog, Text, useToast } from '@umami/react-zen';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { useMessages, useModified } from '@/components/hooks'; import { useMessages, useModified } from '@/components/hooks';
import { TeamAddForm } from './TeamAddForm'; import { TeamAddForm } from './TeamAddForm';
@@ -6,26 +6,28 @@ import { messages } from '@/components/messages';
export function TeamsAddButton({ onSave }: { onSave?: () => void }) { export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { showToast } = useToasts(); const { toast } = useToast();
const { touch } = useModified(); const { touch } = useModified();
const handleSave = async () => { const handleSave = async () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); toast(formatMessage(messages.saved));
touch('teams'); touch('teams');
onSave?.(); onSave?.();
}; };
return ( return (
<ModalTrigger> <DialogTrigger>
<Button variant="primary"> <Button variant="primary">
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>
<Text>{formatMessage(labels.createTeam)}</Text> <Text>{formatMessage(labels.createTeam)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.createTeam)}> <Modal>
{(close: () => void) => <TeamAddForm onSave={handleSave} onClose={close} />} <Dialog title={formatMessage(labels.createTeam)}>
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} />}
</Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Flexbox } from 'react-basics'; import { Row } from '@umami/react-zen';
import { PageHeader } from '@/components/layout/PageHeader'; import { PageHeader } from '@/components/layout/PageHeader';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { useLogin, useMessages } from '@/components/hooks'; import { useLogin, useMessages } from '@/components/hooks';
@@ -12,10 +12,10 @@ export function TeamsHeader({ allowCreate = true }: { allowCreate?: boolean }) {
return ( return (
<PageHeader title={formatMessage(labels.teams)}> <PageHeader title={formatMessage(labels.teams)}>
<Flexbox gap={10}> <Row gap="3">
{!cloudMode && <TeamsJoinButton />} {!cloudMode && <TeamsJoinButton />}
{allowCreate && user.role !== ROLES.viewOnly && <TeamsAddButton />} {allowCreate && user.role !== ROLES.viewOnly && <TeamsAddButton />}
</Flexbox> </Row>
</PageHeader> </PageHeader>
); );
} }

View File

@@ -1,29 +1,31 @@
import { Button, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; import { Button, Icon, Modal, DialogTrigger, Dialog, Text, useToast } from '@umami/react-zen';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { useMessages, useModified } from '@/components/hooks'; import { useMessages, useModified } from '@/components/hooks';
import { TeamJoinForm } from './TeamJoinForm'; import { TeamJoinForm } from './TeamJoinForm';
export function TeamsJoinButton() { export function TeamsJoinButton() {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { showToast } = useToasts(); const { toast } = useToast();
const { touch } = useModified(); const { touch } = useModified();
const handleJoin = () => { const handleJoin = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); toast(formatMessage(messages.saved));
touch('teams'); touch('teams');
}; };
return ( return (
<ModalTrigger> <DialogTrigger>
<Button variant="secondary"> <Button variant="secondary">
<Icon> <Icon>
<Icons.AddUser /> <Icons.AddUser />
</Icon> </Icon>
<Text>{formatMessage(labels.joinTeam)}</Text> <Text>{formatMessage(labels.joinTeam)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.joinTeam)}> <Modal>
{close => <TeamJoinForm onSave={handleJoin} onClose={close} />} <Dialog title={formatMessage(labels.joinTeam)}>
{({ close }) => <TeamJoinForm onSave={handleJoin} onClose={close} />}
</Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
); );
} }

View File

@@ -1,4 +1,4 @@
import { GridColumn, GridTable, Icon, Text } from 'react-basics'; import { DataColumn, DataTable, Icon, Text } from '@umami/react-zen';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
@@ -15,33 +15,33 @@ export function TeamsTable({
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
<GridTable data={data}> <DataTable data={data}>
<GridColumn name="name" label={formatMessage(labels.name)} /> <DataColumn id="name" label={formatMessage(labels.name)} />
<GridColumn name="owner" label={formatMessage(labels.owner)}> <DataColumn id="owner" label={formatMessage(labels.owner)}>
{row => row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username} {(row: any) => row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
</GridColumn> </DataColumn>
<GridColumn name="websites" label={formatMessage(labels.websites)}> <DataColumn id="websites" label={formatMessage(labels.websites)}>
{row => row._count.website} {(row: any) => row._count.website}
</GridColumn> </DataColumn>
<GridColumn name="members" label={formatMessage(labels.members)}> <DataColumn id="members" label={formatMessage(labels.members)}>
{row => row._count.teamUser} {(row: any) => row._count.teamUser}
</GridColumn> </DataColumn>
{showActions && ( {showActions && (
<GridColumn name="action" label=" " alignment="end"> <DataColumn id="action" label=" " align="end">
{row => { {(row: any) => {
const { id } = row; const { id } = row;
return ( return (
<LinkButton href={`/teams/${id}/settings`}> <LinkButton href={`/teams/${id}/settings`}>
<Icon> <Icon>
<Icons.ArrowRight /> <Icons.Arrow />
</Icon> </Icon>
<Text>{formatMessage(labels.view)}</Text> <Text>{formatMessage(labels.view)}</Text>
</LinkButton> </LinkButton>
); );
}} }}
</GridColumn> </DataColumn>
)} )}
</GridTable> </DataTable>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Button, Icon, Icons, Text } from 'react-basics'; import { Button, Icon, Icons, Text } from '@umami/react-zen';
import styles from './WebsiteTags.module.css'; import styles from './WebsiteTags.module.css';
export function WebsiteTags({ export function WebsiteTags({
@@ -21,7 +21,7 @@ export function WebsiteTags({
return ( return (
<div key={websiteId} className={styles.tag}> <div key={websiteId} className={styles.tag}>
<Button onClick={() => onClick(websiteId)} variant="primary" size="sm"> <Button onPress={() => onClick(websiteId)} variant="primary" size="sm">
<Text> <Text>
<b>{`${website.name}`}</b> <b>{`${website.name}`}</b>
</Text> </Text>

View File

@@ -24,7 +24,7 @@ export function UserAddButton({ onSave }: { onSave?: () => void }) {
return ( return (
<DialogTrigger> <DialogTrigger>
<Button variant="primary"> <Button variant="primary" data-test="button-create-user">
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>

View File

@@ -1,17 +1,17 @@
import { import {
Dropdown, Select,
Item, ListItem,
Form, Form,
FormRow, FormField,
FormButtons, FormButtons,
FormInput, FormSubmitButton,
TextField, TextField,
PasswordField, PasswordField,
SubmitButton,
Button, Button,
} from 'react-basics'; } from '@umami/react-zen';
import { useApi, useMessages } from '@/components/hooks'; import { useApi, useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { messages } from '@/components/messages';
export function UserAddForm({ onSave, onClose }) { export function UserAddForm({ onSave, onClose }) {
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
@@ -29,44 +29,38 @@ export function UserAddForm({ onSave, onClose }) {
}); });
}; };
const renderValue = (value: string) => {
if (value === ROLES.user) {
return formatMessage(labels.user);
}
if (value === ROLES.admin) {
return formatMessage(labels.admin);
}
if (value === ROLES.viewOnly) {
return formatMessage(labels.viewOnly);
}
};
return ( return (
<Form onSubmit={handleSubmit} error={error}> <Form onSubmit={handleSubmit} error={error}>
<FormRow label={formatMessage(labels.username)}> <FormField
<FormInput name="username" rules={{ required: formatMessage(labels.required) }}> label={formatMessage(labels.username)}
<TextField autoComplete="new-username" /> name="username"
</FormInput> rules={{ required: formatMessage(labels.required) }}
</FormRow> >
<FormRow label={formatMessage(labels.password)}> <TextField autoComplete="new-username" data-test="input-username" />
<FormInput name="password" rules={{ required: formatMessage(labels.required) }}> </FormField>
<PasswordField autoComplete="new-password" /> <FormField
</FormInput> label={formatMessage(labels.password)}
</FormRow> name="password"
<FormRow label={formatMessage(labels.role)}> rules={{ required: formatMessage(labels.required) }}
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}> >
<Dropdown renderValue={renderValue}> <PasswordField autoComplete="new-password" data-test="input-password" />
<Item key={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</Item> </FormField>
<Item key={ROLES.user}>{formatMessage(labels.user)}</Item> <FormField
<Item key={ROLES.admin}>{formatMessage(labels.admin)}</Item> label={formatMessage(labels.role)}
</Dropdown> name="role"
</FormInput> rules={{ required: formatMessage(labels.required) }}
</FormRow> >
<FormButtons flex> <Select>
<SubmitButton variant="primary" disabled={false}> <ListItem id={ROLES.viewOnly} data-test="dropdown-item-viewOnly">{formatMessage(labels.viewOnly)}</ListItem>
<ListItem id={ROLES.user} data-test="dropdown-item-user">{formatMessage(labels.user)}</ListItem>
<ListItem id={ROLES.admin} data-test="dropdown-item-admin">{formatMessage(labels.admin)}</ListItem>
</Select>
</FormField>
<FormButtons>
<FormSubmitButton variant="primary" data-test="button-submit" isDisabled={false}>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</SubmitButton> </FormSubmitButton>
<Button disabled={isPending} onClick={onClose}> <Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
</FormButtons> </FormButtons>

View File

@@ -16,7 +16,7 @@ export function UserDeleteButton({
return ( return (
<DialogTrigger> <DialogTrigger>
<Button isDisabled={userId === user?.id}> <Button isDisabled={userId === user?.id} data-test="button-delete">
<Icon size="sm"> <Icon size="sm">
<Icons.Trash /> <Icons.Trash />
</Icon> </Icon>

View File

@@ -22,7 +22,9 @@ export function UserDeleteForm({ userId, username, onSave, onClose }) {
return ( return (
<ConfirmationForm <ConfirmationForm
message={formatMessage(messages.confirmDelete, { target: <b>{username}</b> })} message={formatMessage(messages.confirmDelete, {
target: <b key={messages.confirmDelete.id}>{username}</b>,
})}
onConfirm={handleConfirm} onConfirm={handleConfirm}
onClose={onClose} onClose={onClose}
buttonLabel={formatMessage(labels.delete)} buttonLabel={formatMessage(labels.delete)}

View File

@@ -3,7 +3,8 @@ import Link from 'next/link';
import { formatDistance } from 'date-fns'; import { formatDistance } from 'date-fns';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { useMessages, useLocale } from '@/components/hooks'; import { useMessages, useLocale } from '@/components/hooks';
import { UserDeleteButton } from './UserDeleteButton'; import UserDeleteButton from './UserDeleteButton';
import LinkButton from '@/components/common/LinkButton';
export function UsersTable({ export function UsersTable({
data = [], data = [],
@@ -44,7 +45,7 @@ export function UsersTable({
<Row gap="3"> <Row gap="3">
<UserDeleteButton userId={id} username={username} /> <UserDeleteButton userId={id} username={username} />
<Button asChild> <Button asChild>
<Link href={`/settings/users/${id}`}> <Link href={`/settings/users/${id}`} data-test="link-button-edit">
<Icon> <Icon>
<Icons.Edit /> <Icons.Edit />
</Icon> </Icon>

View File

@@ -47,7 +47,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
return ( return (
<Form onSubmit={handleSubmit} error={getMessage(error)} values={user} style={{ width: 300 }}> <Form onSubmit={handleSubmit} error={getMessage(error)} values={user} style={{ width: 300 }}>
<FormField name="username" label={formatMessage(labels.username)}> <FormField name="username" label={formatMessage(labels.username)}>
<TextField /> <TextField data-test="input-username" />
</FormField> </FormField>
<FormField <FormField
name="password" name="password"
@@ -56,7 +56,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) }, minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: 8 }) },
}} }}
> >
<PasswordField autoComplete="new-password" /> <PasswordField autoComplete="new-password" data-test="input-password" />
</FormField> </FormField>
{user.id !== login.id && ( {user.id !== login.id && (
@@ -66,14 +66,14 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
rules={{ required: formatMessage(labels.required) }} rules={{ required: formatMessage(labels.required) }}
> >
<Select defaultSelectedKey={user.role}> <Select defaultSelectedKey={user.role}>
<ListItem id={ROLES.viewOnly}>{formatMessage(labels.viewOnly)}</ListItem> <ListItem id={ROLES.viewOnly} data-test="dropdown-item-viewOnly">{formatMessage(labels.viewOnly)}</ListItem>
<ListItem id={ROLES.user}>{formatMessage(labels.user)}</ListItem> <ListItem id={ROLES.user} data-test="dropdown-item-user">{formatMessage(labels.user)}</ListItem>
<ListItem id={ROLES.admin}>{formatMessage(labels.admin)}</ListItem> <ListItem id={ROLES.admin} data-test="dropdown-item-admin">{formatMessage(labels.admin)}</ListItem>
</Select> </Select>
</FormField> </FormField>
)} )}
<FormButtons> <FormButtons>
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton> <FormSubmitButton data-test="button-submit" variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@@ -1,13 +1,14 @@
import { import {
Form, Form,
FormRow, FormField,
FormButtons, FormButtons,
Flexbox,
TextField, TextField,
Button, Button,
Toggle, Switch,
LoadingButton, FormSubmitButton,
} from 'react-basics'; Box,
useToast,
} from '@umami/react-zen';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { getRandomChars } from '@/lib/crypto'; import { getRandomChars } from '@/lib/crypto';
import { useApi, useMessages, useModified } from '@/components/hooks'; import { useApi, useMessages, useModified } from '@/components/hooks';
@@ -25,6 +26,7 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () =>
mutationFn: (data: any) => post(`/websites/${website.id}`, data), mutationFn: (data: any) => post(`/websites/${website.id}`, data),
}); });
const { touch } = useModified(); const { touch } = useModified();
const { toast } = useToast();
const url = `${hostUrl || window?.location.origin || ''}${ const url = `${hostUrl || window?.location.origin || ''}${
process.env.basePath || '' process.env.basePath || ''
@@ -34,7 +36,8 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () =>
setId(generateId()); setId(generateId());
}; };
const handleCheck = (checked: boolean) => { const handleSwitch = (checked: boolean) => {
console.log({ checked });
const data = { const data = {
name: website.name, name: website.name,
domain: website.domain, domain: website.domain,
@@ -42,6 +45,7 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () =>
}; };
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved));
touch(`website:${website.id}`); touch(`website:${website.id}`);
onSave?.(); onSave?.();
}, },
@@ -54,6 +58,7 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () =>
{ name: website.name, domain: website.domain, shareId: id }, { name: website.name, domain: website.domain, shareId: id },
{ {
onSuccess: async () => { onSuccess: async () => {
toast(formatMessage(messages.saved));
touch(`website:${website.id}`); touch(`website:${website.id}`);
onSave?.(); onSave?.();
}, },
@@ -63,27 +68,21 @@ export function ShareUrl({ hostUrl, onSave }: { hostUrl?: string; onSave?: () =>
return ( return (
<> <>
<Toggle checked={Boolean(id)} onChecked={handleCheck} style={{ marginBottom: 30 }}> <Box marginBottom="6">
{formatMessage(labels.enableShareUrl)} <Switch defaultSelected={!!id} isSelected={!!id} onChange={handleSwitch}>
</Toggle> {formatMessage(labels.enableShareUrl)}
</Switch>
</Box>
{id && ( {id && (
<Form error={error}> <Form onSubmit={handleSave} error={error} values={{ id, url }}>
<FormRow> <FormField label={formatMessage(messages.shareUrl)} name="url">
<p>{formatMessage(messages.shareUrl)}</p> <TextField isReadOnly allowCopy />
<Flexbox gap={10}> </FormField>
<TextField value={url} readOnly allowCopy /> <FormButtons justifyContent="space-between">
<Button onClick={handleGenerate}>{formatMessage(labels.regenerate)}</Button> <Button onPress={handleGenerate}>{formatMessage(labels.regenerate)}</Button>
</Flexbox> <FormSubmitButton variant="primary" isDisabled={id === shareId} isLoading={isPending}>
</FormRow>
<FormButtons>
<LoadingButton
variant="primary"
disabled={id === shareId}
isLoading={isPending}
onClick={handleSave}
>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</LoadingButton> </FormSubmitButton>
</FormButtons> </FormButtons>
</Form> </Form>
)} )}

View File

@@ -1,4 +1,4 @@
import { TextArea } from 'react-basics'; import { TextArea } from '@umami/react-zen';
import { useMessages, useConfig } from '@/components/hooks'; import { useMessages, useConfig } from '@/components/hooks';
const SCRIPT_NAME = 'script.js'; const SCRIPT_NAME = 'script.js';

View File

@@ -1,9 +1,10 @@
import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; import { Button, Modal, DialogTrigger, Dialog, Column } from '@umami/react-zen';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useLogin, useMessages, useModified, useTeams, useTeamUrl } from '@/components/hooks'; import { useLogin, useMessages, useModified, useTeams, useTeamUrl } from '@/components/hooks';
import { WebsiteDeleteForm } from './WebsiteDeleteForm'; import { WebsiteDeleteForm } from './WebsiteDeleteForm';
import { WebsiteResetForm } from './WebsiteResetForm'; import { WebsiteResetForm } from './WebsiteResetForm';
import { WebsiteTransferForm } from './WebsiteTransferForm'; import { WebsiteTransferForm } from './WebsiteTransferForm';
import { ActionForm } from '@/components/layout/ActionForm';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) { export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: () => void }) {
@@ -39,50 +40,58 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?:
}; };
return ( return (
<> <Column gap="6">
<ActionForm <ActionForm
label={formatMessage(labels.transferWebsite)} label={formatMessage(labels.transferWebsite)}
description={formatMessage(messages.transferWebsite)} description={formatMessage(messages.transferWebsite)}
> >
<ModalTrigger disabled={!canTransferWebsite}> <DialogTrigger>
<Button variant="secondary" disabled={!canTransferWebsite}> <Button variant="secondary" isDisabled={!canTransferWebsite}>
{formatMessage(labels.transfer)} {formatMessage(labels.transfer)}
</Button> </Button>
<Modal title={formatMessage(labels.transferWebsite)}> <Modal>
{(close: () => void) => ( <Dialog title={formatMessage(labels.transferWebsite)}>
<WebsiteTransferForm websiteId={websiteId} onSave={handleSave} onClose={close} /> {({ close }) => (
)} <WebsiteTransferForm websiteId={websiteId} onSave={handleSave} onClose={close} />
)}
</Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
</ActionForm> </ActionForm>
<ActionForm <ActionForm
label={formatMessage(labels.resetWebsite)} label={formatMessage(labels.resetWebsite)}
description={formatMessage(messages.resetWebsiteWarning)} description={formatMessage(messages.resetWebsiteWarning)}
> >
<ModalTrigger> <DialogTrigger>
<Button variant="secondary">{formatMessage(labels.reset)}</Button> <Button variant="secondary">{formatMessage(labels.reset)}</Button>
<Modal title={formatMessage(labels.resetWebsite)}> <Modal>
{(close: () => void) => ( <Dialog title={formatMessage(labels.resetWebsite)}>
<WebsiteResetForm websiteId={websiteId} onSave={handleReset} onClose={close} /> {({ close }) => (
)} <WebsiteResetForm websiteId={websiteId} onSave={handleReset} onClose={close} />
)}
</Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
</ActionForm> </ActionForm>
<ActionForm <ActionForm
label={formatMessage(labels.deleteWebsite)} label={formatMessage(labels.deleteWebsite)}
description={formatMessage(messages.deleteWebsiteWarning)} description={formatMessage(messages.deleteWebsiteWarning)}
> >
<ModalTrigger> <DialogTrigger>
<Button data-test="button-delete" variant="danger"> <Button data-test="button-delete" variant="danger">
{formatMessage(labels.delete)} {formatMessage(labels.delete)}
</Button> </Button>
<Modal title={formatMessage(labels.deleteWebsite)}> <Modal>
{(close: () => void) => ( <Dialog title={formatMessage(labels.deleteWebsite)}>
<WebsiteDeleteForm websiteId={websiteId} onSave={handleSave} onClose={close} /> {({ close }) => (
)} <WebsiteDeleteForm websiteId={websiteId} onSave={handleSave} onClose={close} />
)}
</Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
</ActionForm> </ActionForm>
</> </Column>
); );
} }

View File

@@ -26,7 +26,7 @@ export function WebsiteSettings({
<Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}> <Link href={`/websites/${websiteId}`} target={openExternal ? '_blank' : null}>
<Button variant="primary"> <Button variant="primary">
<Icon> <Icon>
<Icons.ArrowRight /> <Icons.Arrow />
</Icon> </Icon>
<Text>{formatMessage(labels.view)}</Text> <Text>{formatMessage(labels.view)}</Text>
</Button> </Button>

View File

@@ -3,13 +3,13 @@ import {
Button, Button,
Form, Form,
FormButtons, FormButtons,
FormRow, FormField,
LoadingButton, FormSubmitButton,
Loading, Loading,
Dropdown, Select,
Item, ListItem,
Flexbox, Text,
} from 'react-basics'; } from '@umami/react-zen';
import { useApi, useLogin, useMessages, useTeams } from '@/components/hooks'; import { useApi, useLogin, useMessages, useTeams } from '@/components/hooks';
import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; import { WebsiteContext } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
@@ -28,12 +28,19 @@ export function WebsiteTransferForm({
const [teamId, setTeamId] = useState<string>(null); const [teamId, setTeamId] = useState<string>(null);
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { post, useMutation } = useApi(); const { post, useMutation } = useApi();
const { mutate, isPending, error } = useMutation({ const { mutate, error } = useMutation({
mutationFn: (data: any) => post(`/websites/${websiteId}/transfer`, data), mutationFn: (data: any) => post(`/websites/${websiteId}/transfer`, data),
}); });
const { result, query } = useTeams(user.id); const { result, query } = useTeams(user.id);
const isTeamWebsite = !!website?.teamId; const isTeamWebsite = !!website?.teamId;
const items = result.data.filter(({ teamUser }) =>
teamUser.find(
({ role, userId }) =>
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id,
),
);
const handleSubmit = async () => { const handleSubmit = async () => {
mutate( mutate(
{ {
@@ -53,45 +60,35 @@ export function WebsiteTransferForm({
setTeamId(key as string); setTeamId(key as string);
}; };
const renderValue = (teamId: string) => result?.data?.find(({ id }) => id === teamId)?.name;
if (query.isLoading) { if (query.isLoading) {
return <Loading icon="dots" position="center" />; return <Loading icon="dots" position="center" />;
} }
return ( return (
<Form error={error}> <Form onSubmit={handleSubmit} error={error} values={{ teamId }}>
<FormRow> <Text>
<Flexbox direction="column" gap={20}> {formatMessage(
{formatMessage( isTeamWebsite ? messages.transferTeamWebsiteToUser : messages.transferUserWebsiteToTeam,
isTeamWebsite ? messages.transferTeamWebsiteToUser : messages.transferUserWebsiteToTeam, )}
)} </Text>
{!isTeamWebsite && ( <FormField name="teamId">
<Dropdown onChange={handleChange} value={teamId} renderValue={renderValue}> {!isTeamWebsite && (
{result.data <Select onSelectionChange={handleChange} value={teamId}>
.filter(({ teamUser }) => {items.map(({ id, name }) => {
teamUser.find( return (
({ role, userId }) => <ListItem key={id} id={id}>
[ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id, {name}
), </ListItem>
) );
.map(({ id, name }) => { })}
return <Item key={id}>{name}</Item>; </Select>
})} )}
</Dropdown> </FormField>
)} <FormButtons>
</Flexbox> <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
</FormRow> <FormSubmitButton variant="primary" isDisabled={!isTeamWebsite && !teamId}>
<FormButtons flex>
<LoadingButton
variant="primary"
isLoading={isPending}
disabled={!isTeamWebsite && !teamId}
onClick={handleSubmit}
>
{formatMessage(labels.transfer)} {formatMessage(labels.transfer)}
</LoadingButton> </FormSubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</FormButtons> </FormButtons>
</Form> </Form>
); );

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { createContext, ReactNode, useEffect } from 'react'; import { createContext, ReactNode, useEffect } from 'react';
import { useTeam, useModified } from '@/components/hooks'; import { useTeam, useModified } from '@/components/hooks';
import { Loading } from 'react-basics'; import { Loading } from '@umami/react-zen';
export const TeamContext = createContext(null); export const TeamContext = createContext(null);

View File

@@ -1,5 +1,14 @@
import { useMessages, useModified } from '@/components/hooks'; import { useMessages, useModified } from '@/components/hooks';
import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics'; import {
Button,
Icon,
Icons,
Modal,
DialogTrigger,
Dialog,
Text,
useToast,
} from '@umami/react-zen';
import { TeamMemberEditForm } from './TeamMemberEditForm'; import { TeamMemberEditForm } from './TeamMemberEditForm';
export function TeamMemberEditButton({ export function TeamMemberEditButton({
@@ -14,34 +23,36 @@ export function TeamMemberEditButton({
onSave?: () => void; onSave?: () => void;
}) { }) {
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { showToast } = useToasts(); const { toast } = useToast();
const { touch } = useModified(); const { touch } = useModified();
const handleSave = () => { const handleSave = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' }); toast(formatMessage(messages.saved));
touch('teams:members'); touch('teams:members');
onSave?.(); onSave?.();
}; };
return ( return (
<ModalTrigger> <DialogTrigger>
<Button> <Button>
<Icon> <Icon>
<Icons.Edit /> <Icons.Edit />
</Icon> </Icon>
<Text>{formatMessage(labels.edit)}</Text> <Text>{formatMessage(labels.edit)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.editMember)}> <Modal>
{(close: () => void) => ( <Dialog title={formatMessage(labels.editMember)}>
<TeamMemberEditForm {({ close }) => (
teamId={teamId} <TeamMemberEditForm
userId={userId} teamId={teamId}
role={role} userId={userId}
onSave={handleSave} role={role}
onClose={close} onSave={handleSave}
/> onClose={close}
)} />
)}
</Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
); );
} }

View File

@@ -2,14 +2,13 @@ import { useApi, useMessages } from '@/components/hooks';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { import {
Button, Button,
Dropdown, Select,
Form, Form,
FormButtons, FormButtons,
FormInput, FormField,
FormRow, ListItem,
Item, FormSubmitButton,
SubmitButton, } from '@umami/react-zen';
} from 'react-basics';
export function TeamMemberEditForm({ export function TeamMemberEditForm({
teamId, teamId,
@@ -39,39 +38,25 @@ export function TeamMemberEditForm({
}); });
}; };
const renderValue = (value: string) => {
if (value === ROLES.teamManager) {
return formatMessage(labels.manager);
}
if (value === ROLES.teamMember) {
return formatMessage(labels.member);
}
if (value === ROLES.teamViewOnly) {
return formatMessage(labels.viewOnly);
}
};
return ( return (
<Form onSubmit={handleSubmit} error={error} values={{ role }}> <Form onSubmit={handleSubmit} error={error} values={{ role }}>
<FormRow label={formatMessage(labels.role)}> <FormField
<FormInput name="role" rules={{ required: formatMessage(labels.required) }}> name="role"
<Dropdown rules={{ required: formatMessage(labels.required) }}
renderValue={renderValue} label={formatMessage(labels.role)}
style={{ >
minWidth: '250px', <Select>
}} <ListItem id={ROLES.teamManager}>{formatMessage(labels.manager)}</ListItem>
> <ListItem id={ROLES.teamMember}>{formatMessage(labels.member)}</ListItem>
<Item key={ROLES.teamManager}>{formatMessage(labels.manager)}</Item> <ListItem id={ROLES.teamViewOnly}>{formatMessage(labels.viewOnly)}</ListItem>
<Item key={ROLES.teamMember}>{formatMessage(labels.member)}</Item> </Select>
<Item key={ROLES.teamViewOnly}>{formatMessage(labels.viewOnly)}</Item> </FormField>
</Dropdown>
</FormInput> <FormButtons>
</FormRow> <FormSubmitButton variant="primary" disabled={false}>
<FormButtons flex>
<SubmitButton variant="primary" disabled={false}>
{formatMessage(labels.save)} {formatMessage(labels.save)}
</SubmitButton> </FormSubmitButton>
<Button disabled={isPending} onClick={onClose}> <Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)} {formatMessage(labels.cancel)}
</Button> </Button>
</FormButtons> </FormButtons>

View File

@@ -1,7 +1,7 @@
import { ConfirmationForm } from '@/components/common/ConfirmationForm'; import { ConfirmationForm } from '@/components/common/ConfirmationForm';
import { useApi, useMessages, useModified } from '@/components/hooks'; import { useApi, useMessages, useModified } from '@/components/hooks';
import { messages } from '@/components/messages'; import { messages } from '@/components/messages';
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics'; import { Button, Icon, Icons, Modal, DialogTrigger, Dialog, Text } from '@umami/react-zen';
export function TeamMemberRemoveButton({ export function TeamMemberRemoveButton({
teamId, teamId,
@@ -33,25 +33,29 @@ export function TeamMemberRemoveButton({
}; };
return ( return (
<ModalTrigger> <DialogTrigger>
<Button> <Button>
<Icon> <Icon>
<Icons.Close /> <Icons.Close />
</Icon> </Icon>
<Text>{formatMessage(labels.remove)}</Text> <Text>{formatMessage(labels.remove)}</Text>
</Button> </Button>
<Modal title={formatMessage(labels.removeMember)}> <Modal>
{(close: () => void) => ( <Dialog title={formatMessage(labels.removeMember)}>
<ConfirmationForm {({ close }) => (
message={formatMessage(messages.confirmRemove, { target: <b>{userName}</b> })} <ConfirmationForm
isLoading={isPending} message={formatMessage(messages.confirmRemove, {
error={error} target: <b key={messages.confirmRemove.id}>{userName}</b>,
onConfirm={handleConfirm.bind(null, close)} })}
onClose={close} isLoading={isPending}
buttonLabel={formatMessage(labels.remove)} error={error}
/> onConfirm={handleConfirm.bind(null, close)}
)} onClose={close}
buttonLabel={formatMessage(labels.remove)}
/>
)}
</Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
); );
} }

View File

@@ -1,4 +1,4 @@
import { GridColumn, GridTable } from 'react-basics'; import { DataColumn, DataTable } from '@umami/react-zen';
import { useMessages, useLogin } from '@/components/hooks'; import { useMessages, useLogin } from '@/components/hooks';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { TeamMemberRemoveButton } from './TeamMemberRemoveButton'; import { TeamMemberRemoveButton } from './TeamMemberRemoveButton';
@@ -24,15 +24,15 @@ export function TeamMembersTable({
}; };
return ( return (
<GridTable data={data}> <DataTable data={data}>
<GridColumn name="username" label={formatMessage(labels.username)}> <DataColumn id="username" label={formatMessage(labels.username)}>
{row => row?.user?.username} {(row: any) => row?.user?.username}
</GridColumn> </DataColumn>
<GridColumn name="role" label={formatMessage(labels.role)}> <DataColumn id="role" label={formatMessage(labels.role)}>
{row => roles[row?.role]} {(row: any) => roles[row?.role]}
</GridColumn> </DataColumn>
<GridColumn name="action" label=" " alignment="end"> <DataColumn id="action" align="end">
{row => { {(row: any) => {
return ( return (
allowEdit && allowEdit &&
row?.role !== ROLES.teamOwner && row?.role !== ROLES.teamOwner &&
@@ -48,7 +48,7 @@ export function TeamMembersTable({
) )
); );
}} }}
</GridColumn> </DataColumn>
</GridTable> </DataTable>
); );
} }

View File

@@ -4,7 +4,7 @@ import { Icons } from '@/components/icons';
import { PageHeader } from '@/components/layout/PageHeader'; import { PageHeader } from '@/components/layout/PageHeader';
import { ROLES } from '@/lib/constants'; import { ROLES } from '@/lib/constants';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { Flexbox, Item, Tabs } from 'react-basics'; import { Column, Tabs, TabList, Tab, TabPanel } from '@umami/react-zen';
import { TeamLeaveButton } from '@/app/(main)/settings/teams/TeamLeaveButton'; import { TeamLeaveButton } from '@/app/(main)/settings/teams/TeamLeaveButton';
import { TeamManage } from './TeamManage'; import { TeamManage } from './TeamManage';
import { TeamEditForm } from './TeamEditForm'; import { TeamEditForm } from './TeamEditForm';
@@ -26,16 +26,22 @@ export function TeamDetails({ teamId }: { teamId: string }) {
) && user.role !== ROLES.viewOnly; ) && user.role !== ROLES.viewOnly;
return ( return (
<Flexbox direction="column"> <Column>
<PageHeader title={team?.name} icon={<Icons.Users />}> <PageHeader title={team?.name} icon={<Icons.Users />}>
{!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />} {!isTeamOwner && <TeamLeaveButton teamId={team.id} teamName={team.name} />}
</PageHeader> </PageHeader>
<Tabs selectedKey={tab} onSelect={(value: any) => setTab(value)} style={{ marginBottom: 30 }}> <Tabs selectedKey={tab} onSelectionChange={(value: any) => setTab(value)}>
<Item key="details">{formatMessage(labels.details)}</Item> <TabList>
{isTeamOwner && <Item key="manage">{formatMessage(labels.manage)}</Item>} <Tab id="details">{formatMessage(labels.details)}</Tab>
{isTeamOwner && <Tab id="manage">{formatMessage(labels.manage)}</Tab>}
</TabList>
<TabPanel id="details">
<TeamEditForm teamId={teamId} allowEdit={canEdit} />
</TabPanel>
<TabPanel id="manage">
<TeamManage teamId={teamId} />
</TabPanel>
</Tabs> </Tabs>
{tab === 'details' && <TeamEditForm teamId={teamId} allowEdit={canEdit} />} </Column>
{tab === 'manage' && <TeamManage teamId={teamId} />}
</Flexbox>
); );
} }

View File

@@ -1,20 +1,18 @@
import { import {
SubmitButton,
Form, Form,
FormInput, FormField,
FormRow,
FormButtons, FormButtons,
FormSubmitButton,
TextField, TextField,
Button, Button,
Flexbox, useToast,
useToasts, } from '@umami/react-zen';
} from 'react-basics';
import { getRandomChars } from '@/lib/crypto'; import { getRandomChars } from '@/lib/crypto';
import { useContext, useRef, useState } from 'react'; import { useContext, useState } from 'react';
import { useApi, useMessages, useModified } from '@/components/hooks'; import { useApi, useMessages, useModified } from '@/components/hooks';
import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider'; import { TeamContext } from '@/app/(main)/teams/[teamId]/TeamProvider';
const generateId = () => getRandomChars(16); const generateId = () => `team_${getRandomChars(16)}`;
export function TeamEditForm({ teamId, allowEdit }: { teamId: string; allowEdit?: boolean }) { export function TeamEditForm({ teamId, allowEdit }: { teamId: string; allowEdit?: boolean }) {
const team = useContext(TeamContext); const team = useContext(TeamContext);
@@ -23,57 +21,48 @@ export function TeamEditForm({ teamId, allowEdit }: { teamId: string; allowEdit?
const { mutate, error } = useMutation({ const { mutate, error } = useMutation({
mutationFn: (data: any) => post(`/teams/${teamId}`, data), mutationFn: (data: any) => post(`/teams/${teamId}`, data),
}); });
const ref = useRef(null);
const [accessCode, setAccessCode] = useState(team.accessCode); const [accessCode, setAccessCode] = useState(team.accessCode);
const { showToast } = useToasts(); const { toast } = useToast();
const { touch } = useModified(); const { touch } = useModified();
const cloudMode = !!process.env.cloudMode; const cloudMode = !!process.env.cloudMode;
const handleSubmit = async (data: any) => { const handleSubmit = async (data: any) => {
mutate(data, { mutate(data, {
onSuccess: async () => { onSuccess: async () => {
ref.current.reset(data);
touch('teams'); touch('teams');
showToast({ message: formatMessage(messages.saved), variant: 'success' }); toast(formatMessage(messages.saved));
}, },
}); });
}; };
const handleRegenerate = () => { const handleRegenerate = () => {
const code = generateId(); setAccessCode(generateId());
ref.current.setValue('accessCode', code, {
shouldValidate: true,
shouldDirty: true,
});
setAccessCode(code);
}; };
return ( return (
<Form ref={ref} onSubmit={handleSubmit} error={error} values={team}> <Form onSubmit={handleSubmit} error={error} values={{ ...team, accessCode }}>
<FormRow label={formatMessage(labels.teamId)}> <FormField name="id" label={formatMessage(labels.teamId)}>
<TextField value={teamId} readOnly allowCopy /> <TextField isReadOnly allowCopy />
</FormRow> </FormField>
<FormRow label={formatMessage(labels.name)}> <FormField
{allowEdit && ( name="name"
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}> label={formatMessage(labels.name)}
<TextField /> rules={{ required: formatMessage(labels.required) }}
</FormInput> >
)} {allowEdit && <TextField />}
{!allowEdit && team.name} {!allowEdit && team.name}
</FormRow> </FormField>
{!cloudMode && allowEdit && ( {!cloudMode && allowEdit && (
<FormRow label={formatMessage(labels.accessCode)}> <FormField name="accessCode" label={formatMessage(labels.accessCode)}>
<Flexbox gap={10}> <TextField isReadOnly allowCopy />
<TextField value={accessCode} readOnly allowCopy /> </FormField>
{allowEdit && (
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
)}
</Flexbox>
</FormRow>
)} )}
{allowEdit && ( {allowEdit && (
<FormButtons> <FormButtons justifyContent="space-between">
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton> {allowEdit && (
<Button onPress={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
)}
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
</FormButtons> </FormButtons>
)} )}
</Form> </Form>

View File

@@ -1,6 +1,7 @@
import { useMessages, useModified } from '@/components/hooks'; import { useMessages, useModified } from '@/components/hooks';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { ActionForm, Button, Modal, ModalTrigger } from 'react-basics'; import { Button, Modal, DialogTrigger, Dialog } from '@umami/react-zen';
import { ActionForm } from '@/components/layout/ActionForm';
import { TeamDeleteForm } from './TeamDeleteForm'; import { TeamDeleteForm } from './TeamDeleteForm';
export function TeamManage({ teamId }: { teamId: string }) { export function TeamManage({ teamId }: { teamId: string }) {
@@ -18,14 +19,14 @@ export function TeamManage({ teamId }: { teamId: string }) {
label={formatMessage(labels.deleteTeam)} label={formatMessage(labels.deleteTeam)}
description={formatMessage(messages.deleteTeamWarning)} description={formatMessage(messages.deleteTeamWarning)}
> >
<ModalTrigger> <DialogTrigger>
<Button variant="danger">{formatMessage(labels.delete)}</Button> <Button variant="danger">{formatMessage(labels.delete)}</Button>
<Modal title={formatMessage(labels.deleteTeam)}> <Modal>
{(close: () => void) => ( <Dialog title={formatMessage(labels.deleteTeam)}>
<TeamDeleteForm teamId={teamId} onSave={handleLeave} onClose={close} /> {({ close }) => <TeamDeleteForm teamId={teamId} onSave={handleLeave} onClose={close} />}
)} </Dialog>
</Modal> </Modal>
</ModalTrigger> </DialogTrigger>
</ActionForm> </ActionForm>
); );
} }

View File

@@ -1,5 +1,5 @@
import { useApi, useMessages } from '@/components/hooks'; import { useApi, useMessages } from '@/components/hooks';
import { Icon, Icons, LoadingButton, Text } from 'react-basics'; import { Icon, Icons, LoadingButton, Text } from '@umami/react-zen';
export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) { export function TeamWebsiteRemoveButton({ teamId, websiteId, onSave }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();

View File

@@ -1,4 +1,4 @@
import { GridColumn, GridTable, Icon, Text } from 'react-basics'; import { DataColumn, DataTable, Icon, Text } from '@umami/react-zen';
import { useLogin, useMessages } from '@/components/hooks'; import { useLogin, useMessages } from '@/components/hooks';
import { Icons } from '@/components/icons'; import { Icons } from '@/components/icons';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
@@ -16,14 +16,14 @@ export function TeamWebsitesTable({
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
<GridTable data={data}> <DataTable data={data}>
<GridColumn name="name" label={formatMessage(labels.name)} /> <DataColumn id="name" label={formatMessage(labels.name)} />
<GridColumn name="domain" label={formatMessage(labels.domain)} /> <DataColumn id="domain" label={formatMessage(labels.domain)} />
<GridColumn name="createdBy" label={formatMessage(labels.createdBy)}> <DataColumn id="createdBy" label={formatMessage(labels.createdBy)}>
{row => row?.createUser?.username} {(row: any) => row?.createUser?.username}
</GridColumn> </DataColumn>
<GridColumn name="action" label=" " alignment="end"> <DataColumn id="action" label=" " align="end">
{row => { {(row: any) => {
const { id: websiteId } = row; const { id: websiteId } = row;
return ( return (
<> <>
@@ -37,14 +37,14 @@ export function TeamWebsitesTable({
)} )}
<LinkButton href={`/teams/${teamId}/websites/${websiteId}`}> <LinkButton href={`/teams/${teamId}/websites/${websiteId}`}>
<Icon> <Icon>
<Icons.ArrowRight /> <Icons.Arrow />
</Icon> </Icon>
<Text>{formatMessage(labels.view)}</Text> <Text>{formatMessage(labels.view)}</Text>
</LinkButton> </LinkButton>
</> </>
); );
}} }}
</GridColumn> </DataColumn>
</GridTable> </DataTable>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Page } from '@/app/(main)/settings/websites/[websiteId]/page'; import Page from '@/app/(main)/settings/websites/[websiteId]/page';
export default function ({ params }) { export default function ({ params }) {
return <Page params={params} />; return <Page params={params} />;

View File

@@ -1,12 +1,12 @@
import { Button, Text, Icon, Icons } from 'react-basics'; import { Text, Icon, Icons } from '@umami/react-zen';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
import Link from 'next/link';
import { WebsiteChart } from './WebsiteChart'; import { WebsiteChart } from './WebsiteChart';
import { useDashboard } from '@/store/dashboard'; import { useDashboard } from '@/store/dashboard';
import { WebsiteHeader } from './WebsiteHeader'; import { WebsiteHeader } from './WebsiteHeader';
import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { WebsiteMetricsBar } from './WebsiteMetricsBar';
import { useMessages, useLocale, useTeamUrl } from '@/components/hooks'; import { useMessages, useTeamUrl } from '@/components/hooks';
import { LinkButton } from '@/components/common/LinkButton';
export function WebsiteChartList({ export function WebsiteChartList({
websites, websites,
@@ -20,7 +20,6 @@ export function WebsiteChartList({
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { websiteOrder, websiteActive } = useDashboard(); const { websiteOrder, websiteActive } = useDashboard();
const { renderTeamUrl } = useTeamUrl(); const { renderTeamUrl } = useTeamUrl();
const { dir } = useLocale();
const ordered = useMemo(() => { const ordered = useMemo(() => {
return websites return websites
@@ -35,16 +34,14 @@ export function WebsiteChartList({
return index < limit ? ( return index < limit ? (
<div key={id}> <div key={id}>
<WebsiteHeader websiteId={id} showLinks={false}> <WebsiteHeader websiteId={id} showLinks={false}>
<Link href={renderTeamUrl(`/websites/${id}`)}> <LinkButton href={renderTeamUrl(`/websites/${id}`)} variant="primary">
<Button variant="primary"> <Text>{formatMessage(labels.viewDetails)}</Text>
<Text>{formatMessage(labels.viewDetails)}</Text> <Icon>
<Icon> <Icon>
<Icon rotate={dir === 'rtl' ? 180 : 0}> <Icons.Arrow />
<Icons.ArrowRight />
</Icon>
</Icon> </Icon>
</Button> </Icon>
</Link> </LinkButton>
</WebsiteHeader> </WebsiteHeader>
<WebsiteMetricsBar websiteId={id} showChange={true} /> <WebsiteMetricsBar websiteId={id} showChange={true} />
{showCharts && <WebsiteChart websiteId={id} />} {showCharts && <WebsiteChart websiteId={id} />}

View File

@@ -1,64 +0,0 @@
.layout {
display: grid;
grid-template-columns: 300px 1fr;
border-top: 1px solid var(--base300);
}
.menu {
display: flex;
flex-direction: column;
position: relative;
padding: 20px 20px 20px 0;
}
.back {
display: inline-flex;
align-items: center;
align-self: center;
margin-bottom: 20px;
}
.content {
min-height: 800px;
padding: 20px 0 20px 20px;
border-left: 1px solid var(--base300);
}
.dropdown {
display: none;
}
@media screen and (max-width: 992px) {
.layout {
grid-template-columns: 1fr;
}
.content {
border: 0;
}
.back {
align-self: flex-start;
margin: 0;
}
.nav {
display: none;
}
.dropdown {
display: flex;
width: 200px;
align-self: flex-end;
}
.menu {
display: flex;
flex-direction: row;
gap: 20px;
align-items: center;
justify-content: space-between;
padding-inline-end: 0;
z-index: 10;
}
}

View File

@@ -1,6 +1,6 @@
import { Dropdown, Icon, Icons, Item, Text } from 'react-basics'; import { Select, Icon, Icons, ListItem, Text, Grid, Column } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
import { useLocale, useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { MenuNav } from '@/components/layout/MenuNav'; import { MenuNav } from '@/components/layout/MenuNav';
import { BrowsersTable } from '@/components/metrics/BrowsersTable'; import { BrowsersTable } from '@/components/metrics/BrowsersTable';
import { CitiesTable } from '@/components/metrics/CitiesTable'; import { CitiesTable } from '@/components/metrics/CitiesTable';
@@ -17,7 +17,7 @@ import { RegionsTable } from '@/components/metrics/RegionsTable';
import { ScreenTable } from '@/components/metrics/ScreenTable'; import { ScreenTable } from '@/components/metrics/ScreenTable';
import { TagsTable } from '@/components/metrics/TagsTable'; import { TagsTable } from '@/components/metrics/TagsTable';
import { ChannelsTable } from '@/components/metrics/ChannelsTable'; import { ChannelsTable } from '@/components/metrics/ChannelsTable';
import styles from './WebsiteExpandedView.module.css'; import Link from 'next/link';
const views = { const views = {
url: PagesTable, url: PagesTable,
@@ -48,7 +48,6 @@ export function WebsiteExpandedView({
websiteId: string; websiteId: string;
domainName?: string; domainName?: string;
}) { }) {
const { dir } = useLocale();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { const {
router, router,
@@ -136,39 +135,25 @@ export function WebsiteExpandedView({
const DetailsComponent = views[view] || (() => null); const DetailsComponent = views[view] || (() => null);
/*
const handleChange = (view: any) => { const handleChange = (view: any) => {
router.push(renderUrl({ view })); router.push(renderUrl({ view }));
}; };
const renderValue = (value: string) => items.find(({ key }) => key === value)?.label; const renderValue = (value: string) => items.find(({ key }) => key === value)?.label;
*/
return ( return (
<div className={styles.layout}> <Grid columns="auto 1fr" gap="6">
<div className={styles.menu}> <Column gap="6" width="200px">
<LinkButton <LinkButton href={renderUrl({ view: undefined })} variant="quiet" scroll={false}>
href={renderUrl({ view: undefined })} <Icon rotate={180}>
className={styles.back} <Icons.Arrow />
variant="quiet"
scroll={false}
>
<Icon rotate={dir === 'rtl' ? 0 : 180}>
<Icons.ArrowRight />
</Icon> </Icon>
<Text>{formatMessage(labels.back)}</Text> <Text>{formatMessage(labels.back)}</Text>
</LinkButton> </LinkButton>
<MenuNav className={styles.nav} items={items} selectedKey={view} shallow={true} /> <MenuNav items={items} selectedKey={view} />
<Dropdown </Column>
className={styles.dropdown} <Column>
items={items}
value={view}
renderValue={renderValue}
onChange={handleChange}
alignment="end"
>
{({ key, label }) => <Item key={key}>{label}</Item>}
</Dropdown>
</div>
<div className={styles.content}>
<DetailsComponent <DetailsComponent
websiteId={websiteId} websiteId={websiteId}
domainName={domainName} domainName={domainName}
@@ -178,7 +163,7 @@ export function WebsiteExpandedView({
allowFilter={true} allowFilter={true}
allowSearch={true} allowSearch={true}
/> />
</div> </Column>
</div> </Grid>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Button, Icon, Icons, Popup, PopupTrigger, Text } from 'react-basics'; import { Button, Icon, Icons, Box, MenuTrigger, Popover, Text } from '@umami/react-zen';
import { PopupForm } from '@/app/(main)/reports/[reportId]/PopupForm'; import { PopupForm } from '@/app/(main)/reports/[reportId]/PopupForm';
import { FilterSelectForm } from '@/app/(main)/reports/[reportId]/FilterSelectForm'; import { FilterSelectForm } from '@/app/(main)/reports/[reportId]/FilterSelectForm';
import { useFields, useMessages, useNavigation, useDateRange } from '@/components/hooks'; import { useFields, useMessages, useNavigation, useDateRange } from '@/components/hooks';
@@ -7,13 +7,9 @@ import styles from './WebsiteFilterButton.module.css';
export function WebsiteFilterButton({ export function WebsiteFilterButton({
websiteId, websiteId,
className,
position = 'bottom',
alignment = 'end',
showText = true, showText = true,
}: { }: {
websiteId: string; websiteId: string;
className?: string;
position?: 'bottom' | 'top' | 'left' | 'right'; position?: 'bottom' | 'top' | 'left' | 'right';
alignment?: 'end' | 'center' | 'start'; alignment?: 'end' | 'center' | 'start';
showText?: boolean; showText?: boolean;
@@ -32,17 +28,17 @@ export function WebsiteFilterButton({
}; };
return ( return (
<PopupTrigger className={className}> <MenuTrigger>
<Button className={styles.button} variant="quiet"> <Button className={styles.button} variant="quiet">
<Icon> <Icon>
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>
{showText && <Text>{formatMessage(labels.filter)}</Text>} {showText && <Text>{formatMessage(labels.filter)}</Text>}
</Button> </Button>
<Popup position={position} alignment={alignment}> <Popover placement="bottom end">
{(close: () => void) => { {({ close }: any) => {
return ( return (
<PopupForm> <Box padding="3" backgroundColor="1">
<FilterSelectForm <FilterSelectForm
websiteId={websiteId} websiteId={websiteId}
fields={fields} fields={fields}
@@ -53,10 +49,10 @@ export function WebsiteFilterButton({
close(); close();
}} }}
/> />
</PopupForm> </Box>
); );
}} }}
</Popup> </Popover>
</PopupTrigger> </MenuTrigger>
); );
} }

View File

@@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Button, Icon, Text } from 'react-basics'; import { Button, Icon, Text } from '@umami/react-zen';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import classNames from 'classnames'; import classNames from 'classnames';

View File

@@ -1,4 +1,4 @@
import { Dropdown, Item } from 'react-basics'; import { Select, ListItem } from '@umami/react-zen';
import classNames from 'classnames'; import classNames from 'classnames';
import { useDateRange, useMessages, useSticky } from '@/components/hooks'; import { useDateRange, useMessages, useSticky } from '@/components/hooks';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
@@ -114,18 +114,16 @@ export function WebsiteMetricsBar({
{compareMode && ( {compareMode && (
<div className={styles.vs}> <div className={styles.vs}>
<b>VS</b> <b>VS</b>
<Dropdown <Select
className={styles.dropdown} className={styles.dropdown}
items={items} items={items}
value={dateCompare || 'prev'} value={dateCompare || 'prev'}
renderValue={value => items.find(i => i.value === value)?.label}
alignment="end"
onChange={(value: any) => setWebsiteDateCompare(websiteId, value)} onChange={(value: any) => setWebsiteDateCompare(websiteId, value)}
> >
{items.map(({ label, value }) => ( {items.map(({ label, value }) => (
<Item key={value}>{label}</Item> <ListItem key={value}>{label}</ListItem>
))} ))}
</Dropdown> </Select>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { createContext, ReactNode, useEffect } from 'react'; import { createContext, ReactNode, useEffect } from 'react';
import { useModified, useWebsite } from '@/components/hooks'; import { useModified, useWebsite } from '@/components/hooks';
import { Loading } from 'react-basics'; import { Loading } from '@umami/react-zen';
export const WebsiteContext = createContext(null); export const WebsiteContext = createContext(null);

View File

@@ -1,4 +1,4 @@
import { GridColumn, GridTable } from 'react-basics'; import { GridColumn, GridTable } from '@umami/react-zen';
import { useEventDataProperties, useEventDataValues, useMessages } from '@/components/hooks'; import { useEventDataProperties, useEventDataValues, useMessages } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { PieChart } from '@/components/charts/PieChart'; import { PieChart } from '@/components/charts/PieChart';

View File

@@ -4,7 +4,7 @@ import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { MetricCard } from '@/components/metrics/MetricCard'; import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar'; import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatLongNumber } from '@/lib/format'; import { formatLongNumber } from '@/lib/format';
import { Flexbox } from 'react-basics'; import { Flexbox } from '@umami/react-zen';
export function EventsMetricsBar({ websiteId }: { websiteId: string }) { export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();

View File

@@ -6,7 +6,7 @@ import { EventsChart } from '@/components/metrics/EventsChart';
import { GridRow } from '@/components/layout/Grid'; import { GridRow } from '@/components/layout/Grid';
import { MetricsTable } from '@/components/metrics/MetricsTable'; import { MetricsTable } from '@/components/metrics/MetricsTable';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
import { Item, Tabs } from 'react-basics'; import { Item, Tabs } from '@umami/react-zen';
import { useState } from 'react'; import { useState } from 'react';
import { EventProperties } from './EventProperties'; import { EventProperties } from './EventProperties';

View File

@@ -1,4 +1,4 @@
import { GridTable, GridColumn, Icon } from 'react-basics'; import { GridTable, GridColumn, Icon } from '@umami/react-zen';
import { useMessages, useTeamUrl, useTimezone } from '@/components/hooks'; import { useMessages, useTeamUrl, useTimezone } from '@/components/hooks';
import { Empty } from '@/components/common/Empty'; import { Empty } from '@/components/common/Empty';
import { Avatar } from '@/components/common/Avatar'; import { Avatar } from '@/components/common/Avatar';

View File

@@ -7,7 +7,7 @@ import { BROWSERS, OS_NAMES } from '@/lib/constants';
import { stringToColor } from '@/lib/format'; import { stringToColor } from '@/lib/format';
import { RealtimeData } from '@/lib/types'; import { RealtimeData } from '@/lib/types';
import { useContext, useMemo, useState } from 'react'; import { useContext, useMemo, useState } from 'react';
import { Icon, SearchField, StatusLight, Text } from 'react-basics'; import { Icon, SearchField, StatusLight, Text } from '@umami/react-zen';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import { WebsiteContext } from '../WebsiteProvider'; import { WebsiteContext } from '../WebsiteProvider';
import styles from './RealtimeLog.module.css'; import styles from './RealtimeLog.module.css';

Some files were not shown because too many files have changed in this diff Show More