Merge branch 'dev' of https://github.com/umami-software/umami into francis/uc-24-kafka-test

This commit is contained in:
Francis Cao 2022-08-04 17:48:00 -07:00
commit 1ffd5775f8
138 changed files with 2849 additions and 239 deletions

View File

@ -1 +0,0 @@
web: npm run start-env

View File

@ -6,10 +6,15 @@ import { setItem } from 'lib/web';
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
import Button from './Button';
import styles from './UpdateNotice.module.css';
import useUser from 'hooks/useUser';
import useConfig from 'hooks/useConfig';
export default function UpdateNotice() {
const { user } = useUser();
const { updatesDisabled } = useConfig();
const { latest, checked, hasUpdate, releaseUrl } = useStore();
const [dismissed, setDismissed] = useState(false);
const allowCheck = user?.is_admin && !updatesDisabled;
const updateCheck = useCallback(() => {
setItem(VERSION_CHECK, { version: latest, time: Date.now() });
@ -27,12 +32,12 @@ export default function UpdateNotice() {
}
useEffect(() => {
if (!checked) {
if (!checked && allowCheck) {
checkVersion();
}
}, []);
}, [checked]);
if (!hasUpdate || dismissed) {
if (!hasUpdate || dismissed || !allowCheck) {
return null;
}

View File

@ -1,4 +1,5 @@
import React from 'react';
import { useRouter } from 'next/router';
import Script from 'next/script';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link';
@ -8,6 +9,7 @@ import { HOMEPAGE_URL, REPO_URL } from 'lib/constants';
export default function Footer() {
const { current } = useStore();
const { pathname } = useRouter();
return (
<footer className={classNames(styles.footer, 'row')}>
@ -28,9 +30,7 @@ export default function Footer() {
<div className={classNames(styles.version, 'col-12 col-md-4')}>
<Link href={REPO_URL}>{`v${current}`}</Link>
</div>
{!process.env.telemetryDisabled && (
<img src={`https://i.umami.is/a.png?v=${current}`} alt="" />
)}
{!pathname.includes('/share/') && <Script src={`/telemetry.js?v=${current}`} />}
</footer>
);
}

View File

@ -8,10 +8,10 @@ import ThemeButton from 'components/settings/ThemeButton';
import HamburgerButton from 'components/common/HamburgerButton';
import UpdateNotice from 'components/common/UpdateNotice';
import UserButton from 'components/settings/UserButton';
import Logo from 'assets/logo.svg';
import styles from './Header.module.css';
import useUser from 'hooks/useUser';
import { HOMEPAGE_URL } from 'lib/constants';
import Logo from 'assets/logo.svg';
import styles from './Header.module.css';
export default function Header() {
const { user } = useUser();
@ -19,7 +19,7 @@ export default function Header() {
return (
<>
{user?.is_admin && !process.env.updatesDisabled && <UpdateNotice />}
<UpdateNotice />
<header className={classNames(styles.header, 'row')}>
<div className={styles.title}>
<Icon icon={<Logo />} size="large" className={styles.logo} />

View File

@ -12,7 +12,7 @@ export default function EventsChart({ websiteId, className, token }) {
const [{ startDate, endDate, unit, modified }] = useDateRange(websiteId);
const [timezone] = useTimezone();
const {
query: { url, eventType },
query: { url, eventName },
} = usePageQuery();
const { data, loading } = useFetch(
@ -24,11 +24,11 @@ export default function EventsChart({ websiteId, className, token }) {
unit,
tz: timezone,
url,
event_type: eventType,
event_name: eventName,
token,
},
},
[modified, eventType],
[modified, eventName],
);
const datasets = useMemo(() => {

View File

@ -92,8 +92,7 @@ export default function RealtimeLog({ data, websites, websiteId }) {
}
function getDetail({
event_type,
event_value,
event_name,
view_id,
session_id,
url,
@ -103,10 +102,10 @@ export default function RealtimeLog({ data, websites, websiteId }) {
device,
website_id,
}) {
if (event_type) {
if (event_name) {
return (
<div>
<Tag>{event_type}</Tag> {event_value}
<Tag>{event_name}</Tag>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { useRouter } from 'next/router';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
@ -7,19 +7,24 @@ import WebsiteList from 'components/pages/WebsiteList';
import Button from 'components/common/Button';
import DashboardSettingsButton from 'components/settings/DashboardSettingsButton';
import useFetch from 'hooks/useFetch';
import useStore from 'store/app';
import useDashboard from 'store/dashboard';
import DashboardEdit from './DashboardEdit';
import styles from './WebsiteList.module.css';
const selector = state => state.dashboard;
const messages = defineMessages({
dashboard: { id: 'label.dashboard', defaultMessage: 'Dashboard' },
more: { id: 'label.more', defaultMessage: 'More' },
});
export default function Dashboard() {
const router = useRouter();
const { id } = router.query;
const userId = id?.[0];
const store = useStore(selector);
const { showCharts, limit } = store;
const dashboard = useDashboard();
const { showCharts, limit, editing } = dashboard;
const [max, setMax] = useState(limit);
const { data } = useFetch('/websites', { params: { user_id: userId } });
const { formatMessage } = useIntl();
function handleMore() {
setMax(max + limit);
@ -32,15 +37,14 @@ export default function Dashboard() {
return (
<Page>
<PageHeader>
<div>
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
</div>
<DashboardSettingsButton />
<div>{formatMessage(messages.dashboard)}</div>
{!editing && <DashboardSettingsButton />}
</PageHeader>
<WebsiteList websites={data} showCharts={showCharts} limit={max} />
{editing && <DashboardEdit data={data} />}
{!editing && <WebsiteList data={data} showCharts={showCharts} limit={max} />}
{max < data.length && (
<Button className={styles.button} onClick={handleMore}>
<FormattedMessage id="label.more" defaultMessage="More" />
{formatMessage(messages.more)}
</Button>
)}
</Page>

View File

@ -0,0 +1,104 @@
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import useDashboard, { saveDashboard } from 'store/dashboard';
import Button from 'components/common/Button';
import { useMemo } from 'react';
import { orderByWebsiteMap } from 'lib/format';
import styles from './DashboardEdit.module.css';
import classNames from 'classnames';
const messages = defineMessages({
save: { id: 'label.save', defaultMessage: 'Save' },
reset: { id: 'label.reset', defaultMessage: 'Reset' },
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
});
const dragId = 'dashboard-website-ordering';
export default function DashboardEdit({ data: websites }) {
const settings = useDashboard();
const { websiteOrder } = settings;
const { formatMessage } = useIntl();
const [order, setOrder] = useState(websiteOrder);
const ordered = useMemo(() => orderByWebsiteMap(websites, order), [websites, order]);
console.log({ order, ordered });
function handleWebsiteDrag({ destination, source }) {
if (!destination || destination.index === source.index) return;
const orderedWebsites = [...ordered];
const [removed] = orderedWebsites.splice(source.index, 1);
orderedWebsites.splice(destination.index, 0, removed);
setOrder(
orderedWebsites.map((i, k) => ({ [i.website_uuid]: k })).reduce((a, b) => ({ ...a, ...b })),
);
}
function handleSave() {
saveDashboard({
editing: false,
websiteOrder: order,
});
}
function handleCancel() {
saveDashboard({ editing: false, websiteOrder });
}
function handleReset() {
setOrder({});
}
return (
<>
<div className={styles.buttons}>
<Button onClick={handleSave} variant="action" size="small">
{formatMessage(messages.save)}
</Button>
<Button onClick={handleCancel} size="small">
{formatMessage(messages.cancel)}
</Button>
<Button onClick={handleReset} size="small">
{formatMessage(messages.reset)}
</Button>
</div>
<div className={styles.dragActive}>
<DragDropContext onDragEnd={handleWebsiteDrag}>
<Droppable droppableId={dragId}>
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
style={{ marginBottom: snapshot.isDraggingOver ? 260 : null }}
>
{ordered.map(({ website_id, name, domain }, index) => (
<Draggable key={website_id} draggableId={`${dragId}-${website_id}`} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
className={classNames(styles.item, {
[styles.active]: snapshot.isDragging,
})}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className={styles.text}>
<h1>{name}</h1>
<h2>{domain}</h2>
</div>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</>
);
}

View File

@ -0,0 +1,40 @@
.buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 20px;
}
.item {
padding: 5px 0;
}
.item h1 {
font-weight: 600;
font-size: 16px;
}
.item h2 {
font-size: 14px;
color: var(--gray700);
}
.text {
padding: 20px;
border-radius: 5px;
border: 1px solid var(--gray400);
background: var(--gray50);
}
.active .text {
border-color: var(--gray600);
box-shadow: 4px 4px 4px var(--gray100);
}
.dragActive {
cursor: grab;
}
.dragActive:active {
cursor: grabbing;
}

View File

@ -37,7 +37,8 @@ export default function TestConsole() {
function handleClick() {
window.umami('event (default)');
window.umami.trackView('/page-view', 'https://www.google.com');
window.umami.trackEvent('event (custom)', 'custom-type');
window.umami.trackEvent('event (custom)', null, 'custom-type');
window.umami.trackEvent('event (custom)', { test: 'test-data' }, 'custom-data-type');
}
return (

View File

@ -1,25 +1,37 @@
import { FormattedMessage } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import Link from 'components/common/Link';
import WebsiteChart from 'components/metrics/WebsiteChart';
import Page from 'components/layout/Page';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteList.module.css';
import useDashboard from 'store/dashboard';
import { orderByWebsiteMap } from 'lib/format';
import { useMemo } from 'react';
const messages = defineMessages({
noWebsites: {
id: 'message.no-websites-configured',
defaultMessage: "You don't have any websites configured.",
},
goToSettngs: {
id: 'message.go-to-settings',
defaultMessage: 'Go to settings',
},
});
export default function WebsiteList({ data, showCharts, limit }) {
const { websiteOrder } = useDashboard();
const { formatMessage } = useIntl();
const websites = useMemo(() => orderByWebsiteMap(data, websiteOrder), [data, websiteOrder]);
export default function WebsiteList({ websites, showCharts, limit }) {
if (websites.length === 0) {
return (
<Page>
<EmptyPlaceholder
msg={
<FormattedMessage
id="message.no-websites-configured"
defaultMessage="You don't have any websites configured."
/>
}
>
<EmptyPlaceholder msg={formatMessage(messages.noWebsites)}>
<Link href="/settings" icon={<Arrow />} iconRight>
<FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
{formatMessage(messages.goToSettngs)}
</Link>
</EmptyPlaceholder>
</Page>

View File

@ -1,26 +1,39 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import MenuButton from 'components/common/MenuButton';
import Gear from 'assets/gear.svg';
import useStore, { setDashboard, defaultDashboardConfig } from 'store/app';
import { saveDashboard } from 'store/dashboard';
const selector = state => state.dashboard;
const messages = defineMessages({
toggleCharts: { id: 'message.toggle-charts', defaultMessage: 'Toggle charts' },
editDashboard: { id: 'message.edit-dashboard', defaultMessage: 'Edit dashboard' },
});
export default function DashboardSettingsButton() {
const settings = useStore(selector);
const { formatMessage } = useIntl();
const menuOptions = [
{
label: <FormattedMessage id="message.toggle-charts" defaultMessage="Toggle charts" />,
label: formatMessage(messages.toggleCharts),
value: 'charts',
},
{
label: formatMessage(messages.editDashboard),
value: 'order',
},
];
function handleSelect(value) {
if (value === 'charts') {
setDashboard({ ...defaultDashboardConfig, showCharts: !settings.showCharts });
saveDashboard(state => {
const bs = { showCharts: !state.showCharts };
console.log('WTF', { state, bs });
return bs;
});
}
if (value === 'order') {
saveDashboard({ editing: true });
}
//setDashboard(value);
}
return <MenuButton icon={<Gear />} options={menuOptions} onSelect={handleSelect} hideLabel />;

View File

@ -0,0 +1,5 @@
.buttonGroup {
display: flex;
place-items: center;
gap: 10px;
}

View File

@ -36,6 +36,7 @@ export default function WebsiteSettings() {
const [showUrl, setShowUrl] = useState();
const [saved, setSaved] = useState(0);
const [message, setMessage] = useState();
const { data } = useFetch('/websites', { params: { include_all: !!user?.is_admin } }, [saved]);
const Buttons = row => (

View File

@ -0,0 +1,62 @@
-- DropForeignKey
ALTER TABLE `event` DROP FOREIGN KEY `event_ibfk_1`;
ALTER TABLE `event` DROP FOREIGN KEY `event_ibfk_2`;
DROP INDEX `event_created_at_idx` ON `event`;
DROP INDEX `event_session_id_idx` ON `event`;
DROP INDEX `event_website_id_idx` ON `event`;
CREATE INDEX `event_old_created_at_idx` ON `event` (created_at);
CREATE INDEX `event_old_session_id_idx` ON `event` (session_id);
CREATE INDEX `event_old_website_id_idx` ON `event` (website_id);
-- RenameTable
RENAME TABLE `event` TO `_event_old`;
-- CreateTable
CREATE TABLE `event`
(
`event_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`website_id` INTEGER UNSIGNED NOT NULL,
`session_id` INTEGER UNSIGNED NOT NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`url` VARCHAR(500) NOT NULL,
`event_name` VARCHAR(50) NOT NULL,
INDEX `event_created_at_idx`(`created_at`),
INDEX `event_session_id_idx`(`session_id`),
INDEX `event_website_id_idx`(`website_id`),
PRIMARY KEY (`event_id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `event` ADD CONSTRAINT `event_ibfk_2` FOREIGN KEY (`session_id`) REFERENCES `session`(`session_id`) ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE `event` ADD CONSTRAINT `event_ibfk_1` FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE CASCADE ON UPDATE NO ACTION;
-- CreateTable
CREATE TABLE `event_data` (
`event_data_id` INTEGER NOT NULL AUTO_INCREMENT,
`event_id` INTEGER UNSIGNED NOT NULL,
`event_data` JSON NOT NULL,
UNIQUE INDEX `event_data_event_id_key`(`event_id`),
PRIMARY KEY (`event_data_id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `event_data` ADD CONSTRAINT `event_data_event_id_fkey` FOREIGN KEY (`event_id`) REFERENCES `event`(`event_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- RenameIndex
ALTER TABLE `account` RENAME INDEX `username` TO `account_username_key`;
-- RenameIndex
ALTER TABLE `session` RENAME INDEX `session_uuid` TO `session_session_uuid_key`;
-- RenameIndex
ALTER TABLE `website` RENAME INDEX `share_id` TO `website_share_id_key`;
-- RenameIndex
ALTER TABLE `website` RENAME INDEX `website_uuid` TO `website_website_uuid_key`;

View File

@ -9,7 +9,7 @@ datasource db {
model account {
user_id Int @id @default(autoincrement()) @db.UnsignedInt
username String @unique(map: "username") @db.VarChar(255)
username String @unique() @db.VarChar(255)
password String @db.VarChar(60)
is_admin Boolean @default(false)
created_at DateTime? @default(now()) @db.Timestamp(0)
@ -18,21 +18,28 @@ model account {
}
model event {
event_id Int @id @default(autoincrement()) @db.UnsignedInt
website_id Int @db.UnsignedInt
session_id Int @db.UnsignedInt
created_at DateTime? @default(now()) @db.Timestamp(0)
url String @db.VarChar(500)
event_type String @db.VarChar(50)
event_value String @db.VarChar(50)
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade, onUpdate: NoAction, map: "event_ibfk_2")
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade, onUpdate: NoAction, map: "event_ibfk_1")
event_id Int @id @default(autoincrement()) @db.UnsignedInt
website_id Int @db.UnsignedInt
session_id Int @db.UnsignedInt
created_at DateTime? @default(now()) @db.Timestamp(0)
url String @db.VarChar(500)
event_name String @db.VarChar(50)
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade, onUpdate: NoAction, map: "event_ibfk_2")
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade, onUpdate: NoAction, map: "event_ibfk_1")
event_data event_data?
@@index([created_at])
@@index([session_id])
@@index([website_id])
}
model event_data {
event_data_id Int @id @default(autoincrement())
event_id Int @unique @db.UnsignedInt
event_data Json
event event @relation(fields: [event_id], references: [event_id])
}
model pageview {
view_id Int @id @default(autoincrement()) @db.UnsignedInt
website_id Int @db.UnsignedInt
@ -52,7 +59,7 @@ model pageview {
model session {
session_id Int @id @default(autoincrement()) @db.UnsignedInt
session_uuid String @unique(map: "session_uuid") @db.VarChar(36)
session_uuid String @unique() @db.VarChar(36)
website_id Int @db.UnsignedInt
created_at DateTime? @default(now()) @db.Timestamp(0)
hostname String? @db.VarChar(100)
@ -72,11 +79,11 @@ model session {
model website {
website_id Int @id @default(autoincrement()) @db.UnsignedInt
website_uuid String @unique(map: "website_uuid") @db.VarChar(36)
website_uuid String @unique() @db.VarChar(36)
user_id Int @db.UnsignedInt
name String @db.VarChar(100)
domain String? @db.VarChar(500)
share_id String? @unique(map: "share_id") @db.VarChar(64)
share_id String? @unique() @db.VarChar(64)
created_at DateTime? @default(now()) @db.Timestamp(0)
account account @relation(fields: [user_id], references: [user_id], onDelete: Cascade, onUpdate: NoAction, map: "website_ibfk_1")
event event[]

View File

@ -0,0 +1,66 @@
-- DropForeignKey
ALTER TABLE "event" DROP CONSTRAINT "event_session_id_fkey";
ALTER TABLE "event" DROP CONSTRAINT "event_website_id_fkey";
-- RenameIndex
ALTER INDEX "event_pkey" RENAME TO "event_old_pkey";
ALTER INDEX "event_created_at_idx" RENAME TO "event_old_created_at_idx";
ALTER INDEX "event_session_id_idx" RENAME TO "event_old_session_id_idx";
ALTER INDEX "event_website_id_idx" RENAME TO "event_old_website_id_idx";
-- RenameTable
ALTER TABLE "event" RENAME TO "_event_old";
-- CreateTable
CREATE TABLE "event" (
"event_id" SERIAL NOT NULL,
"website_id" INTEGER NOT NULL,
"session_id" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"url" VARCHAR(500) NOT NULL,
"event_name" VARCHAR(50) NOT NULL,
PRIMARY KEY ("event_id")
);
-- CreateIndex
CREATE INDEX "event_created_at_idx" ON "event"("created_at");
-- CreateIndex
CREATE INDEX "event_session_id_idx" ON "event"("session_id");
-- CreateIndex
CREATE INDEX "event_website_id_idx" ON "event"("website_id");
-- AddForeignKey
ALTER TABLE "event" ADD CONSTRAINT "event_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "event" ADD CONSTRAINT "event_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- CreateTable
CREATE TABLE "event_data" (
"event_data_id" SERIAL NOT NULL,
"event_id" INTEGER NOT NULL,
"event_data" JSONB NOT NULL,
CONSTRAINT "event_data_pkey" PRIMARY KEY ("event_data_id")
);
-- CreateIndex
CREATE UNIQUE INDEX "event_data_event_id_key" ON "event_data"("event_id");
-- AddForeignKey
ALTER TABLE "event_data" ADD CONSTRAINT "event_data_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "event"("event_id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- RenameIndex
ALTER INDEX "account.username_unique" RENAME TO "account_username_key";
-- RenameIndex
ALTER INDEX "session.session_uuid_unique" RENAME TO "session_session_uuid_key";
-- RenameIndex
ALTER INDEX "website.share_id_unique" RENAME TO "website_share_id_key";
-- RenameIndex
ALTER INDEX "website.website_uuid_unique" RENAME TO "website_website_uuid_key";

View File

@ -18,21 +18,28 @@ model account {
}
model event {
event_id Int @id @default(autoincrement())
website_id Int
session_id Int
created_at DateTime? @default(now()) @db.Timestamptz(6)
url String @db.VarChar(500)
event_type String @db.VarChar(50)
event_value String @db.VarChar(50)
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade, onUpdate: NoAction)
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade, onUpdate: NoAction)
event_id Int @id @default(autoincrement())
website_id Int
session_id Int
created_at DateTime? @default(now()) @db.Timestamptz(6)
url String @db.VarChar(500)
event_name String @db.VarChar(50)
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade)
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade)
event_data event_data?
@@index([created_at])
@@index([session_id])
@@index([website_id])
}
model event_data {
event_data_id Int @id @default(autoincrement())
event_id Int @unique
event_data Json
event event @relation(fields: [event_id], references: [event_id])
}
model pageview {
view_id Int @id @default(autoincrement())
website_id Int
@ -40,8 +47,8 @@ model pageview {
created_at DateTime? @default(now()) @db.Timestamptz(6)
url String @db.VarChar(500)
referrer String? @db.VarChar(500)
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade, onUpdate: NoAction)
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade, onUpdate: NoAction)
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade)
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade)
@@index([created_at])
@@index([session_id])
@ -58,13 +65,13 @@ model session {
hostname String? @db.VarChar(100)
browser String? @db.VarChar(20)
os String? @db.VarChar(20)
device String? @db.VarChar(20)
screen String? @db.VarChar(11)
language String? @db.VarChar(35)
country String? @db.Char(2)
device String? @db.VarChar(20)
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade, onUpdate: NoAction)
event event[]
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade)
pageview pageview[]
event event[]
@@index([created_at])
@@index([website_id])
@ -73,15 +80,15 @@ model session {
model website {
website_id Int @id @default(autoincrement())
website_uuid String @unique @db.Uuid
name String @db.VarChar(100)
created_at DateTime? @default(now()) @db.Timestamptz(6)
user_id Int
name String @db.VarChar(100)
domain String? @db.VarChar(500)
share_id String? @unique(map: "website_share_id_idx") @db.VarChar(64)
account account @relation(fields: [user_id], references: [user_id], onDelete: NoAction, onUpdate: NoAction)
event event[]
share_id String? @unique @db.VarChar(64)
created_at DateTime? @default(now()) @db.Timestamptz(6)
account account @relation(fields: [user_id], references: [user_id], onDelete: Cascade)
pageview pageview[]
session session[]
event event[]
@@index([user_id])
}

View File

@ -6,7 +6,7 @@ import useStore from 'store/app';
const selector = state => state.shareToken;
function parseHeaders(headers = {}, { authToken, shareToken }) {
function parseHeaders(headers, { authToken, shareToken }) {
if (authToken) {
headers.authorization = `Bearer ${authToken}`;
}
@ -25,7 +25,7 @@ export default function useApi() {
return {
get: useCallback(
async (url, params, headers) => {
async (url, params = {}, headers = {}) => {
return get(
`${basePath}/api${url}`,
params,
@ -36,7 +36,7 @@ export default function useApi() {
),
post: useCallback(
async (url, params, headers) => {
async (url, params = {}, headers = {}) => {
return post(
`${basePath}/api${url}`,
params,
@ -47,7 +47,7 @@ export default function useApi() {
),
put: useCallback(
async (url, params, headers) => {
async (url, params = {}, headers = {}) => {
return put(
`${basePath}/api${url}`,
params,
@ -58,7 +58,7 @@ export default function useApi() {
),
del: useCallback(
async (url, params, headers) => {
async (url, params = {}, headers = {}) => {
return del(
`${basePath}/api${url}`,
params,

24
hooks/useConfig.js Normal file
View File

@ -0,0 +1,24 @@
import { useEffect } from 'react';
import useStore, { setConfig } from 'store/app';
import useApi from 'hooks/useApi';
let fetched = false;
export default function useConfig() {
const { config } = useStore();
const { get } = useApi();
async function loadConfig() {
const { data } = await get('/config');
setConfig(data);
}
useEffect(() => {
if (!config && !fetched) {
fetched = true;
loadConfig();
}
}, []);
return config || {};
}

View File

@ -8,7 +8,9 @@
"metrics.device.desktop",
"metrics.device.laptop",
"metrics.device.tablet",
"metrics.referrers"
"metrics.referrers",
"metrics.utm",
"metrics.utm_medium"
],
"en-GB": "*",
"fr-FR": ["metrics.actions", "metrics.pages"],

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "هل أنت متأكد من اعادة تعيين الإحصائيات لـ {target}؟",
"message.copied": "تم النسخ!",
"message.delete-warning": "كافة البيانات المرتبطة سيم حذفها ايضا.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "حدث خطأ ما.",
"message.get-share-url": "احصل على رابط المشاركة",
"message.get-tracking-code": "احصل على كود التتبع",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "আপনি কি নিশ্চিত যে আপনি {target} এর পরিসংখ্যান পুনরায় সেট করতে চান?",
"message.copied": "কপি হয়েছে",
"message.delete-warning": "সমস্ত সম্পর্কিত ডেটা পাশাপাশি মুছে ফেলা হবে।",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "কিছু ভুল হয়েছে।",
"message.get-share-url": "শেয়ার ইউআরএল",
"message.get-tracking-code": "ট্র্যাকিং কোড পান",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Segur que vols restablir les estadístiques de {target}?",
"message.copied": "S'ha copiat",
"message.delete-warning": "També s'esborraran totes les dades relacionades.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "S'ha produït un error.",
"message.get-share-url": "Obté l'enllaç per compartir",
"message.get-tracking-code": "Obté el codi de seguiment",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Zkopírováno!",
"message.delete-warning": "Všechna související data budou také smazána.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Něco se pokazilo.",
"message.get-share-url": "Získat sdílené URL",
"message.get-tracking-code": "Získat měřící kód",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Er du sikker på at du ville nulstille {target}'s statistikker?",
"message.copied": "Kopieret!",
"message.delete-warning": "Alle tilknyttede data slettes også.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Noget gik galt.",
"message.get-share-url": "Få delings-URL",
"message.get-tracking-code": "Få sporingskode",

View File

@ -37,7 +37,7 @@
"label.more": "Mehr",
"label.name": "Name",
"label.new-password": "Neues Passwort",
"label.none": "None",
"label.none": "Keine",
"label.owner": "Besitzer",
"label.password": "Passwort",
"label.passwords-dont-match": "Passwörter stimmen nicht überein",
@ -68,6 +68,7 @@
"message.confirm-reset": "Sind Sie sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?",
"message.copied": "In Zwischenablage kopiert!",
"message.delete-warning": "Alle zugehörigen Daten werden ebenfalls gelöscht.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Es ist ein Fehler aufgetreten.",
"message.get-share-url": "Freigabe-URL abrufen",
"message.get-tracking-code": "Erstelle Tracking Kennung",
@ -107,11 +108,11 @@
"metrics.screens": "Bildschirmauflösungen",
"metrics.unique-visitors": "Eindeutige Besucher",
"metrics.utm": "UTM",
"metrics.utm_campaign": "UTM Campaign",
"metrics.utm_content": "UTM Content",
"metrics.utm_campaign": "UTM Kampagne",
"metrics.utm_content": "UTM Inhalt",
"metrics.utm_medium": "UTM Medium",
"metrics.utm_source": "UTM Source",
"metrics.utm_term": "UTM Term",
"metrics.utm_source": "UTM Quelle",
"metrics.utm_term": "UTM Begriff",
"metrics.views": "Aufrufe",
"metrics.visitors": "Besucher"
}

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Αντιγράφηκε!",
"message.delete-warning": "Όλα τα σχετικά δεδομένα θα διαγραφούν επίσης.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Κάτι πήγε στραβά.",
"message.get-share-url": "Λήψη URL κοινής χρήσης",
"message.get-tracking-code": "Λήψη κώδικα παρακολούθησης",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are you sure you want to reset {target}'s statistics?",
"message.copied": "Copied!",
"message.delete-warning": "All associated data will be deleted as well.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Something went wrong.",
"message.get-share-url": "Get share URL",
"message.get-tracking-code": "Get tracking code",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are you sure you want to reset {target}'s statistics?",
"message.copied": "Copied!",
"message.delete-warning": "All associated data will be deleted as well.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Something went wrong.",
"message.get-share-url": "Get share URL",
"message.get-tracking-code": "Get tracking code",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "¿Seguro que deseas restablecer las estadísticas de {target}?",
"message.copied": "¡Copiado!",
"message.delete-warning": "Toda la información relacionada será eliminada.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Algo falló.",
"message.get-share-url": "Obtener URL para compartir",
"message.get-tracking-code": "Obtener código de rastreo",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "آیا از بازنشانی آمار {target} مطمئن هستید?",
"message.copied": "کپی شد!",
"message.delete-warning": "همه‌ی داده‌های مرتبط هم حذف خواهد شد.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "مشکلی پیش آمده است.",
"message.get-share-url": "دریافت URL برای اشتراک گذاری",
"message.get-tracking-code": "گرفتن کد رهگیری",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Haluatko varmasti poistaa sivuston {target} tilastot?",
"message.copied": "Kopioitu!",
"message.delete-warning": "Kaikki siihen liittyvät tiedot poistetaan.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Jotain meni pieleen.",
"message.get-share-url": "Hanki jakamisen URL-osoite",
"message.get-tracking-code": "Hanki seurantakoodi",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Avrita!",
"message.delete-warning": "Øll data ið er knýtt at verður eisini strika.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Okkurt bleiv gali.",
"message.get-share-url": "Fá leinku sum tú kanst deila",
"message.get-tracking-code": "Fá sporings kotu",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Êtes-vous sûr de vouloir réinistialiser les statistiques de {target} ?",
"message.copied": "Copié !",
"message.delete-warning": "Toutes les données associées seront également supprimées.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Un problème est survenu.",
"message.get-share-url": "Obtenir l'URL de partage",
"message.get-tracking-code": "Obtenir le code de suivi",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Tes a certeza de querer restablecer as estatísticas de {target}?",
"message.copied": "Copiado!",
"message.delete-warning": "Tamén serán borrados tódolos datos asociados.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Houbo un fallo.",
"message.get-share-url": "Obter URL de compartición",
"message.get-tracking-code": "Obter código de seguimento",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "הועתק!",
"message.delete-warning": "כל המידע המקושר יימחק",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "משהו השתבש",
"message.get-share-url": "קבלת URL שיתוף",
"message.get-tracking-code": "קבלת קוד מעקב",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "कॉपी हो गया!",
"message.delete-warning": "सभी संबद्ध डेटा को भी हटा दिया जाएगा।",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "कुछ गलत हो गया।",
"message.get-share-url": "शेयर URL प्राप्त करें",
"message.get-tracking-code": "ट्रैकिंग कोड प्राप्त करें",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Kimásolva!",
"message.delete-warning": "Minden társított adat törlésre kerül.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Valami baj történt.",
"message.get-share-url": "Megosztási URL kimásolása",
"message.get-tracking-code": "Követési kód kimásolása",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Anda yakin ingin mengatur ulang statistik {target}?",
"message.copied": "Tersalin!",
"message.delete-warning": "Semua data terkait juga akan dihapus.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Ada yang salah.",
"message.get-share-url": "Dapatkan URL berbagi",
"message.get-tracking-code": "Dapatkan kode pelacakan",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Sei sicuro di voler azzerare le statistiche di {target}?",
"message.copied": "Copiato!",
"message.delete-warning": "Saranno eliminati anche tutti i dati associati.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Si è verificato un errore.",
"message.get-share-url": "Ottieni l'URL di condivisione",
"message.get-tracking-code": "Ottieni il codice di tracking",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "コピーしました!",
"message.delete-warning": "関連するすべてのデータも削除されます。",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "問題が発生しました。",
"message.get-share-url": "共有リンクを取得",
"message.get-tracking-code": "トラッキングコードを取得",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "복사했습니다!",
"message.delete-warning": "관련된 모든 데이터도 삭제됩니다.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "오류가 발생하였습니다.",
"message.get-share-url": "공유 URL 가져오기",
"message.get-tracking-code": "추적 코드 가져오기",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are esate tikri, jog norite atstatyti svetainės {target} statistikos duomenis?",
"message.copied": "Nukopijuota!",
"message.delete-warning": "Visi susiję duomenys taip pat bus ištrinti.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Kažkas įvyko ne taip.",
"message.get-share-url": "Gauti bendrinimo nuorodą",
"message.get-tracking-code": "Gauti sekimo kodą",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Хуулсан!",
"message.delete-warning": "Үүнтэй холбоотой бүх өгөгдөл устах болно.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Ямар нэг зүйл буруу боллоо.",
"message.get-share-url": "Хуваалцах холбоос авах",
"message.get-tracking-code": "Мөрдөх код авах",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Disalin!",
"message.delete-warning": "Semua data yang berkaitan juga akan dihapuskan.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Ada yang tidak kena.",
"message.get-share-url": "Dapatkan URL berkongsi",
"message.get-tracking-code": "Dapatkan kod penjejakan",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Er du sikker på at du vil nullstille {target}'s statistikk?",
"message.copied": "Kopiert!",
"message.delete-warning": "Alle tilknyttede data slettes også.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Noe gikk galt.",
"message.get-share-url": "Få delings-URL",
"message.get-tracking-code": "Få sporingskode",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Weet je zeker dat je de statistieken van {target} opnieuw wilt instellen?",
"message.copied": "Gekopiëerd!",
"message.delete-warning": "Alle verwante gegezens zullen ook verwijderd worden.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Er is iets misgegaan.",
"message.get-share-url": "Openbare URL",
"message.get-tracking-code": "Tracking code",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Czy na pewno chcesz zresetować statystyki {target}?",
"message.copied": "Skopiowano!",
"message.delete-warning": "Wszystkie powiązane dane również zostaną usunięte.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Coś poszło nie tak.",
"message.get-share-url": "Uzyskaj adres URL udostępniania",
"message.get-tracking-code": "Pobierz kod śledzenia",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Você tem certeza que deseja redefinir as estatísticas de {target}?",
"message.copied": "Copiado!",
"message.delete-warning": "Todos os dados associados também serão eliminados.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Ocorreu um erro.",
"message.get-share-url": "Obter link de compartilhamento",
"message.get-tracking-code": "Obter código de rastreamento",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Tem a certeza que pretende restaurar as estatísticas de {target}?",
"message.copied": "Copiado!",
"message.delete-warning": "Todos os dados associados também serão eliminados.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Ocorreu um erro.",
"message.get-share-url": "Obter link de partilha",
"message.get-tracking-code": "Obter código de rastreamento",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Sunteți sigur că doriți să resetați statisticile pentru {target}?",
"message.copied": "Copiat!",
"message.delete-warning": "Toate datele asociate vor fi șterse, de asemenea.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Ceva n-a mers bine.",
"message.get-share-url": "Obține adresa URL de partajare",
"message.get-tracking-code": "Obține codul de urmărire",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Вы уверены, что хотите сбросить статистику {target}?",
"message.copied": "Скопировано!",
"message.delete-warning": "Все связанные данные будут также удалены.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Что-то пошло не так.",
"message.get-share-url": "Получить публичную ссылку",
"message.get-tracking-code": "Получить код отслеживания",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Skopírované!",
"message.delete-warning": "Všetky príbuzné data budu tiež zmazané.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Niečo sa pokazilo.",
"message.get-share-url": "Získať zdielané URL",
"message.get-tracking-code": "Získať tracking kód",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Kopirano!",
"message.delete-warning": "Izbrisani bodo tudi vsi povezani podatki.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Prišlo je do napake.",
"message.get-share-url": "Pridobi URL za skupno rabo",
"message.get-tracking-code": "Pridobi kodo za sledenje",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Är du säker på att du vill återställa statistiken för {target}?",
"message.copied": "Kopierad!",
"message.delete-warning": "All tillhörande data kommer också raderas.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Något gick fel.",
"message.get-share-url": "Visa delnings-URL",
"message.get-tracking-code": "Visa spårningskod",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "நகலெடுக்கப்பட்டது!",
"message.delete-warning": "தொடர்புடைய எல்லா தரவும் நீக்கப்படும்.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "ஏதோ தவறு நடந்துவிட்டது.",
"message.get-share-url": "கள முகவரியை ஐப் பெறுக",
"message.get-tracking-code": "கண்காணிப்பு குறியீட்டைப் பெறுக",

118
lang/th-TH.json Normal file
View File

@ -0,0 +1,118 @@
{
"label.accounts": "บัญชี",
"label.add-account": "เพิ่มบัญชี",
"label.add-website": "เพิ่มเว็บไซต์",
"label.administrator": "ผู้ดูแลระบบ",
"label.all": "ทั้งหมด",
"label.all-events": "เหตุการณ์ทั้งหมด",
"label.all-time": "ทุกช่วงเวลา",
"label.all-websites": "เว็บไซต์ทั้งหมด",
"label.back": "ย้อนกลับ",
"label.cancel": "ยกเลิก",
"label.change-password": "เปลี่ยนรหัสผ่าน",
"label.confirm-password": "ยืนยันรหัสผ่าน",
"label.copy-to-clipboard": "คัดลอกไปยังคลิปบอร์ด",
"label.current-password": "รหัสผ่านปัจจุบัน",
"label.custom-range": "กำหนดช่วงเวลา",
"label.dashboard": "แดชบอร์ด",
"label.date-range": "ตั้งแต่วันที่",
"label.default-date-range": "ช่วงเวลา",
"label.delete": "ลบ",
"label.delete-account": "ลบบัญชี",
"label.delete-website": "ลบเว็บไซต์",
"label.dismiss": "ยกเลิก",
"label.domain": "โดเมน",
"label.edit": "แก้ไข",
"label.edit-account": "แก้ไขบัญชี",
"label.edit-website": "แก้ไขเว็บไซต์",
"label.enable-share-url": "เปิดใช้งานการแชร์ลิงก์",
"label.invalid": "ไม่ถูกต้อง",
"label.invalid-domain": "โดเมนไม่ถูกต้อง",
"label.language": "ภาษา",
"label.last-days": "{x} วันที่ผ่านมา",
"label.last-hours": "{x} ชั่วโมงที่ผ่านมา",
"label.logged-in-as": "เข้าสู่ระบบโดย {username}",
"label.login": "เข้าสู่ระบบ",
"label.logout": "ออกจากระบบ",
"label.more": "เพิ่มเติม",
"label.name": "ชื่อ",
"label.new-password": "รหัสผ่านใหม่",
"label.none": "ไม่ได้กำหนด",
"label.owner": "เจ้าของ",
"label.password": "รหัสผ่าน",
"label.passwords-dont-match": "รหัสผ่านไม่ตรงกัน",
"label.profile": "โปรไฟล์",
"label.realtime": "เรียลไทม์",
"label.realtime-logs": "Log แบบเรียลไทม์",
"label.refresh": "รีเฟรช",
"label.required": "ต้องการ",
"label.reset": "รีเซต",
"label.reset-website": "รีเซตข้อมูลสถิติ",
"label.save": "บันทึก",
"label.settings": "ตั้งค่า",
"label.share-url": "แชร์ลิงก์",
"label.single-day": "วันที่",
"label.theme": "ธีม",
"label.this-month": "เดือนปัจจุบัน",
"label.this-week": "สัปดาห์ปัจจุบัน",
"label.this-year": "ปีปัจจุบัน",
"label.timezone": "เขตเวลา",
"label.today": "วันนี้",
"label.tracking-code": "โค้ดสำหรับใช้ติดตาม",
"label.unknown": "ไม่รู้จัก",
"label.username": "ชื่อผู้ใช้",
"label.view-details": "แสดงรายละเอียด",
"label.websites": "เว็บไซต์",
"message.active-users": "มีผู้ใช้งาน {x} {x, plural, one {คนในขณะนี้} other {คนในขณะนี้}}",
"message.confirm-delete": "คุณแน่ใจหรือไม่ว่าต้องการลบ {target} ?",
"message.confirm-reset": "คุณแน่ใจหรือไม่ว่าต้องการรีเซตข้อมูลสถิติของ {target} ?",
"message.copied": "คัดลอกแล้ว!",
"message.delete-warning": "ข้อมูลที่เกี่ยวข้องทั้งหมดจะถูกลบ.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "เกิดข้อผิดพลาด.",
"message.get-share-url": "รับลิงก์สำหรับแชร์",
"message.get-tracking-code": "รับโค้ดสำหรับใช้ติดตาม",
"message.go-to-settings": "ไปที่การตั้งค่า",
"message.incorrect-username-password": "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง.",
"message.log.visitor": "ผู้เข้าชมจาก {country} กำลังใช้งานผ่าน {browser} บน {os} {device}",
"message.new-version-available": "umami เวอร์ชันใหม่ ({version}) มาแล้ว!",
"message.no-data-available": "ไม่มีข้อมูล.",
"message.no-websites-configured": "คุณยังไม่ได้ตั้งค่าเว็บไซต์ใด ๆ ไว้.",
"message.page-not-found": "ไม่พบหน้านี้.",
"message.powered-by": "ขับเคลื่อนโดย {name}",
"message.reset-warning": "สถิติทั้งหมดสำหรับเว็บไซต์นี้จะถูกลบออก แต่โค้ดสำหรับใช้ติดตามของคุณจะยังคงอยู่เหมือนเดิม.",
"message.save-success": "บันทึกข้อมูลเรียบร้อย.",
"message.share-url": "นี่คือลิงก์ที่แชร์แบบสาธารณะสำหรับ {target}.",
"message.toggle-charts": "เปิด/ปิดแผนภูมิ",
"message.track-stats": "หากต้องการติดตามสถิติสำหรับ {target} ให้วางโค้ดต่อไปนี้ในส่วน {head} ของเว็บไซต์ของคุณ.",
"message.type-delete": "พิมพ์ข้อความ {delete} ในช่องด้านล่างเพื่อยืนยัน.",
"message.type-reset": "พิมพ์ข้อความ {reset} ในช่องด้านล่างเพื่อยืนยัน.",
"metrics.actions": "การกระทำ",
"metrics.average-visit-time": "ระยะเวลาเข้าชมเฉลี่ย",
"metrics.bounce-rate": "อัตราตีกลับ",
"metrics.browsers": "เบราว์เซอร์",
"metrics.countries": "ประเทศ",
"metrics.device.desktop": "เดสก์ท็อป",
"metrics.device.laptop": "แล็ปท็อป",
"metrics.device.mobile": "โทรศัพท์มือถือ",
"metrics.device.tablet": "แท็บเล็ต",
"metrics.devices": "อุปกรณ์",
"metrics.events": "เหตุการณ์",
"metrics.filter.combined": "ข้อมูลรวม",
"metrics.filter.raw": "ข้อมูลดิบ",
"metrics.languages": "ภาษา",
"metrics.operating-systems": "ระบบปฏิบัติการ",
"metrics.page-views": "การเข้าชม",
"metrics.pages": "หน้าเพจ",
"metrics.referrers": "แหล่งที่มา",
"metrics.screens": "ขนาดหน้าจอ",
"metrics.unique-visitors": "ผู้เข้าชม",
"metrics.utm": "UTM",
"metrics.utm_campaign": "UTM Campaign",
"metrics.utm_content": "UTM Content",
"metrics.utm_medium": "UTM Medium",
"metrics.utm_source": "UTM Source",
"metrics.utm_term": "UTM Term",
"metrics.views": "การเข้าชม",
"metrics.visitors": "ผู้เข้าชม"
}

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Are your sure you want to reset {target}'s statistics?",
"message.copied": "Panoya kopyalandı!",
"message.delete-warning": "İlişkili tüm veriler de silinecektir.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Bir şeyler ters gitti!",
"message.get-share-url": "Paylaşım adresini al",
"message.get-tracking-code": "İzleme kodunu al",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Ви впевнені, що бажаєте скинути статистику для {target}?",
"message.copied": "Скопійовано!",
"message.delete-warning": "Усі пов'язані дані будуть видалені також.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Щось пішло не так.",
"message.get-share-url": "Отримати публічне посилання",
"message.get-tracking-code": "Отримати код для відслідковування",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "کیا آپ واقعی {target} کے اعدادوشمار کو دوبارہ ترتیب دینا چاہتے ہیں؟",
"message.copied": "کاپی کیا گیا!",
"message.delete-warning": "تمام متعلقہ ڈیٹا بھی حذف کر دیا جائے گا۔",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "کچھ غلط ہو گیا.",
"message.get-share-url": "شیئر URL حاصل کریں",
"message.get-tracking-code": "ٹریکنگ کوڈ حاصل کریں",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "Bạn có chắc chắn muốn tái thiết lập thống kê {target}?",
"message.copied": "Đã sao chép!",
"message.delete-warning": "Tất cả các dữ liệu liên quan cũng sẽ bị xoá.",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "Đã xảy ra lỗi.",
"message.get-share-url": "Lấy URL chia sẻ",
"message.get-tracking-code": "Lấy mã theo dõi",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "您确定要重置 {target} 的数据吗?",
"message.copied": "复制成功!",
"message.delete-warning": "所有相关数据将会被删除。",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "出现错误。",
"message.get-share-url": "获取共享链接",
"message.get-tracking-code": "获取跟踪代码",

View File

@ -68,6 +68,7 @@
"message.confirm-reset": "您確定要重置 {target} 的數據嗎?",
"message.copied": "複製成功!",
"message.delete-warning": "所有相關數據將會被刪除。",
"message.edit-dashboard": "Edit dashboard",
"message.failure": "出現錯誤。",
"message.get-share-url": "獲得分享連結",
"message.get-tracking-code": "獲得追蹤代碼",

View File

@ -9,6 +9,7 @@ export const SHARE_TOKEN_HEADER = 'x-umami-share-token';
export const HOMEPAGE_URL = 'https://umami.is';
export const REPO_URL = 'https://github.com/umami-software/umami';
export const UPDATES_URL = 'https://api.umami.is/v1/updates';
export const TELEMETRY_PIXEL = 'https://i.umami.is/a.png';
export const DEFAULT_LOCALE = 'en-US';
export const DEFAULT_THEME = 'light';

View File

@ -217,7 +217,7 @@ export function getFilterQuery(table, column, filters = {}, params = []) {
}
break;
case 'event_type':
case 'event_name':
if (table === 'event') {
arr.push(`and ${table}.${key}=$${params.length + 1}`);
params.push(decodeURIComponent(filter));
@ -247,17 +247,17 @@ export function getFilterQuery(table, column, filters = {}, params = []) {
}
export function parseFilters(table, column, filters = {}, params = [], sessionKey = 'session_id') {
const { domain, url, event_url, referrer, os, browser, device, country, event_type } = filters;
const { domain, url, event_url, referrer, os, browser, device, country, event_name } = filters;
const pageviewFilters = { domain, url, referrer };
const sessionFilters = { os, browser, device, country };
const eventFilters = { url: event_url, event_type };
const eventFilters = { url: event_url, event_name };
return {
pageviewFilters,
sessionFilters,
eventFilters,
event: { event_type },
event: { event_name },
joinSession:
os || browser || device || country
? `inner join session on ${table}.${sessionKey} = session.${sessionKey}`

View File

@ -78,3 +78,14 @@ export function stringToColor(str) {
}
return color;
}
export function orderByWebsiteMap(websites, orderMap) {
if (!websites) return [];
let ordered = [...websites];
for (let website of websites) {
ordered[orderMap[website.website_uuid]] = website;
}
return ordered;
}

View File

@ -1,5 +1,6 @@
import {
arSA,
bn,
cs,
sk,
da,
@ -30,6 +31,7 @@ import {
sl,
sv,
ta,
th,
tr,
uk,
zhCN,
@ -41,6 +43,7 @@ import {
export const languages = {
'ar-SA': { label: 'العربية', dateLocale: arSA, dir: 'rtl' },
'bn-BD': { label: 'বাংলা', dateLocale: bn },
'zh-CN': { label: '中文', dateLocale: zhCN },
'zh-TW': { label: '中文(繁體)', dateLocale: zhTW },
'ca-ES': { label: 'Català', dateLocale: ca },
@ -77,6 +80,7 @@ export const languages = {
'fi-FI': { label: 'Suomi', dateLocale: fi },
'sv-SE': { label: 'Svenska', dateLocale: sv },
'ta-IN': { label: 'தமிழ்', dateLocale: ta },
'th-TH': { label: 'ภาษาไทย', dateLocale: th },
'tr-TR': { label: 'Türkçe', dateLocale: tr },
'uk-UA': { label: 'українська', dateLocale: uk },
'ur-PK': { label: 'Urdu (Pakistan)', dateLocale: uk, dir: 'rtl' },

View File

@ -68,7 +68,7 @@ export async function getCountry(req, ip) {
// Database lookup
if (!lookup) {
lookup = await maxmind.open(path.resolve('./public/geo/GeoLite2-Country.mmdb'));
lookup = await maxmind.open(path.resolve('node_modules/.geo/GeoLite2-Country.mmdb'));
}
const result = lookup.get(ip);

View File

@ -61,10 +61,17 @@ export const setItem = (key, data, session) => {
}
};
export const getItem = (key, session) =>
typeof window !== 'undefined'
? JSON.parse((session ? sessionStorage : localStorage).getItem(key) || null)
: null;
export const getItem = (key, session) => {
if (typeof window !== 'undefined') {
const value = (session ? sessionStorage : localStorage).getItem(key);
if (value !== 'undefined') {
return JSON.parse(value);
}
}
return null;
};
export const removeItem = (key, session) => {
if (typeof window !== 'undefined') {

View File

@ -33,14 +33,6 @@ function customScriptName(req) {
}
}
function forceSSL(req, res) {
if (process.env.FORCE_SSL && req.nextUrl.protocol === 'http:') {
res.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
return res;
}
export default function middleware(req) {
const fns = [customCollectEndpoint, customScriptName];
@ -51,5 +43,5 @@ export default function middleware(req) {
}
}
return forceSSL(req, NextResponse.next());
return NextResponse.next();
}

View File

@ -1,12 +1,40 @@
require('dotenv').config();
const pkg = require('./package.json');
const contentSecurityPolicy = `
default-src 'self';
img-src *;
script-src 'self' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
connect-src 'self' api.umami.is;
frame-ancestors 'self';
`;
const headers = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'Content-Security-Policy',
value: contentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
},
];
if (process.env.FORCE_SSL) {
headers.push({
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
});
}
module.exports = {
env: {
currentVersion: pkg.version,
loginDisabled: process.env.DISABLE_LOGIN,
updatesDisabled: process.env.DISABLE_UPDATES,
telemetryDisabled: process.env.DISABLE_TELEMETRY,
},
basePath: process.env.BASE_PATH,
output: 'standalone',
@ -25,13 +53,16 @@ module.exports = {
async headers() {
return [
{
source: `/(.*\\.js)`,
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=2592000', // 30 days
},
],
source: '/:path*',
headers,
},
];
},
async rewrites() {
return [
{
source: '/telemetry.js',
destination: '/api/scripts/telemetry',
},
];
},

View File

@ -20,7 +20,7 @@
"build-app": "next build",
"build-tracker": "rollup -c rollup.tracker.config.js",
"build-db": "npm-run-all copy-db-files build-db-client",
"build-lang": "npm-run-all format-lang compile-lang",
"build-lang": "npm-run-all format-lang compile-lang download-country-names download-language-names",
"build-geo": "node scripts/build-geo.js",
"build-db-schema": "prisma db pull",
"build-db-client": "prisma generate",
@ -86,6 +86,7 @@
"npm-run-all": "^4.1.5",
"prop-types": "^15.7.2",
"react": "^17.0.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.0",
"react-intl": "^5.24.7",
"react-simple-maps": "^2.3.0",
@ -128,5 +129,8 @@
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^7.0.0",
"tar": "^6.1.2"
}
},
"cacheDirectories": [
".next/cache"
]
}

View File

@ -1,4 +1,3 @@
import React from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { IntlProvider } from 'react-intl';

View File

@ -65,7 +65,7 @@ export default async (req, res) => {
const { type, payload } = getJsonBody(req);
let { url, referrer, event_type, event_value } = payload;
let { url, referrer, event_name, event_data } = payload;
if (process.env.REMOVE_TRAILING_SLASH) {
url = removeTrailingSlash(url);
@ -74,7 +74,7 @@ export default async (req, res) => {
if (type === 'pageview') {
await savePageView(website_id, { session_id, session_uuid, url, referrer });
} else if (type === 'event') {
await saveEvent(website_id, { session_id, session_uuid, url, event_type, event_value });
await saveEvent(website_id, { session_id, session_uuid, url, event_name, event_data });
} else {
return badRequest(res);
}

14
pages/api/config.js Normal file
View File

@ -0,0 +1,14 @@
import { ok, methodNotAllowed } from 'lib/response';
export default async (req, res) => {
if (req.method === 'GET') {
return ok(res, {
basePath: process.env.BASE_PATH || '',
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
updatesDisabled: !!process.env.DISABLE_UPDATES,
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
});
}
return methodNotAllowed(res);
};

View File

@ -0,0 +1,18 @@
import { TELEMETRY_PIXEL } from 'lib/constants';
export default function handler(req, res) {
const { v } = req.query;
const script = `
(()=>{const i=document.createElement('img');
i.setAttribute('src','${TELEMETRY_PIXEL}?v=${v}');
i.setAttribute('style','width:0;height:0;position:absolute;pointer-events:none;');
document.body.appendChild(i);})();
`;
res.setHeader('content-type', 'text/javascript');
if (process.env.DISABLE_TELEMETRY) {
res.send('/* telemetry disabled */');
} else {
res.send(script.replace(/\s\s+/g, ''));
}
}

View File

@ -14,7 +14,7 @@ export default async (req, res) => {
return unauthorized(res);
}
const { id, start_at, end_at, unit, tz, url, event_type } = req.query;
const { id, start_at, end_at, unit, tz, url, event_name } = req.query;
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return badRequest(res);
@ -26,7 +26,7 @@ export default async (req, res) => {
const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, {
url,
event_type,
event_name,
});
return ok(res, events);

View File

@ -22,7 +22,7 @@ function getTable(type) {
function getColumn(type) {
if (type === 'event') {
return `concat(event_type, '\t', event_value)`;
return `event_name`;
}
return type;
}

View File

@ -2,8 +2,8 @@ import React from 'react';
import Layout from 'components/layout/Layout';
import LoginForm from 'components/forms/LoginForm';
export default function LoginPage() {
if (process.env.loginDisabled) {
export default function LoginPage({ loginDisabled }) {
if (loginDisabled) {
return null;
}
@ -13,3 +13,9 @@ export default function LoginPage() {
</Layout>
);
}
export async function getServerSideProps() {
return {
props: { loginDisabled: !!process.env.DISABLE_LOGIN },
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -477,6 +477,12 @@
"value": "كافة البيانات المرتبطة سيم حذفها ايضا."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -0,0 +1,838 @@
{
"label.accounts": [
{
"type": 0,
"value": "অ্যাকাউন্ট"
}
],
"label.add-account": [
{
"type": 0,
"value": "অ্যাকাউন্ট যুক্ত করুন"
}
],
"label.add-website": [
{
"type": 0,
"value": "ওয়েবসাইট যুক্ত করুন"
}
],
"label.administrator": [
{
"type": 0,
"value": "অ্যাডমিন"
}
],
"label.all": [
{
"type": 0,
"value": "সবগুলো"
}
],
"label.all-events": [
{
"type": 0,
"value": "সবগুলো ঘটনা"
}
],
"label.all-time": [
{
"type": 0,
"value": "সব সময়"
}
],
"label.all-websites": [
{
"type": 0,
"value": "সবগুলো ওয়েবসাইট"
}
],
"label.back": [
{
"type": 0,
"value": "পেছনে"
}
],
"label.cancel": [
{
"type": 0,
"value": "বাতিল"
}
],
"label.change-password": [
{
"type": 0,
"value": "পাসওয়ার্ড পরিবর্তন করুন"
}
],
"label.confirm-password": [
{
"type": 0,
"value": "পাসওয়ার্ড নিশ্চিত করুন"
}
],
"label.copy-to-clipboard": [
{
"type": 0,
"value": "কপি করুন"
}
],
"label.current-password": [
{
"type": 0,
"value": "বর্তমান পাসওয়ার্ড"
}
],
"label.custom-range": [
{
"type": 0,
"value": "কাস্টম রেঞ্জ"
}
],
"label.dashboard": [
{
"type": 0,
"value": "ড্যাশবোর্ড"
}
],
"label.date-range": [
{
"type": 0,
"value": "তারিখের পরিসীমা"
}
],
"label.default-date-range": [
{
"type": 0,
"value": "ডিফল্ট তারিখের পরিসীমা"
}
],
"label.delete": [
{
"type": 0,
"value": "মুছে ফেলুন"
}
],
"label.delete-account": [
{
"type": 0,
"value": "অ্যাকাউন্ট মুছুন"
}
],
"label.delete-website": [
{
"type": 0,
"value": "ওয়েবসাইট মুছুন"
}
],
"label.dismiss": [
{
"type": 0,
"value": "বাতিল"
}
],
"label.domain": [
{
"type": 0,
"value": "ডোমেইন"
}
],
"label.edit": [
{
"type": 0,
"value": "সম্পাদনা করুন"
}
],
"label.edit-account": [
{
"type": 0,
"value": "অ্যাকাউন্ট সম্পাদনা করুন"
}
],
"label.edit-website": [
{
"type": 0,
"value": "ওয়েবসাইট সম্পাদনা করুন"
}
],
"label.enable-share-url": [
{
"type": 0,
"value": "শেয়ার ইউআরএল শেয়ার করুন"
}
],
"label.invalid": [
{
"type": 0,
"value": "ভুল"
}
],
"label.invalid-domain": [
{
"type": 0,
"value": "ভুল ডোমেন"
}
],
"label.language": [
{
"type": 0,
"value": "ভাষা"
}
],
"label.last-days": [
{
"type": 0,
"value": "শেষ "
},
{
"type": 1,
"value": "x"
},
{
"type": 0,
"value": " দিন"
}
],
"label.last-hours": [
{
"type": 0,
"value": "শেষ "
},
{
"type": 1,
"value": "x"
},
{
"type": 0,
"value": " ঘন্টা"
}
],
"label.logged-in-as": [
{
"type": 1,
"value": "username"
},
{
"type": 0,
"value": " অ্যাকাউন্টে লগ ইন"
}
],
"label.login": [
{
"type": 0,
"value": "লগিন"
}
],
"label.logout": [
{
"type": 0,
"value": "লগ আউট"
}
],
"label.more": [
{
"type": 0,
"value": "আরও"
}
],
"label.name": [
{
"type": 0,
"value": "নাম"
}
],
"label.new-password": [
{
"type": 0,
"value": "নতুন পাসওয়ার্ড"
}
],
"label.none": [
{
"type": 0,
"value": "কিছুই না"
}
],
"label.owner": [
{
"type": 0,
"value": "মালিক"
}
],
"label.password": [
{
"type": 0,
"value": "পাসওয়ার্ড"
}
],
"label.passwords-dont-match": [
{
"type": 0,
"value": "পাসওয়ার্ড মেলে না"
}
],
"label.profile": [
{
"type": 0,
"value": "প্রোফাইল"
}
],
"label.realtime": [
{
"type": 0,
"value": "সরাসরি"
}
],
"label.realtime-logs": [
{
"type": 0,
"value": "সরাসরি লগ"
}
],
"label.refresh": [
{
"type": 0,
"value": "রিফ্রেশ"
}
],
"label.required": [
{
"type": 0,
"value": "প্রয়োজনীয়"
}
],
"label.reset": [
{
"type": 0,
"value": "রিসেট"
}
],
"label.reset-website": [
{
"type": 0,
"value": "ওয়েবসাইট রিসেট করুন"
}
],
"label.save": [
{
"type": 0,
"value": "সংরক্ষণ"
}
],
"label.settings": [
{
"type": 0,
"value": "সেটিংস"
}
],
"label.share-url": [
{
"type": 0,
"value": "ইউআরএল শেয়ার করুন"
}
],
"label.single-day": [
{
"type": 0,
"value": "একদিন"
}
],
"label.theme": [
{
"type": 0,
"value": "থিম"
}
],
"label.this-month": [
{
"type": 0,
"value": "এই মাস"
}
],
"label.this-week": [
{
"type": 0,
"value": "এই সপ্তাহ"
}
],
"label.this-year": [
{
"type": 0,
"value": "এই বছর"
}
],
"label.timezone": [
{
"type": 0,
"value": "সময়স্থান"
}
],
"label.today": [
{
"type": 0,
"value": "আজ"
}
],
"label.tracking-code": [
{
"type": 0,
"value": "ট্র্যাকিং কোড"
}
],
"label.unknown": [
{
"type": 0,
"value": "অজানা"
}
],
"label.username": [
{
"type": 0,
"value": "ব্যবহারকারীর নাম"
}
],
"label.view-details": [
{
"type": 0,
"value": "বিস্তারিত দেখুন"
}
],
"label.websites": [
{
"type": 0,
"value": "সবগুলো ওয়েবসাইট"
}
],
"message.active-users": [
{
"type": 1,
"value": "x"
},
{
"type": 0,
"value": " বর্তমান "
},
{
"offset": 0,
"options": {
"one": {
"value": [
{
"type": 0,
"value": "visitor"
}
]
},
"other": {
"value": [
{
"type": 0,
"value": "visitors"
}
]
}
},
"pluralType": "cardinal",
"type": 6,
"value": "x"
}
],
"message.confirm-delete": [
{
"type": 0,
"value": "আপনি কি নিশ্চিত যে আপনি "
},
{
"type": 1,
"value": "target"
},
{
"type": 0,
"value": " মুছতে চান?"
}
],
"message.confirm-reset": [
{
"type": 0,
"value": "আপনি কি নিশ্চিত যে আপনি "
},
{
"type": 1,
"value": "target"
},
{
"type": 0,
"value": " এর পরিসংখ্যান পুনরায় সেট করতে চান?"
}
],
"message.copied": [
{
"type": 0,
"value": "কপি হয়েছে"
}
],
"message.delete-warning": [
{
"type": 0,
"value": "সমস্ত সম্পর্কিত ডেটা পাশাপাশি মুছে ফেলা হবে।"
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,
"value": "কিছু ভুল হয়েছে।"
}
],
"message.get-share-url": [
{
"type": 0,
"value": "শেয়ার ইউআরএল"
}
],
"message.get-tracking-code": [
{
"type": 0,
"value": "ট্র্যাকিং কোড পান"
}
],
"message.go-to-settings": [
{
"type": 0,
"value": "সেটিংস এ যান"
}
],
"message.incorrect-username-password": [
{
"type": 0,
"value": "ভুল ব্যবহারকারীর নাম/পাসওয়ার্ড।"
}
],
"message.log.visitor": [
{
"type": 1,
"value": "country"
},
{
"type": 0,
"value": " থেকে একজন ভিসিটর "
},
{
"type": 1,
"value": "ব্রাউজার"
},
{
"type": 0,
"value": ", ব্যবহার করছেন "
},
{
"type": 1,
"value": "os"
},
{
"type": 0,
"value": " "
},
{
"type": 1,
"value": "device"
},
{
"type": 0,
"value": " এর মধ্যে।"
}
],
"message.new-version-available": [
{
"type": 0,
"value": "নতুন সংস্করণ "
},
{
"type": 1,
"value": "version"
},
{
"type": 0,
"value": " পাওয়া গিয়েছে।"
}
],
"message.no-data-available": [
{
"type": 0,
"value": "কোন তথ্য নেই।"
}
],
"message.no-websites-configured": [
{
"type": 0,
"value": "কোনও ওয়েবসাইট কনফিগার করা নেই।"
}
],
"message.page-not-found": [
{
"type": 0,
"value": "পৃষ্ঠা খুঁজে পাওয়া যায়নি।"
}
],
"message.powered-by": [
{
"type": 1,
"value": "name"
},
{
"type": 0,
"value": " দ্বারা চালিত"
}
],
"message.reset-warning": [
{
"type": 0,
"value": "এই ওয়েবসাইটের সমস্ত পরিসংখ্যান মুছে ফেলা হবে, তবে আপনার ট্র্যাকিং কোডটি অক্ষত থাকবে।"
}
],
"message.save-success": [
{
"type": 0,
"value": "সংরক্ষিত হয়েছে।"
}
],
"message.share-url": [
{
"type": 0,
"value": "এটি "
},
{
"type": 1,
"value": "target"
},
{
"type": 0,
"value": " এর জন্য প্রকাশ্যে শেয়ার করার ইউআরএল।"
}
],
"message.toggle-charts": [
{
"type": 0,
"value": "চার্ট পরিবর্তন করুন"
}
],
"message.track-stats": [
{
"type": 1,
"value": "target"
},
{
"type": 0,
"value": " এর জন্য পরিসংখ্যানগুলি ট্র্যাক করতে, আপনার ওয়েবসাইটের "
},
{
"type": 1,
"value": "head"
},
{
"type": 0,
"value": " বিভাগে নিম্নলিখিত কোডটি রাখুন।"
}
],
"message.type-delete": [
{
"type": 0,
"value": "নিশ্চিত করতে নীচের বাক্সে "
},
{
"type": 1,
"value": "delete"
},
{
"type": 0,
"value": " টাইপ করুন।"
}
],
"message.type-reset": [
{
"type": 0,
"value": "নিশ্চিত করতে নীচের বাক্সে "
},
{
"type": 1,
"value": "reset"
},
{
"type": 0,
"value": " টাইপ করুন।"
}
],
"metrics.actions": [
{
"type": 0,
"value": "অ্যাকশনস"
}
],
"metrics.average-visit-time": [
{
"type": 0,
"value": "গড় পরিদর্শনের সময়"
}
],
"metrics.bounce-rate": [
{
"type": 0,
"value": "বহিষ্কারের হার"
}
],
"metrics.browsers": [
{
"type": 0,
"value": "ব্রাউজার"
}
],
"metrics.countries": [
{
"type": 0,
"value": "দেশ"
}
],
"metrics.device.desktop": [
{
"type": 0,
"value": "ডেস্কটপ"
}
],
"metrics.device.laptop": [
{
"type": 0,
"value": "ল্যাপটপ"
}
],
"metrics.device.mobile": [
{
"type": 0,
"value": "মুঠোফোন"
}
],
"metrics.device.tablet": [
{
"type": 0,
"value": "ট্যাবলেট"
}
],
"metrics.devices": [
{
"type": 0,
"value": "ডিভাইস গুলো"
}
],
"metrics.events": [
{
"type": 0,
"value": "ঘটনা"
}
],
"metrics.filter.combined": [
{
"type": 0,
"value": "সম্মিলিত"
}
],
"metrics.filter.raw": [
{
"type": 0,
"value": "অপরিশোধিত"
}
],
"metrics.languages": [
{
"type": 0,
"value": "ভাষা"
}
],
"metrics.operating-systems": [
{
"type": 0,
"value": "অপারেটিং সিস্টেম গুলো"
}
],
"metrics.page-views": [
{
"type": 0,
"value": "পৃষ্ঠা পরিদর্শন গুলো"
}
],
"metrics.pages": [
{
"type": 0,
"value": "পৃষ্ঠাগুলি"
}
],
"metrics.referrers": [
{
"type": 0,
"value": "রেফারার্স"
}
],
"metrics.screens": [
{
"type": 0,
"value": "স্ক্রিনগুলি"
}
],
"metrics.unique-visitors": [
{
"type": 0,
"value": "অনন্য ভিজিটর"
}
],
"metrics.utm": [
{
"type": 0,
"value": "UTM"
}
],
"metrics.utm_campaign": [
{
"type": 0,
"value": "UTM প্রচার"
}
],
"metrics.utm_content": [
{
"type": 0,
"value": "UTM সামগ্রী"
}
],
"metrics.utm_medium": [
{
"type": 0,
"value": "UTM মিডিয়াম"
}
],
"metrics.utm_source": [
{
"type": 0,
"value": "UTM উৎস"
}
],
"metrics.utm_term": [
{
"type": 0,
"value": "UTM শব্দ"
}
],
"metrics.views": [
{
"type": 0,
"value": "ভিউস"
}
],
"metrics.visitors": [
{
"type": 0,
"value": "পরিদর্শনার্থী"
}
]
}

View File

@ -477,6 +477,12 @@
"value": "També s'esborraran totes les dades relacionades."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -477,6 +477,12 @@
"value": "Všechna související data budou také smazána."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -477,6 +477,12 @@
"value": "Alle tilknyttede data slettes også."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -250,7 +250,7 @@
"label.none": [
{
"type": 0,
"value": "None"
"value": "Keine"
}
],
"label.owner": [
@ -477,6 +477,12 @@
"value": "Alle zugehörigen Daten werden ebenfalls gelöscht."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,
@ -794,13 +800,13 @@
"metrics.utm_campaign": [
{
"type": 0,
"value": "UTM Campaign"
"value": "UTM Kampagne"
}
],
"metrics.utm_content": [
{
"type": 0,
"value": "UTM Content"
"value": "UTM Inhalt"
}
],
"metrics.utm_medium": [
@ -812,13 +818,13 @@
"metrics.utm_source": [
{
"type": 0,
"value": "UTM Source"
"value": "UTM Quelle"
}
],
"metrics.utm_term": [
{
"type": 0,
"value": "UTM Term"
"value": "UTM Begriff"
}
],
"metrics.views": [

View File

@ -477,6 +477,12 @@
"value": "Όλα τα σχετικά δεδομένα θα διαγραφούν επίσης."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -477,6 +477,12 @@
"value": "All associated data will be deleted as well."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -477,6 +477,12 @@
"value": "All associated data will be deleted as well."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -477,6 +477,12 @@
"value": "Toda la información relacionada será eliminada."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -477,6 +477,12 @@
"value": "همه‌ی داده‌های مرتبط هم حذف خواهد شد."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -477,6 +477,12 @@
"value": "Kaikki siihen liittyvät tiedot poistetaan."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -477,6 +477,12 @@
"value": "Øll data ið er knýtt at verður eisini strika."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -473,6 +473,12 @@
"value": "Toutes les données associées seront également supprimées."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -477,6 +477,12 @@
"value": "Tamén serán borrados tódolos datos asociados."
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

View File

@ -469,6 +469,12 @@
"value": "כל המידע המקושר יימחק"
}
],
"message.edit-dashboard": [
{
"type": 0,
"value": "Edit dashboard"
}
],
"message.failure": [
{
"type": 0,

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