diff --git a/dashboard/app.vue b/dashboard/app.vue
index 8bef9f1..e4fce55 100644
--- a/dashboard/app.vue
+++ b/dashboard/app.vue
@@ -36,7 +36,7 @@ const { showDialog, closeDialog, dialogComponent, dialogParams, dialogStyle, dia
+ class="absolute bottom-8 right-4 bg-red-400 text-white text-[.9rem] font-bold px-4 py-[.2rem] rounded-lg z-[100]">
XS
SM - MOBILE
MD - TABLET
diff --git a/dashboard/components/CVerticalNavigation.vue b/dashboard/components/CVerticalNavigation.vue
index f671e6c..51736d1 100644
--- a/dashboard/components/CVerticalNavigation.vue
+++ b/dashboard/components/CVerticalNavigation.vue
@@ -9,7 +9,8 @@ export type Entry = {
icon?: string,
action?: () => any,
adminOnly?: boolean,
- external?: boolean
+ external?: boolean,
+ grow?: boolean
}
export type Section = {
@@ -66,24 +67,35 @@ async function deleteSnapshot(close: () => any) {
async function generatePDF() {
-try {
- const res = await $fetch
('/api/project/generate_pdf', {
- ...signHeaders(),
- responseType: 'blob'
- });
+ try {
+ const res = await $fetch('/api/project/generate_pdf', {
+ ...signHeaders(),
+ responseType: 'blob'
+ });
- const url = URL.createObjectURL(res);
- const a = document.createElement('a');
- a.href = url;
- a.download = `Report.pdf`;
- a.click();
- URL.revokeObjectURL(url);
-} catch (ex: any) {
- alert(ex.message);
+ const url = URL.createObjectURL(res);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `Report.pdf`;
+ a.click();
+ URL.revokeObjectURL(url);
+ } catch (ex: any) {
+ alert(ex.message);
+ }
}
+
+const { setToken } = useAccessToken();
+const router = useRouter();
+
+function onLogout() {
+ console.log('LOGOUT')
+ setToken('');
+ setLoggedUser(undefined);
+ router.push('/login');
}
+
@@ -95,10 +107,14 @@ try {
-
+
+
+
+
+
+
PROJECT SELECTOR
@@ -175,7 +191,7 @@ try {
-
+
@@ -185,7 +201,7 @@ try {
class="bg-lyx-background text-gray-300 py-2 px-4 rounded-lg" :class="{
'text-gray-700 pointer-events-none': entry.disabled,
'bg-lyx-background-lighter': route.path == (entry.to || '#'),
- 'hover:bg-lyx-background-light': route.path != (entry.to || '#')
+ 'hover:bg-lyx-background-light': route.path != (entry.to || '#'),
}">
+
+
+
diff --git a/dashboard/components/dashboard/EventsChart.vue b/dashboard/components/dashboard/EventsChart.vue
index 2a5e2d9..bbf2e87 100644
--- a/dashboard/components/dashboard/EventsChart.vue
+++ b/dashboard/components/dashboard/EventsChart.vue
@@ -55,7 +55,7 @@ const chartData = ref
>({
{
rotation: 1,
data: [],
- backgroundColor: ['#6bbbe3','#5655d0', '#a6d5cb', '#fae0b9'],
+ backgroundColor: ['#6bbbe3', '#5655d0', '#a6d5cb', '#fae0b9'],
borderColor: ['#1d1d1f'],
borderWidth: 2
},
@@ -65,27 +65,45 @@ const chartData = ref>({
const { doughnutChartProps, doughnutChartRef } = useDoughnutChart({ chartData: chartData, options: chartOptions });
+
+const res = useEventsData();
+
onMounted(async () => {
- const activeProject = useActiveProject()
+ res.onResponse(resData => {
+ if (!resData.value) return;
- const eventsData = await $fetch(`/api/metrics/${activeProject.value?._id}/visits/events`, signHeaders());
- chartData.value.labels = eventsData.map(e => {
- return `${e._id}`;
- });
- chartData.value.datasets[0].data = eventsData.map(e => e.count);
- doughnutChartRef.value?.update();
+ chartData.value.labels = resData.value.map(e => {
+ return `${e._id}`;
+ });
- if (window.innerWidth < 800) {
- if (chartOptions.value?.plugins?.legend?.display) {
- chartOptions.value.plugins.legend.display = false;
+ chartData.value.datasets[0].data = resData.value.map(e => e.count);
+ doughnutChartRef.value?.update();
+
+ if (window.innerWidth < 800) {
+ if (chartOptions.value?.plugins?.legend?.display) {
+ chartOptions.value.plugins.legend.display = false;
+ }
}
- }
+
+ });
+})
+
+
+const chartVisible = computed(() => {
+ if (res.pending.value) return false;
+ if (!res.data.value) return false;
+ return true;
})
-
+
diff --git a/dashboard/components/events/EventsStackedBarChart.vue b/dashboard/components/events/EventsStackedBarChart.vue
index 3ad09ae..369e60a 100644
--- a/dashboard/components/events/EventsStackedBarChart.vue
+++ b/dashboard/components/events/EventsStackedBarChart.vue
@@ -1,52 +1,74 @@
\ No newline at end of file
diff --git a/dashboard/pages/plans.vue b/dashboard/components/settings/billing.vue
similarity index 100%
rename from dashboard/pages/plans.vue
rename to dashboard/components/settings/billing.vue
diff --git a/dashboard/pages/members.vue b/dashboard/components/settings/members.vue
similarity index 100%
rename from dashboard/pages/members.vue
rename to dashboard/components/settings/members.vue
diff --git a/dashboard/composables/useDataService.ts b/dashboard/composables/useDataService.ts
index 1517f27..c7826e0 100644
--- a/dashboard/composables/useDataService.ts
+++ b/dashboard/composables/useDataService.ts
@@ -46,8 +46,8 @@ export function useFirstInteractionData() {
}
-export function useTimelineAdvanced(endpoint: string, slice: Ref, customBody: Object = {}) {
- const response = useCustomFetch<{ _id: string, count: number }[]>(
+export function useTimelineAdvanced(endpoint: string, slice: Ref, customBody: Object = {}) {
+ const response = useCustomFetch(
`/api/metrics/${activeProject.value?._id}/timeline/${endpoint}`,
() => signHeaders({ 'Content-Type': 'application/json' }).headers, {
method: 'POST',
@@ -59,12 +59,16 @@ export function useTimelineAdvanced(endpoint: string, slice: Ref, customB
}
-export function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers', slice: Ref) {
- return useTimelineAdvanced(endpoint, slice);
+export function useTimeline(endpoint: 'visits' | 'sessions' | 'referrers' | 'events_stacked', slice: Ref) {
+ return useTimelineAdvanced<{ _id: string, count: number }[]>(endpoint, slice);
}
export async function useReferrersTimeline(referrer: string, slice: Ref) {
- return await useTimelineAdvanced('referrers', slice, { referrer });
+ return await useTimelineAdvanced<{ _id: string, count: number }[]>('referrers', slice, { referrer });
+}
+
+export function useEventsStackedTimeline(slice: Ref) {
+ return useTimelineAdvanced<{ _id: string, name: string, count: number }[]>('events_stacked', slice);
}
@@ -162,4 +166,5 @@ export function useDevicesData(limit: number = 10) {
{ lazy: false, watchProps: [snapshot] }
);
return res;
-}
\ No newline at end of file
+}
+
diff --git a/dashboard/layouts/dashboard.vue b/dashboard/layouts/dashboard.vue
index 718df33..9b937f0 100644
--- a/dashboard/layouts/dashboard.vue
+++ b/dashboard/layouts/dashboard.vue
@@ -12,7 +12,7 @@ const sections: Section[] = [
title: 'General',
entries: [
{ label: 'Projects', icon: 'far fa-table-layout', to: '/project_selector' },
- { label: 'Members', icon: 'far fa-users', to: '/members' },
+ // { label: 'Members', icon: 'far fa-users', to: '/members' },
{ label: 'Admin', icon: 'fas fa-cat', adminOnly: true, to: '/admin' },
]
},
@@ -22,6 +22,7 @@ const sections: Section[] = [
{ label: 'Dashboard', to: '/', icon: 'far fa-home' },
{ label: 'Events', to: '/events', icon: 'far fa-bolt' },
{ label: 'Analyst', to: '/analyst', icon: 'far fa-microchip-ai' },
+ { label: 'Settings', to: '/settings', icon: 'far fa-gear' },
// { label: 'Report', to: '/report', icon: 'far fa-notes' },
// { label: 'AI', to: '/dashboard/settings', icon: 'far fa-robot brightness-[.4]' },
// { label: 'Visits', to: '/dashboard/visits', icon: 'far fa-eye' },
@@ -39,23 +40,8 @@ const sections: Section[] = [
label: 'Github', to: 'https://github.com/litlyx/litlyx', icon: 'fab fa-github', external: true,
action() { Lit.event('git_clicked') },
},
- { label: 'Billing', to: '/plans', icon: 'far fa-wallet' },
- { label: 'Book a demo', to: '/book_demo', icon: 'far fa-calendar' },
- ]
- },
- {
- title: 'Actions',
- entries: [
- {
- label: 'Logout',
- icon: 'far fa-arrow-right-from-bracket',
- action: () => {
- console.log('LOGOUT')
- setToken('');
- setLoggedUser(undefined);
- router.push('/login');
- }
- },
+ // { label: 'Billing', to: '/plans', icon: 'far fa-wallet' },
+ // { label: 'Book a demo', to: '/book_demo', icon: 'far fa-calendar' },
]
}
];
diff --git a/dashboard/pages/events.vue b/dashboard/pages/events.vue
index c1dcc66..a719d7e 100644
--- a/dashboard/pages/events.vue
+++ b/dashboard/pages/events.vue
@@ -17,7 +17,8 @@ const eventsStackedSelectIndex = ref(0);
-
+
+
@@ -29,19 +30,10 @@ const eventsStackedSelectIndex = ref(0);
-
-
-
- Top events
-
-
- Displays key events.
-
-
-
+
+
-
diff --git a/dashboard/pages/index.vue b/dashboard/pages/index.vue
index 0d02b24..d89cfaf 100644
--- a/dashboard/pages/index.vue
+++ b/dashboard/pages/index.vue
@@ -70,10 +70,6 @@ const selectLabels = [
// { label: 'Month', value: 'month' },
];
-function testAlert() {
- createAlert('test', 'test', 'fas fa-home', 40000);
-}
-
@@ -99,7 +95,7 @@ function testAlert() {
-
+
@@ -111,7 +107,7 @@ function testAlert() {
-
+
diff --git a/dashboard/pages/settings.vue b/dashboard/pages/settings.vue
new file mode 100644
index 0000000..b87c7c5
--- /dev/null
+++ b/dashboard/pages/settings.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
Settings
+
+
+
+ TODO
+
+
+
+
+
+
+
+
+ TODO
+
+
+
+
\ No newline at end of file
diff --git a/dashboard/server/api/metrics/[project_id]/timeline/events.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/events.post.ts
index c791cd6..36ed24d 100644
--- a/dashboard/server/api/metrics/[project_id]/timeline/events.post.ts
+++ b/dashboard/server/api/metrics/[project_id]/timeline/events.post.ts
@@ -15,8 +15,8 @@ export default defineEventHandler(async event => {
const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
- if (!from) return setResponseStatus(event, 400, 'to is required');
- if (!from) return setResponseStatus(event, 400, 'slice is required');
+ if (!to) return setResponseStatus(event, 400, 'to is required');
+ if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:events:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
diff --git a/dashboard/server/api/metrics/[project_id]/timeline/events_stacked.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/events_stacked.post.ts
index b01cf04..3f5c5b1 100644
--- a/dashboard/server/api/metrics/[project_id]/timeline/events_stacked.post.ts
+++ b/dashboard/server/api/metrics/[project_id]/timeline/events_stacked.post.ts
@@ -2,6 +2,7 @@ import { EventModel } from "@schema/metrics/EventSchema";
import { getTimeline } from "./generic";
import { Redis, TIMELINE_EXPIRE_TIME } from "~/server/services/CacheService";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
+import { executeAdvancedTimelineAggregation } from "~/server/services/TimelineService";
export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event);
@@ -11,16 +12,23 @@ export default defineEventHandler(async event => {
if (!project) return;
- const { slice, duration } = await readBody(event);
+ const { slice, from, to } = await readBody(event);
+
+ if (!from) return setResponseStatus(event, 400, 'from is required');
+ if (!to) return setResponseStatus(event, 400, 'to is required');
+ if (!slice) return setResponseStatus(event, 400, 'slice is required');
- return await Redis.useCache({ key: `timeline:events_stacked:${project_id}:${slice}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
- const timelineStackedEvents = await getTimeline(EventModel, project_id, slice, duration,
- {},
- {},
- { name: "$_id.name" },
- { name: '$name' }
- );
+ return await Redis.useCache({ key: `timeline:events_stacked:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`, exp: TIMELINE_EXPIRE_TIME }, async () => {
+
+ const timelineStackedEvents = await executeAdvancedTimelineAggregation<{ name: String }>({
+ model: EventModel,
+ projectId: project._id,
+ from, to, slice,
+ customProjection: { name: "$_id.name" },
+ customIdGroup: { name: '$name' },
+ })
+
return timelineStackedEvents;
});
diff --git a/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts
index 7302c68..813ef6e 100644
--- a/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts
+++ b/dashboard/server/api/metrics/[project_id]/timeline/referrers.post.ts
@@ -16,8 +16,8 @@ export default defineEventHandler(async event => {
const { slice, from, to, referrer } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
- if (!from) return setResponseStatus(event, 400, 'to is required');
- if (!from) return setResponseStatus(event, 400, 'slice is required');
+ if (!to) return setResponseStatus(event, 400, 'to is required');
+ if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:referrers:${referrer}:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
diff --git a/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts
index c81db86..3a776a7 100644
--- a/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts
+++ b/dashboard/server/api/metrics/[project_id]/timeline/sessions.post.ts
@@ -16,8 +16,8 @@ export default defineEventHandler(async event => {
const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
- if (!from) return setResponseStatus(event, 400, 'to is required');
- if (!from) return setResponseStatus(event, 400, 'slice is required');
+ if (!to) return setResponseStatus(event, 400, 'to is required');
+ if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:sessions:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
diff --git a/dashboard/server/api/metrics/[project_id]/timeline/sessions_duration.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/sessions_duration.post.ts
index a5fb3b5..46b44fe 100644
--- a/dashboard/server/api/metrics/[project_id]/timeline/sessions_duration.post.ts
+++ b/dashboard/server/api/metrics/[project_id]/timeline/sessions_duration.post.ts
@@ -27,8 +27,8 @@ export default defineEventHandler(async event => {
const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
- if (!from) return setResponseStatus(event, 400, 'to is required');
- if (!from) return setResponseStatus(event, 400, 'slice is required');
+ if (!to) return setResponseStatus(event, 400, 'to is required');
+ if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:sessions_duration:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
diff --git a/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts b/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts
index a87e35c..0e49ec2 100644
--- a/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts
+++ b/dashboard/server/api/metrics/[project_id]/timeline/visits.post.ts
@@ -17,8 +17,8 @@ export default defineEventHandler(async event => {
const { slice, from, to } = await readBody(event);
if (!from) return setResponseStatus(event, 400, 'from is required');
- if (!from) return setResponseStatus(event, 400, 'to is required');
- if (!from) return setResponseStatus(event, 400, 'slice is required');
+ if (!to) return setResponseStatus(event, 400, 'to is required');
+ if (!slice) return setResponseStatus(event, 400, 'slice is required');
return await Redis.useCache({
key: `timeline:visits:${project_id}:${slice}:${from || 'none'}:${to || 'none'}`,
diff --git a/dashboard/server/services/TimelineService.ts b/dashboard/server/services/TimelineService.ts
index e8518aa..88c6e1c 100644
--- a/dashboard/server/services/TimelineService.ts
+++ b/dashboard/server/services/TimelineService.ts
@@ -16,14 +16,16 @@ export type TimelineAggregationOptions = {
export type AdvancedTimelineAggregationOptions = TimelineAggregationOptions & {
customMatch?: Record,
customGroup?: Record,
- customProjection?: Record
+ customProjection?: Record,
+ customIdGroup?: Record
}
-export async function executeAdvancedTimelineAggregation(options: AdvancedTimelineAggregationOptions) {
+export async function executeAdvancedTimelineAggregation(options: AdvancedTimelineAggregationOptions) {
options.customMatch = options.customMatch || {};
options.customGroup = options.customGroup || {};
options.customProjection = options.customProjection || {};
+ options.customIdGroup = options.customIdGroup || {};
const { group, sort, fromParts } = DateService.getQueryDateRange(options.slice);
@@ -35,7 +37,7 @@ export async function executeAdvancedTimelineAggregation(options: AdvancedTimeli
...options.customMatch
}
},
- { $group: { _id: group, count: { $sum: 1 }, ...options.customGroup } },
+ { $group: { _id: { ...group, ...options.customIdGroup }, count: { $sum: 1 }, ...options.customGroup } },
{ $sort: sort },
{ $project: { _id: { $dateFromParts: fromParts }, count: "$count", ...options.customProjection } }
]
@@ -44,7 +46,7 @@ export async function executeAdvancedTimelineAggregation(options: AdvancedTimeli
console.log(JSON.stringify(aggregation, null, 2));
}
- const timeline: { _id: string, count: number }[] = await options.model.aggregate(aggregation);
+ const timeline: { _id: string, count: number & T }[] = await options.model.aggregate(aggregation);
return timeline;
diff --git a/dashboard/utils/DateUtils.ts b/dashboard/utils/DateUtils.ts
index be4a987..bff919a 100644
--- a/dashboard/utils/DateUtils.ts
+++ b/dashboard/utils/DateUtils.ts
@@ -60,7 +60,6 @@ export function fixMetrics(result: { data: MetricsTimeline[], from: string, to:
}
}
-
const allKeys = !options.advanced ? [] : Array.from(new Set(result.data.map((e: any) => e[options.advancedGroupKey])).values());
const fixed: any[] = allDates.map(matchDate => {
@@ -85,6 +84,8 @@ export function fixMetrics(result: { data: MetricsTimeline[], from: string, to:
return returnObject;
});
+ console.log({ allKeys })
+
if (slice === 'day' || slice == 'hour') fixed.pop();
const data = fixed.map(e => e.count);