diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index 2dc5b675..00000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-# Number of days of inactivity before an issue becomes stale
-daysUntilStale: 60
-# Number of days of inactivity before a stale issue is closed
-daysUntilClose: 7
-# Issues with these labels will never be considered stale
-exemptLabels:
- - pinned
- - security
- - enhancement
- - bug
-# Label to use when marking an issue as stale
-staleLabel: wontfix
-# Comment to post when marking an issue as stale. Set to `false` to disable
-markComment: >
- This issue has been automatically marked as stale because it has not had
- recent activity. It will be closed if no further activity occurs. Thank you
- for your contributions.
-# Comment to post when closing a stale issue. Set to `false` to disable
-closeComment: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c140f626..775f9ecf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,13 +16,13 @@ jobs:
strategy:
matrix:
include:
- - node-version: 14.x
+ - node-version: 16.x
db-type: postgresql
- - node-version: 14.x
+ - node-version: 16.x
db-type: mysql
- - node-version: 16.x
+ - node-version: 18.x
db-type: postgresql
- - node-version: 16.x
+ - node-version: 18.x
db-type: mysql
steps:
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
new file mode 100644
index 00000000..bf2505b1
--- /dev/null
+++ b/.github/workflows/stale-issues.yml
@@ -0,0 +1,22 @@
+name: Close stale issues
+on:
+ schedule:
+ - cron: '30 1 * * *'
+
+jobs:
+ stale:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - uses: actions/stale@v8
+ with:
+ days-before-issue-stale: 60
+ days-before-issue-close: 7
+ stale-issue-label: 'stale'
+ stale-issue-message: 'This issue is stale because it has been open for 60 days with no activity.'
+ close-issue-message: 'This issue was closed because it has been inactive for 7 days since being marked as stale.'
+ days-before-pr-stale: -1
+ days-before-pr-close: -1
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 7066fb28..99087ab5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,7 @@ node_modules
*.iml
*.log
.vscode
+.tool-versions
# debug
npm-debug.log*
diff --git a/assets/bar-chart.svg b/assets/bar-chart.svg
new file mode 100644
index 00000000..25a182a3
--- /dev/null
+++ b/assets/bar-chart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/common/UpdateNotice.js b/components/common/UpdateNotice.js
index 161a5a67..bef6be98 100644
--- a/components/common/UpdateNotice.js
+++ b/components/common/UpdateNotice.js
@@ -1,15 +1,23 @@
-import { useState, useEffect, useCallback } from 'react';
+import { useEffect, useCallback, useState } from 'react';
import { Button, Row, Column } from 'react-basics';
import { setItem } from 'next-basics';
import useStore, { checkVersion } from 'store/version';
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
import styles from './UpdateNotice.module.css';
import useMessages from 'hooks/useMessages';
+import { useRouter } from 'next/router';
-export function UpdateNotice() {
+export function UpdateNotice({ user, config }) {
const { formatMessage, labels, messages } = useMessages();
const { latest, checked, hasUpdate, releaseUrl } = useStore();
- const [dismissed, setDismissed] = useState(false);
+ const { pathname } = useRouter();
+ const [dismissed, setDismissed] = useState(checked);
+ const allowUpdate =
+ user?.isAdmin &&
+ !config?.updatesDisabled &&
+ !config?.cloudMode &&
+ !pathname.includes('/share/') &&
+ !dismissed;
const updateCheck = useCallback(() => {
setItem(VERSION_CHECK, { version: latest, time: Date.now() });
@@ -27,12 +35,12 @@ export function UpdateNotice() {
}
useEffect(() => {
- if (!checked) {
+ if (allowUpdate) {
checkVersion();
}
- }, [checked]);
+ }, [allowUpdate]);
- if (!hasUpdate || dismissed) {
+ if (!allowUpdate || !hasUpdate) {
return null;
}
diff --git a/components/common/UpdateNotice.module.css b/components/common/UpdateNotice.module.css
index f5e29445..db7a0abd 100644
--- a/components/common/UpdateNotice.module.css
+++ b/components/common/UpdateNotice.module.css
@@ -4,7 +4,7 @@
gap: 20px;
margin: 20px auto;
justify-self: center;
- background: #fff;
+ background: var(--base50);
padding: 20px;
border: 1px solid var(--base300);
border-radius: var(--border-radius);
@@ -15,7 +15,8 @@
display: flex;
justify-content: center;
align-items: center;
- font-weight: 600;
+ color: var(--font-color100);
+ font-weight: 700;
}
.buttons {
diff --git a/components/icons.ts b/components/icons.ts
index efd6914b..e42b15fe 100644
--- a/components/icons.ts
+++ b/components/icons.ts
@@ -1,5 +1,7 @@
import { Icons } from 'react-basics';
import AddUser from 'assets/add-user.svg';
+import Bars from 'assets/bars.svg';
+import BarChart from 'assets/bar-chart.svg';
import Bolt from 'assets/bolt.svg';
import Calendar from 'assets/calendar.svg';
import Clock from 'assets/clock.svg';
@@ -22,6 +24,8 @@ import Visitor from 'assets/visitor.svg';
const icons = {
...Icons,
AddUser,
+ Bars,
+ BarChart,
Bolt,
Calendar,
Clock,
diff --git a/components/input/RefreshButton.js b/components/input/RefreshButton.js
index 444f3247..de7ce8cf 100644
--- a/components/input/RefreshButton.js
+++ b/components/input/RefreshButton.js
@@ -10,11 +10,7 @@ export function RefreshButton({ websiteId, isLoading }) {
function handleClick() {
if (!isLoading && dateRange) {
- if (/^\d+/.test(dateRange.value)) {
- setWebsiteDateRange(websiteId, dateRange.value);
- } else {
- setWebsiteDateRange(websiteId, dateRange);
- }
+ setWebsiteDateRange(websiteId, dateRange);
}
}
diff --git a/components/input/WebsiteDateFilter.js b/components/input/WebsiteDateFilter.js
index 91721974..47e6f016 100644
--- a/components/input/WebsiteDateFilter.js
+++ b/components/input/WebsiteDateFilter.js
@@ -1,25 +1,13 @@
-import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import DateFilter from './DateFilter';
import styles from './WebsiteDateFilter.module.css';
export default function WebsiteDateFilter({ websiteId }) {
- const { get } = useApi();
const [dateRange, setDateRange] = useDateRange(websiteId);
const { value, startDate, endDate } = dateRange;
const handleChange = async value => {
- if (value === 'all' && websiteId) {
- const data = await get(`/websites/${websiteId}`);
-
- if (data) {
- const start = new Date(data.createdAt).getTime();
- const end = Date.now();
- setDateRange(`range:${start}:${end}`);
- }
- } else if (value !== 'all') {
- setDateRange(value);
- }
+ setDateRange(value);
};
return (
diff --git a/components/layout/AppLayout.js b/components/layout/AppLayout.js
index 45ba7e23..c30b1018 100644
--- a/components/layout/AppLayout.js
+++ b/components/layout/AppLayout.js
@@ -11,17 +11,14 @@ import styles from './AppLayout.module.css';
export function AppLayout({ title, children }) {
const { user } = useRequireLogin();
const config = useConfig();
- const { pathname } = useRouter();
if (!user || !config) {
return null;
}
- const allowUpdate = user?.isAdmin && !config?.updatesDisabled && !pathname.includes('/share/');
-
return (
- {allowUpdate &&
}
+
{title ? `${title} | umami` : 'umami'}
diff --git a/components/layout/AppLayout.module.css b/components/layout/AppLayout.module.css
index a83039ce..58c1cacf 100644
--- a/components/layout/AppLayout.module.css
+++ b/components/layout/AppLayout.module.css
@@ -8,7 +8,6 @@
.nav {
height: 60px;
width: 100vw;
- z-index: var(--z-index-overlay);
grid-column: 1;
grid-row: 1 / 2;
}
diff --git a/components/messages.js b/components/messages.js
index d41c4430..7bd4e9bc 100644
--- a/components/messages.js
+++ b/components/messages.js
@@ -81,6 +81,7 @@ export const labels = defineMessages({
devices: { id: 'label.devices', defaultMessage: 'Devices' },
countries: { id: 'label.countries', defaultMessage: 'Countries' },
languages: { id: 'label.languages', defaultMessage: 'Languages' },
+ event: { id: 'label.event', defaultMessage: 'Event' },
events: { id: 'label.events', defaultMessage: 'Events' },
query: { id: 'label.query', defaultMessage: 'Query' },
queryParameters: { id: 'label.query-parameters', defaultMessage: 'Query parameters' },
@@ -159,6 +160,8 @@ export const labels = defineMessages({
value: { id: 'labels.value', defaultMessage: 'Value' },
overview: { id: 'labels.overview', defaultMessage: 'Overview' },
totalRecords: { id: 'labels.total-records', defaultMessage: 'Total records' },
+ insights: { id: 'label.insights', defaultMessage: 'Insights' },
+ dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' },
});
export const messages = defineMessages({
@@ -270,4 +273,8 @@ export const messages = defineMessages({
id: 'message.no-event-data',
defaultMessage: 'No event data is available.',
},
+ newVersionAvailable: {
+ id: 'new-version-available',
+ defaultMessage: 'A new version of Umami {version} is available!',
+ },
});
diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js
index e79b977d..64051946 100644
--- a/components/metrics/ActiveUsers.js
+++ b/components/metrics/ActiveUsers.js
@@ -29,7 +29,7 @@ export function ActiveUsers({ websiteId, value, refetchInterval = 60000 }) {
}
return (
-
+
{formatMessage(messages.activeUsers, { x: count })}
);
diff --git a/components/metrics/ActiveUsers.module.css b/components/metrics/ActiveUsers.module.css
index cc231d16..1d87fcd8 100644
--- a/components/metrics/ActiveUsers.module.css
+++ b/components/metrics/ActiveUsers.module.css
@@ -1,10 +1,14 @@
.container {
display: flex;
align-items: center;
+ margin-left: 20px;
}
.text {
display: flex;
+ white-space: nowrap;
+ font-size: var(--font-size-md);
+ font-weight: 400;
}
.value {
diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js
index 362c616e..1b481c48 100644
--- a/components/metrics/PageviewsChart.js
+++ b/components/metrics/PageviewsChart.js
@@ -3,7 +3,7 @@ import BarChart from './BarChart';
import { useLocale, useTheme, useMessages } from 'hooks';
import { renderDateLabels, renderStatusTooltipPopup } from 'lib/charts';
-export function PageviewsChart({ websiteId, data, unit, className, loading, ...props }) {
+export function PageviewsChart({ websiteId, data, unit, loading, ...props }) {
const { formatMessage, labels } = useMessages();
const { colors } = useTheme();
const { locale } = useLocale();
@@ -31,7 +31,6 @@ export function PageviewsChart({ websiteId, data, unit, className, loading, ...p
get('/websites', { userId }));
+ const { data, isLoading, error } = useQuery(['websites'], () =>
+ get('/websites', { userId, includeTeams: 1 }),
+ );
const hasData = data && data.length !== 0;
const { dir } = useLocale();
diff --git a/components/pages/dashboard/DashboardSettingsButton.js b/components/pages/dashboard/DashboardSettingsButton.js
index a963aa5f..99dab14f 100644
--- a/components/pages/dashboard/DashboardSettingsButton.js
+++ b/components/pages/dashboard/DashboardSettingsButton.js
@@ -1,4 +1,4 @@
-import { Menu, Icon, Text, PopupTrigger, Popup, Item, Button } from 'react-basics';
+import { TooltipPopup, Icon, Text, Flexbox, Popup, Item, Button } from 'react-basics';
import Icons from 'components/icons';
import { saveDashboard } from 'store/dashboard';
import useMessages from 'hooks/useMessages';
@@ -6,40 +6,30 @@ import useMessages from 'hooks/useMessages';
export function DashboardSettingsButton() {
const { formatMessage, labels } = useMessages();
- const menuOptions = [
- {
- label: formatMessage(labels.toggleCharts),
- value: 'charts',
- },
- {
- label: formatMessage(labels.editDashboard),
- value: 'order',
- },
- ];
+ const handleToggleCharts = () => {
+ saveDashboard(state => ({ showCharts: !state.showCharts }));
+ };
- function handleSelect(value) {
- if (value === 'charts') {
- saveDashboard(state => ({ showCharts: !state.showCharts }));
- }
- if (value === 'order') {
- saveDashboard({ editing: true });
- }
- }
+ const handleEdit = () => {
+ saveDashboard({ editing: true });
+ };
return (
-
-
+
);
}
diff --git a/components/pages/event-data/EventDataTable.js b/components/pages/event-data/EventDataTable.js
index 2724962f..8260ac35 100644
--- a/components/pages/event-data/EventDataTable.js
+++ b/components/pages/event-data/EventDataTable.js
@@ -13,14 +13,15 @@ export function EventDataTable({ data = [] }) {
return (
+
+ {row => (
+
+ {row.event}
+
+ )}
+
- {row => {
- return (
-
- {row.field}
-
- );
- }}
+ {row => row.field}
{({ total }) => total.toLocaleString()}
diff --git a/components/pages/event-data/EventDataValueTable.js b/components/pages/event-data/EventDataValueTable.js
index 2a20c9b0..2637053e 100644
--- a/components/pages/event-data/EventDataValueTable.js
+++ b/components/pages/event-data/EventDataValueTable.js
@@ -5,14 +5,14 @@ import Icons from 'components/icons';
import PageHeader from 'components/layout/PageHeader';
import Empty from 'components/common/Empty';
-export function EventDataTable({ data = [], field }) {
+export function EventDataValueTable({ data = [], event }) {
const { formatMessage, labels } = useMessages();
const { resolveUrl } = usePageQuery();
const Title = () => {
return (
<>
-
+
@@ -20,7 +20,7 @@ export function EventDataTable({ data = [], field }) {
{formatMessage(labels.back)}
- {field}
+ {event}
>
);
};
@@ -31,6 +31,7 @@ export function EventDataTable({ data = [], field }) {
{data.length <= 0 && }
{data.length > 0 && (
+
{({ total }) => total.toLocaleString()}
@@ -41,4 +42,4 @@ export function EventDataTable({ data = [], field }) {
);
}
-export default EventDataTable;
+export default EventDataValueTable;
diff --git a/components/pages/realtime/RealtimeCountries.js b/components/pages/realtime/RealtimeCountries.js
index 525eb28f..62964eab 100644
--- a/components/pages/realtime/RealtimeCountries.js
+++ b/components/pages/realtime/RealtimeCountries.js
@@ -1,17 +1,26 @@
import { useCallback } from 'react';
+import { useRouter } from 'next/router';
import DataTable from 'components/metrics/DataTable';
import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames';
import useMessages from 'hooks/useMessages';
+import classNames from 'classnames';
+import styles from './RealtimeCountries.module.css';
export function RealtimeCountries({ data }) {
const { formatMessage, labels } = useMessages();
const { locale } = useLocale();
const countryNames = useCountryNames(locale);
+ const { basePath } = useRouter();
const renderCountryName = useCallback(
- ({ x }) => {countryNames[x]},
- [countryNames, locale],
+ ({ x: code }) => (
+
+
+ {countryNames[code]}
+
+ ),
+ [countryNames, locale, basePath],
);
return (
diff --git a/components/pages/realtime/RealtimeCountries.module.css b/components/pages/realtime/RealtimeCountries.module.css
new file mode 100644
index 00000000..e55063c3
--- /dev/null
+++ b/components/pages/realtime/RealtimeCountries.module.css
@@ -0,0 +1,5 @@
+.row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
diff --git a/components/pages/realtime/RealtimeLog.js b/components/pages/realtime/RealtimeLog.js
index fe12963c..744bff00 100644
--- a/components/pages/realtime/RealtimeLog.js
+++ b/components/pages/realtime/RealtimeLog.js
@@ -52,7 +52,7 @@ export function RealtimeLog({ data, websiteDomain }) {
const getTime = ({ createdAt }) => dateFormat(new Date(createdAt), 'pp', locale);
- const getColor = ({ sessionId }) => stringToColor(sessionId);
+ const getColor = ({ id, sessionId }) => stringToColor(sessionId || id);
const getIcon = ({ __type }) => icons[__type];
diff --git a/components/pages/realtime/RealtimePage.js b/components/pages/realtime/RealtimePage.js
index 2d2eceba..507c123c 100644
--- a/components/pages/realtime/RealtimePage.js
+++ b/components/pages/realtime/RealtimePage.js
@@ -93,9 +93,7 @@ export function RealtimePage({ websiteId }) {
-
-
-
+
diff --git a/components/pages/reports/event-data/FieldAddForm.js b/components/pages/reports/FieldAddForm.js
similarity index 86%
rename from components/pages/reports/event-data/FieldAddForm.js
rename to components/pages/reports/FieldAddForm.js
index c95fcac3..e8831247 100644
--- a/components/pages/reports/event-data/FieldAddForm.js
+++ b/components/pages/reports/FieldAddForm.js
@@ -1,10 +1,10 @@
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { REPORT_PARAMETERS } from 'lib/constants';
-import PopupForm from '../PopupForm';
-import FieldSelectForm from '../FieldSelectForm';
-import FieldAggregateForm from '../FieldAggregateForm';
-import FieldFilterForm from '../FieldFilterForm';
+import PopupForm from './PopupForm';
+import FieldSelectForm from './FieldSelectForm';
+import FieldAggregateForm from './FieldAggregateForm';
+import FieldFilterForm from './FieldFilterForm';
import styles from './FieldAddForm.module.css';
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
diff --git a/components/pages/reports/event-data/FieldAddForm.module.css b/components/pages/reports/FieldAddForm.module.css
similarity index 100%
rename from components/pages/reports/event-data/FieldAddForm.module.css
rename to components/pages/reports/FieldAddForm.module.css
diff --git a/components/pages/reports/FieldAggregateForm.js b/components/pages/reports/FieldAggregateForm.js
index cdcbdacc..abd1dbd9 100644
--- a/components/pages/reports/FieldAggregateForm.js
+++ b/components/pages/reports/FieldAggregateForm.js
@@ -19,6 +19,10 @@ export default function FieldAggregateForm({ name, type, onSelect }) {
{ label: formatMessage(labels.total), value: 'total' },
{ label: formatMessage(labels.unique), value: 'unique' },
],
+ uuid: [
+ { label: formatMessage(labels.total), value: 'total' },
+ { label: formatMessage(labels.unique), value: 'unique' },
+ ],
};
const items = options[type];
diff --git a/components/pages/reports/FieldSelectForm.js b/components/pages/reports/FieldSelectForm.js
index 1ff6412a..0e41ea1f 100644
--- a/components/pages/reports/FieldSelectForm.js
+++ b/components/pages/reports/FieldSelectForm.js
@@ -9,10 +9,10 @@ export default function FieldSelectForm({ fields, onSelect }) {