Skip to content

Commit b51a035

Browse files
committed
Merge branch 'main' of github.com:lagossapp/lagoss
2 parents 82285f3 + 146ab48 commit b51a035

20 files changed

Lines changed: 1649 additions & 908 deletions

File tree

packages/dashboard/app/app.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ onErrorCaptured(err => {
2525
@import 'tailwindcss';
2626
@import '@nuxt/ui';
2727
28-
html {
29-
scrollbar-gutter: stable;
30-
}
31-
3228
@layer base {
3329
button:not(:disabled),
3430
[role='button']:not(:disabled) {

packages/dashboard/app/components/AppAnalytics.vue

Lines changed: 197 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,194 @@
11
<template>
2-
<div class="flex w-full justify-between gap-4">
3-
<div class="flex flex-col gap-2">
4-
<span class="text-neutral-500">Requests</span>
5-
<p>
6-
<span class="text-2xl font-bold">{{ formatNumber(requests) }}</span>
7-
<span class="text-neutral-500"> / {{ formatNumber(plan.freeRequests) }}</span>
8-
</p>
2+
<!-- Stats & Performance card -->
3+
<Card class="p-0!">
4+
<!-- Usage section -->
5+
<div class="px-5 pt-5 pb-5">
6+
<div class="mb-4 flex items-center gap-2">
7+
<span class="flex h-5 w-5 items-center justify-center rounded-md bg-teal-100 dark:bg-teal-500/15">
8+
<UIcon name="i-heroicons-chart-bar" class="h-3 w-3 text-teal-600 dark:text-teal-400" />
9+
</span>
10+
<span class="text-[11px] font-semibold uppercase tracking-widest text-neutral-400 dark:text-neutral-500"
11+
>Usage</span
12+
>
13+
</div>
14+
<div class="grid grid-cols-4 divide-x divide-neutral-100 dark:divide-neutral-800">
15+
<div class="flex flex-col gap-1 pr-5">
16+
<span class="text-[11px] uppercase tracking-wide text-neutral-400 dark:text-neutral-500">Requests</span>
17+
<span class="text-2xl font-semibold tabular-nums tracking-tight text-neutral-900 dark:text-neutral-100">{{
18+
formatNumber(requests)
19+
}}</span>
20+
<span class="text-[11px] text-neutral-400 dark:text-neutral-500"
21+
>of {{ formatNumber(plan.freeRequests) }} free</span
22+
>
23+
</div>
24+
<div class="flex flex-col gap-1 px-5">
25+
<span class="text-[11px] uppercase tracking-wide text-neutral-400 dark:text-neutral-500">Avg CPU Time</span>
26+
<span class="text-2xl font-semibold tabular-nums tracking-tight text-neutral-900 dark:text-neutral-100">{{
27+
formatSeconds(cpuTimeAvg)
28+
}}</span>
29+
<span class="text-[11px] text-neutral-400 dark:text-neutral-500"
30+
>/ {{ formatSeconds(plan.totalTimeout / 1000) }} limit</span
31+
>
32+
</div>
33+
<div class="flex flex-col gap-1 px-5">
34+
<span class="text-[11px] uppercase tracking-wide text-neutral-400 dark:text-neutral-500">Avg IN</span>
35+
<span class="text-2xl font-semibold tabular-nums tracking-tight text-neutral-900 dark:text-neutral-100">
36+
{{
37+
formatBytes(
38+
requests && requests > 0 && usage?.length
39+
? usage.reduce((acc, { bytesIn }) => acc + bytesIn, 0) / requests
40+
: 0,
41+
)
42+
}}
43+
</span>
44+
<span class="text-[11px] text-neutral-400 dark:text-neutral-500">per request</span>
45+
</div>
46+
<div class="flex flex-col gap-1 pl-5">
47+
<span class="text-[11px] uppercase tracking-wide text-neutral-400 dark:text-neutral-500">Avg OUT</span>
48+
<span class="text-2xl font-semibold tabular-nums tracking-tight text-neutral-900 dark:text-neutral-100">
49+
{{
50+
formatBytes(
51+
requests && requests > 0 && usage?.length
52+
? usage.reduce((acc, { bytesOut }) => acc + bytesOut, 0) / requests
53+
: 0,
54+
)
55+
}}
56+
</span>
57+
<span class="text-[11px] text-neutral-400 dark:text-neutral-500">per request</span>
58+
</div>
59+
</div>
960
</div>
1061

11-
<div class="flex flex-col gap-2">
12-
<span class="text-neutral-500">Avg. CPU Time</span>
13-
<p>
14-
<span class="text-2xl font-bold">{{ formatSeconds(cpuTimeAvg) }}</span>
15-
<span class="text-neutral-500"> / {{ formatSeconds(plan.totalTimeout / 1000) }}</span>
16-
</p>
62+
<div class="mx-5 h-px bg-neutral-100 dark:bg-neutral-800" />
63+
64+
<!-- Performance section -->
65+
<div class="px-5 pb-5 pt-5">
66+
<div class="mb-4 flex items-center gap-2">
67+
<span class="flex h-5 w-5 items-center justify-center rounded-md bg-violet-100 dark:bg-violet-500/15">
68+
<UIcon name="i-heroicons-bolt" class="h-3 w-3 text-violet-600 dark:text-violet-400" />
69+
</span>
70+
<span class="text-[11px] font-semibold uppercase tracking-widest text-neutral-400 dark:text-neutral-500"
71+
>Performance</span
72+
>
73+
</div>
74+
<div class="grid grid-cols-4 divide-x divide-neutral-100 dark:divide-neutral-800">
75+
<div class="flex flex-col gap-1 pr-5">
76+
<span class="text-[11px] uppercase tracking-wide text-neutral-400 dark:text-neutral-500">Error Rate</span>
77+
<span class="text-2xl font-semibold tabular-nums tracking-tight" :class="errorRateColor">
78+
{{ metrics ? `${metrics.error_rate.toFixed(1)}%` : '—' }}
79+
</span>
80+
</div>
81+
<div class="flex flex-col gap-1 px-5">
82+
<span class="text-[11px] uppercase tracking-wide text-neutral-400 dark:text-neutral-500">p50 Latency</span>
83+
<span class="text-2xl font-semibold tabular-nums tracking-tight text-neutral-900 dark:text-neutral-100">
84+
{{ metrics?.p50 != null ? formatSeconds(metrics.p50 / 1_000_000) : '—' }}
85+
</span>
86+
</div>
87+
<div class="flex flex-col gap-1 px-5">
88+
<span class="text-[11px] uppercase tracking-wide text-neutral-400 dark:text-neutral-500">p95 Latency</span>
89+
<span class="text-2xl font-semibold tabular-nums tracking-tight text-neutral-900 dark:text-neutral-100">
90+
{{ metrics?.p95 != null ? formatSeconds(metrics.p95 / 1_000_000) : '—' }}
91+
</span>
92+
</div>
93+
<div class="flex flex-col gap-1 pl-5">
94+
<span class="text-[11px] uppercase tracking-wide text-neutral-400 dark:text-neutral-500">p99 Latency</span>
95+
<span class="text-2xl font-semibold tabular-nums tracking-tight text-neutral-900 dark:text-neutral-100">
96+
{{ metrics?.p99 != null ? formatSeconds(metrics.p99 / 1_000_000) : '—' }}
97+
</span>
98+
</div>
99+
</div>
17100
</div>
18-
19-
<div class="flex flex-col gap-2">
20-
<span class="text-neutral-500">Avg. IN bytes</span>
21-
<span class="text-2xl font-bold"
22-
>{{
23-
formatBytes(
24-
requests && requests > 0 && usage?.length
25-
? usage.reduce((acc, { bytesIn }) => acc + bytesIn, 0) / requests
26-
: 0,
27-
)
28-
}}
29-
</span>
30-
</div>
31-
32-
<div class="flex flex-col gap-2">
33-
<span class="text-neutral-500">Avg. OUT bytes</span>
34-
<span class="text-2xl font-bold"
35-
>{{
36-
formatBytes(
37-
requests && requests > 0 && usage?.length
38-
? usage.reduce((acc, { bytesOut }) => acc + bytesOut, 0) / requests
39-
: 0,
40-
)
41-
}}
42-
</span>
43-
</div>
44-
</div>
101+
</Card>
45102

46103
<template v-if="showGraphs">
47-
<div>
48-
<h2>Requests</h2>
49-
<Chart
50-
v-if="result && chartLabels"
51-
:labels="chartLabels"
52-
:datasets="[
53-
{
54-
label: 'Requests',
55-
color: '#3B82F6',
56-
data: result.map(({ requests }) => requests),
57-
transform: formatNumber,
58-
},
59-
]"
60-
class="h-72"
61-
/>
62-
</div>
63-
64-
<div>
65-
<h2>CPU Time</h2>
66-
<Chart
67-
v-if="result && chartLabels"
68-
:labels="chartLabels"
69-
:datasets="[
70-
{
71-
label: 'CPU Time',
72-
color: '#F59E0B',
73-
data: result.map(({ cpuTime }) => cpuTime / 1_000_000),
74-
transform: formatSeconds,
75-
},
76-
]"
77-
:axisTransform="(self, ticks) => ticks.map(formatSeconds)"
78-
class="h-72"
79-
/>
80-
</div>
81-
82-
<div>
83-
<h2 class="text-xl">Network</h2>
84-
<Chart
85-
v-if="result && chartLabels"
86-
:labels="chartLabels"
87-
:datasets="[
88-
{
89-
label: 'IN bytes',
90-
color: '#10B981',
91-
data: result.map(({ bytesIn }) => bytesIn),
92-
transform: formatBytes,
93-
},
94-
{
95-
label: 'OUT bytes',
96-
color: '#3B82F6',
97-
data: result.map(({ bytesOut }) => bytesOut),
98-
transform: formatBytes,
99-
},
100-
]"
101-
:axisTransform="(self, ticks) => ticks.map(formatBytes)"
102-
class="h-72"
103-
/>
104-
</div>
104+
<!-- Requests chart -->
105+
<Card class="p-0! overflow-hidden">
106+
<div class="flex items-center gap-2 border-b border-neutral-100 px-5 py-3.5 dark:border-neutral-800">
107+
<span class="flex h-5 w-5 items-center justify-center rounded-md bg-teal-100 dark:bg-teal-500/15">
108+
<UIcon name="i-heroicons-arrow-trending-up" class="h-3 w-3 text-teal-600 dark:text-teal-400" />
109+
</span>
110+
<span class="text-[11px] font-semibold uppercase tracking-widest text-neutral-400 dark:text-neutral-500"
111+
>Requests</span
112+
>
113+
</div>
114+
<div class="p-5">
115+
<Chart
116+
v-if="result && chartLabels"
117+
:labels="chartLabels"
118+
:datasets="[
119+
{
120+
label: 'Requests',
121+
color: '#14b8a6',
122+
data: result.map(({ requests }) => requests),
123+
transform: formatNumber,
124+
},
125+
]"
126+
class="h-64"
127+
/>
128+
</div>
129+
</Card>
130+
131+
<!-- CPU Time chart -->
132+
<Card class="p-0! overflow-hidden">
133+
<div class="flex items-center gap-2 border-b border-neutral-100 px-5 py-3.5 dark:border-neutral-800">
134+
<span class="flex h-5 w-5 items-center justify-center rounded-md bg-amber-100 dark:bg-amber-500/15">
135+
<UIcon name="i-heroicons-cpu-chip" class="h-3 w-3 text-amber-600 dark:text-amber-400" />
136+
</span>
137+
<span class="text-[11px] font-semibold uppercase tracking-widest text-neutral-400 dark:text-neutral-500"
138+
>CPU Time</span
139+
>
140+
</div>
141+
<div class="p-5">
142+
<Chart
143+
v-if="result && chartLabels"
144+
:labels="chartLabels"
145+
:datasets="[
146+
{
147+
label: 'CPU Time',
148+
color: '#f59e0b',
149+
data: result.map(({ cpuTime }) => cpuTime / 1_000_000),
150+
transform: formatSeconds,
151+
},
152+
]"
153+
:axisTransform="(self, ticks) => ticks.map(formatSeconds)"
154+
class="h-64"
155+
/>
156+
</div>
157+
</Card>
158+
159+
<!-- Network chart -->
160+
<Card class="p-0! overflow-hidden">
161+
<div class="flex items-center gap-2 border-b border-neutral-100 px-5 py-3.5 dark:border-neutral-800">
162+
<span class="flex h-5 w-5 items-center justify-center rounded-md bg-emerald-100 dark:bg-emerald-500/15">
163+
<UIcon name="i-heroicons-signal" class="h-3 w-3 text-emerald-600 dark:text-emerald-400" />
164+
</span>
165+
<span class="text-[11px] font-semibold uppercase tracking-widest text-neutral-400 dark:text-neutral-500"
166+
>Network</span
167+
>
168+
</div>
169+
<div class="p-5">
170+
<Chart
171+
v-if="result && chartLabels"
172+
:labels="chartLabels"
173+
:datasets="[
174+
{
175+
label: 'IN bytes',
176+
color: '#10b981',
177+
data: result.map(({ bytesIn }) => bytesIn),
178+
transform: formatBytes,
179+
},
180+
{
181+
label: 'OUT bytes',
182+
color: '#14b8a6',
183+
data: result.map(({ bytesOut }) => bytesOut),
184+
transform: formatBytes,
185+
},
186+
]"
187+
:axisTransform="(self, ticks) => ticks.map(formatBytes)"
188+
class="h-64"
189+
/>
190+
</div>
191+
</Card>
105192
</template>
106193
</template>
107194

@@ -156,6 +243,18 @@ const { data: usage } = await useFetch(() => `/api/apps/${app.value.id}/usage`,
156243
query: computed(() => ({ timeframe: timeframe.value })),
157244
});
158245
246+
// TODO: refresh data every 10 seconds
247+
const { data: metrics } = await useFetch(() => `/api/apps/${app.value.id}/metrics`, {
248+
query: computed(() => ({ timeframe: timeframe.value })),
249+
});
250+
251+
const errorRateColor = computed(() => {
252+
const rate = metrics.value?.error_rate ?? 0;
253+
if (rate >= 5) return 'text-red-500 dark:text-red-400';
254+
if (rate >= 1) return 'text-yellow-500 dark:text-yellow-400';
255+
return 'text-emerald-500 dark:text-emerald-400';
256+
});
257+
159258
const requests = computed(() => usage.value?.reduce((acc, { requests }) => acc + requests, 0));
160259
161260
const cpuTimeAvg = computed(() => {

0 commit comments

Comments
 (0)