mirror of
https://github.com/umami-software/umami.git
synced 2025-12-06 01:18:00 +01:00
Compare commits
6 commits
33cb195fd0
...
33e927ed1f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33e927ed1f | ||
|
|
2993db14f0 | ||
|
|
1483241494 | ||
|
|
c427c6f547 | ||
|
|
3379cc6e89 | ||
|
|
d7fd22645c |
12 changed files with 72 additions and 52 deletions
29
README.md
29
README.md
|
|
@ -27,10 +27,10 @@ A detailed getting started guide can be found at [umami.is/docs](https://umami.i
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- A server with Node.js version 18.18 or newer
|
- A server with Node.js version 18.18+.
|
||||||
- A database. Umami supports [PostgreSQL](https://www.postgresql.org/) (minimum v12.14) databases.
|
- A PostgreSQL database version v12.14+.
|
||||||
|
|
||||||
### Get the Source Code and Install Packages
|
### Get the source code and install packages
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/umami-software/umami.git
|
git clone https://github.com/umami-software/umami.git
|
||||||
|
|
@ -58,7 +58,7 @@ postgresql://username:mypassword@localhost:5432/mydb
|
||||||
pnpm run build
|
pnpm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
_The build step will create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**._
|
The build step will create tables in your database if you are installing for the first time. It will also create a login user with username **admin** and password **umami**.
|
||||||
|
|
||||||
### Start the Application
|
### Start the Application
|
||||||
|
|
||||||
|
|
@ -66,37 +66,36 @@ _The build step will create tables in your database if you are installing for th
|
||||||
pnpm run start
|
pnpm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
_By default, this will launch the application on `http://localhost:3000`. You will need to either [proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly._
|
By default, this will launch the application on `http://localhost:3000`. You will need to either [proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐳 Installing with Docker
|
## 🐳 Installing with Docker
|
||||||
|
|
||||||
To build the Umami container and start up a Postgres database, run:
|
Umami provides Docker images as well as a Docker compose file for easy deployment.
|
||||||
|
|
||||||
```bash
|
Docker image:
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, to pull just the Umami Docker image with PostgreSQL support:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull docker.umami.is/umami-software/umami:latest
|
docker pull docker.umami.is/umami-software/umami:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Docker compose to run Umami with a Postgres database, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔄 Getting Updates
|
## 🔄 Getting Updates
|
||||||
> [!WARNING]
|
|
||||||
> If you are updating from Umami V2, image "postgresql-latest" is deprecated. You must change it to "latest".
|
|
||||||
> e.g., rename `docker.umami.is/umami-software/umami:postgresql-latest` to `docker.umami.is/umami-software/umami:latest`.
|
|
||||||
|
|
||||||
To get the latest features, simply do a pull, install any new dependencies, and rebuild:
|
To get the latest features, simply do a pull, install any new dependencies, and rebuild:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull
|
git pull
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
To update the Docker image, simply pull the new images and rebuild:
|
To update the Docker image, simply pull the new images and rebuild:
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,14 @@
|
||||||
"arrowParentheses": "asNeeded"
|
"arrowParentheses": "asNeeded"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"css": {
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineEnding": "lf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"assist": {
|
"assist": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"actions": {
|
"actions": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"description": "A modern, privacy-focused alternative to Google Analytics.",
|
"description": "A modern, privacy-focused alternative to Google Analytics.",
|
||||||
"author": "Umami Software, Inc. <hello@umami.is>",
|
"author": "Umami Software, Inc. <hello@umami.is>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export function AdminLayout({ children }: { children: ReactNode }) {
|
||||||
border="right"
|
border="right"
|
||||||
backgroundColor
|
backgroundColor
|
||||||
marginRight="2"
|
marginRight="2"
|
||||||
|
padding="3"
|
||||||
>
|
>
|
||||||
<AdminNav />
|
<AdminNav />
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
||||||
label={formatMessage(labels.role)}
|
label={formatMessage(labels.role)}
|
||||||
rules={{ required: formatMessage(labels.required) }}
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
>
|
>
|
||||||
<Select defaultSelectedKey={user.role}>
|
<Select defaultValue={user.role}>
|
||||||
<ListItem id={ROLES.viewOnly} data-test="dropdown-item-viewOnly">
|
<ListItem id={ROLES.viewOnly} data-test="dropdown-item-viewOnly">
|
||||||
{formatMessage(labels.viewOnly)}
|
{formatMessage(labels.viewOnly)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
|
||||||
border="right"
|
border="right"
|
||||||
backgroundColor
|
backgroundColor
|
||||||
marginRight="2"
|
marginRight="2"
|
||||||
|
padding="3"
|
||||||
>
|
>
|
||||||
<SettingsNav />
|
<SettingsNav />
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { SettingsLayout } from './SettingsLayout';
|
||||||
|
|
||||||
export default function ({ children }) {
|
export default function ({ children }) {
|
||||||
if (process.env.cloudMode) {
|
if (process.env.cloudMode) {
|
||||||
//return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SettingsLayout>{children}</SettingsLayout>;
|
return <SettingsLayout>{children}</SettingsLayout>;
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export function SideMenu({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap overflowY="auto" justifyContent="space-between">
|
<Column gap overflowY="auto" justifyContent="space-between" position="sticky" top="20px">
|
||||||
{title && (
|
{title && (
|
||||||
<Row padding>
|
<Row padding>
|
||||||
<Heading size="1">{title}</Heading>
|
<Heading size="1">{title}</Heading>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useToast } from '@umami/react-zen';
|
import { useToast } from '@umami/react-zen';
|
||||||
|
import type { ApiError } from '@/lib/types';
|
||||||
import { useApi } from '../useApi';
|
import { useApi } from '../useApi';
|
||||||
import { useModified } from '../useModified';
|
import { useModified } from '../useModified';
|
||||||
|
|
||||||
export function useUpdateQuery(path: string, params?: Record<string, any>) {
|
export function useUpdateQuery(path: string, params?: Record<string, any>) {
|
||||||
const { post, useMutation } = useApi();
|
const { post, useMutation } = useApi();
|
||||||
const query = useMutation({
|
const query = useMutation<any, ApiError, Record<string, any>>({
|
||||||
mutationFn: (data: Record<string, any>) => post(path, { ...data, ...params }),
|
mutationFn: (data: Record<string, any>) => post(path, { ...data, ...params }),
|
||||||
});
|
});
|
||||||
const { touch } = useModified();
|
const { touch } = useModified();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { FormattedMessage, type MessageDescriptor, useIntl } from 'react-intl';
|
import { FormattedMessage, type MessageDescriptor, useIntl } from 'react-intl';
|
||||||
import { labels, messages } from '@/components/messages';
|
import { labels, messages } from '@/components/messages';
|
||||||
|
import type { ApiError } from '@/lib/types';
|
||||||
|
|
||||||
type FormatMessage = (
|
type FormatMessage = (
|
||||||
descriptor: MessageDescriptor,
|
descriptor: MessageDescriptor,
|
||||||
|
|
@ -12,7 +13,7 @@ interface UseMessages {
|
||||||
messages: typeof messages;
|
messages: typeof messages;
|
||||||
labels: typeof labels;
|
labels: typeof labels;
|
||||||
getMessage: (id: string) => string;
|
getMessage: (id: string) => string;
|
||||||
getErrorMessage: (error: unknown) => string | undefined;
|
getErrorMessage: (error: ApiError) => string | undefined;
|
||||||
FormattedMessage: typeof FormattedMessage;
|
FormattedMessage: typeof FormattedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,7 +26,7 @@ export function useMessages(): UseMessages {
|
||||||
return message ? formatMessage(message) : id;
|
return message ? formatMessage(message) : id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown) => {
|
const getErrorMessage = (error: ApiError) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,10 @@ async function rawQuery(sql: string, data: Record<string, any>, name?: string):
|
||||||
return `$${params.length}${type ?? ''}`;
|
return `$${params.length}${type ?? ''}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (process.env.DATABASE_REPLICA_URL && '$replica' in client) {
|
||||||
|
return client.$replica().$queryRawUnsafe(query, ...params);
|
||||||
|
}
|
||||||
|
|
||||||
return client.$queryRawUnsafe(query, ...params);
|
return client.$queryRawUnsafe(query, ...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,54 +300,54 @@ function getSchema() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClient() {
|
function getClient() {
|
||||||
if (!process.env.DATABASE_URL) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = process.env.DATABASE_URL;
|
const url = process.env.DATABASE_URL;
|
||||||
const replicaUrl = process.env.DATABASE_REPLICA_URL;
|
const replicaUrl = process.env.DATABASE_REPLICA_URL;
|
||||||
const logQuery = process.env.LOG_QUERY;
|
const logQuery = process.env.LOG_QUERY;
|
||||||
|
const schema = getSchema();
|
||||||
|
|
||||||
const connectionUrl = new URL(url);
|
const baseAdapter = new PrismaPg({ connectionString: url }, { schema });
|
||||||
const schema = connectionUrl.searchParams.get('schema') ?? undefined;
|
|
||||||
|
|
||||||
const adapter = new PrismaPg({ connectionString: url.toString() }, { schema });
|
const baseClient = new PrismaClient({
|
||||||
|
adapter: baseAdapter,
|
||||||
const prisma = new PrismaClient({
|
|
||||||
adapter,
|
|
||||||
errorFormat: 'pretty',
|
errorFormat: 'pretty',
|
||||||
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (replicaUrl) {
|
if (logQuery) {
|
||||||
const replicaAdapter = new PrismaPg({ connectionString: replicaUrl.toString() }, { schema });
|
baseClient.$on('query', log);
|
||||||
|
|
||||||
const replicaClient = new PrismaClient({
|
|
||||||
adapter: replicaAdapter,
|
|
||||||
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
prisma.$extends(
|
|
||||||
readReplicas({
|
|
||||||
replicas: [replicaClient],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!replicaUrl) {
|
||||||
|
log('Prisma initialized');
|
||||||
|
globalThis[PRISMA] ??= baseClient;
|
||||||
|
return baseClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replicaAdapter = new PrismaPg({ connectionString: replicaUrl }, { schema });
|
||||||
|
|
||||||
|
const replicaClient = new PrismaClient({
|
||||||
|
adapter: replicaAdapter,
|
||||||
|
errorFormat: 'pretty',
|
||||||
|
...(logQuery ? PRISMA_LOG_OPTIONS : {}),
|
||||||
|
});
|
||||||
|
|
||||||
if (logQuery) {
|
if (logQuery) {
|
||||||
prisma.$on('query' as never, log);
|
replicaClient.$on('query', log);
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Prisma initialized');
|
const extended = baseClient.$extends(
|
||||||
|
readReplicas({
|
||||||
|
replicas: [replicaClient],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (!globalThis[PRISMA]) {
|
log('Prisma initialized (with replica)');
|
||||||
globalThis[PRISMA] = prisma;
|
globalThis[PRISMA] ??= extended;
|
||||||
}
|
|
||||||
|
|
||||||
return prisma;
|
return extended;
|
||||||
}
|
}
|
||||||
|
|
||||||
const client: PrismaClient = globalThis[PRISMA] || getClient();
|
const client = (globalThis[PRISMA] || getClient()) as ReturnType<typeof getClient>;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
client,
|
client,
|
||||||
|
|
|
||||||
|
|
@ -136,3 +136,8 @@ export interface RealtimeData {
|
||||||
urls: Record<string, number>;
|
urls: Record<string, number>;
|
||||||
visitors: any[];
|
visitors: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiError extends Error {
|
||||||
|
code?: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue