Skip to content

Commit 7686d2b

Browse files
committed
Improved UI
1 parent 138541d commit 7686d2b

2 files changed

Lines changed: 45 additions & 20 deletions

File tree

apps/backend/src/lib/sign-up-rules.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ async function logRuleTrigger(
2525
action: action.type,
2626
userId: userId ?? null,
2727
email: context.email,
28-
emailDomain: context.emailDomain,
2928
authMethod: context.authMethod,
3029
oauthProvider: context.oauthProvider,
3130
});

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ import { ArrowsDownUpIcon, CheckIcon, PencilSimpleIcon, PlusIcon, TrashIcon, XIc
3131
import type { CompleteConfig } from "@stackframe/stack-shared/dist/config/schema";
3232
import { useAsyncCallback } from "@stackframe/stack-shared/dist/hooks/use-async-callback";
3333
import type { SignUpRule, SignUpRuleAction } from "@stackframe/stack-shared/dist/interface/crud/sign-up-rules";
34+
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
3435
import { typedEntries } from "@stackframe/stack-shared/dist/utils/objects";
3536
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
3637
import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
3738
import React, { useMemo, useState } from "react";
38-
import { Area, AreaChart, ResponsiveContainer } from "recharts";
39+
import { Area, AreaChart, ResponsiveContainer, YAxis } from "recharts";
3940
import { AppEnabledGuard } from "../app-enabled-guard";
4041
import { PageLayout } from "../page-layout";
4142
import { useAdminApp } from "../use-admin-app";
@@ -65,18 +66,32 @@ type ConfigWithSignUpRules = CompleteConfig & {
6566
function RuleSparkline({
6667
data,
6768
totalCount,
69+
isLoading,
6870
}: {
6971
data: { hour: string, count: number }[],
7072
totalCount: number,
73+
isLoading: boolean,
7174
}) {
72-
if (data.length === 0 || totalCount === 0) {
73-
return null;
75+
// Show skeleton while loading
76+
if (isLoading) {
77+
return (
78+
<div className="flex items-center gap-1">
79+
<div className="w-10 h-4 bg-muted animate-pulse rounded" />
80+
<div className="w-4 h-3 bg-muted animate-pulse rounded" />
81+
</div>
82+
);
7483
}
7584

85+
// Ensure we have at least 2 data points for the chart to render a line
86+
const chartData = data.length >= 2 ? data : [{ hour: '0', count: 0 }, { hour: '1', count: 0 }];
87+
// Calculate max for Y domain - use at least 1 to avoid divide-by-zero
88+
const maxCount = Math.max(1, ...chartData.map(d => d.count));
89+
7690
return (
77-
<div className="flex items-center gap-1">
78-
<ResponsiveContainer width={40} height={20}>
79-
<AreaChart data={data} margin={{ top: 2, right: 0, bottom: 2, left: 0 }}>
91+
<div className="flex items-center gap-1" title="past 48h">
92+
<ResponsiveContainer width={40} height={16}>
93+
<AreaChart data={chartData} margin={{ top: 2, right: 0, bottom: 2, left: 0 }}>
94+
<YAxis hide domain={[0, maxCount]} />
8095
<Area
8196
type="monotone"
8297
dataKey="count"
@@ -85,6 +100,7 @@ function RuleSparkline({
85100
fill="currentColor"
86101
fillOpacity={0.15}
87102
className="text-muted-foreground"
103+
isAnimationActive={false}
88104
/>
89105
</AreaChart>
90106
</ResponsiveContainer>
@@ -245,6 +261,7 @@ function RuleEditor({
245261
function SortableRuleRow({
246262
entry,
247263
analytics,
264+
isAnalyticsLoading,
248265
isEditing,
249266
onEdit,
250267
onDelete,
@@ -254,6 +271,7 @@ function SortableRuleRow({
254271
}: {
255272
entry: SignUpRuleEntry,
256273
analytics?: RuleAnalytics,
274+
isAnalyticsLoading: boolean,
257275
isEditing: boolean,
258276
onEdit: () => void,
259277
onDelete: () => void,
@@ -364,15 +382,14 @@ function SortableRuleRow({
364382
onClick={(e) => e.stopPropagation()}
365383
onPointerDown={(e) => e.stopPropagation()}
366384
>
367-
{/* Sparkline chart inline */}
368-
{analytics && analytics.totalCount > 0 && (
369-
<div className="hidden sm:flex items-center mr-1" title={`${analytics.totalCount} triggers in last 48h`}>
370-
<RuleSparkline
371-
data={analytics.hourlyCounts}
372-
totalCount={analytics.totalCount}
373-
/>
374-
</div>
375-
)}
385+
{/* Sparkline and trigger count */}
386+
<div className="hidden sm:flex items-center mr-1">
387+
<RuleSparkline
388+
data={analytics?.hourlyCounts ?? []}
389+
totalCount={analytics?.totalCount ?? 0}
390+
isLoading={isAnalyticsLoading}
391+
/>
392+
</div>
376393
<Button
377394
variant="ghost"
378395
size="sm"
@@ -482,9 +499,11 @@ function DeleteRuleDialog({
482499
function useSignUpRulesAnalytics() {
483500
const stackAdminApp = useAdminApp();
484501
const [analytics, setAnalytics] = useState<Map<string, RuleAnalytics>>(new Map());
502+
const [isLoading, setIsLoading] = useState(true);
485503

486504
React.useEffect(() => {
487505
let cancelled = false;
506+
setIsLoading(true);
488507

489508
const fetchAnalytics = async () => {
490509
const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest(
@@ -494,18 +513,23 @@ function useSignUpRulesAnalytics() {
494513
);
495514
if (cancelled) return;
496515

516+
if (!response.ok) {
517+
throw new StackAssertionError(`Failed to fetch sign-up rules stats: ${response.status} ${response.statusText}`);
518+
}
519+
497520
const data = await response.json();
498521

499522
const analyticsMap = new Map<string, RuleAnalytics>();
500523
for (const trigger of data.rule_triggers ?? []) {
501524
analyticsMap.set(trigger.rule_id, {
502525
ruleId: trigger.rule_id,
503526
totalCount: trigger.total_count,
504-
hourlyCounts: trigger.hourly_counts,
527+
hourlyCounts: trigger.hourly_counts ?? [],
505528
});
506529
}
507530

508531
setAnalytics(analyticsMap);
532+
setIsLoading(false);
509533
};
510534

511535
runAsynchronouslyWithAlert(fetchAnalytics);
@@ -515,7 +539,7 @@ function useSignUpRulesAnalytics() {
515539
};
516540
}, [stackAdminApp]);
517541

518-
return { analytics };
542+
return { analytics, isLoading };
519543
}
520544

521545
export default function PageClient() {
@@ -531,14 +555,14 @@ export default function PageClient() {
531555
const [ruleToDelete, setRuleToDelete] = useState<SignUpRuleEntry | null>(null);
532556

533557
// Fetch analytics data
534-
const { analytics: ruleAnalytics } = useSignUpRulesAnalytics();
558+
const { analytics: ruleAnalytics, isLoading: isAnalyticsLoading } = useSignUpRulesAnalytics();
535559

536560
// Type assertion needed because schema changes take effect at build time
537561
const configWithRules = config as ConfigWithSignUpRules;
538562

539563
// Server state (source of truth)
540564
const serverRules = useMemo(() =>
541-
typedEntries(configWithRules.auth.signUpRules ?? {}).map(([id, rule]) => ({ id, rule })),
565+
typedEntries(configWithRules.auth.signUpRules).map(([id, rule]) => ({ id, rule })),
542566
[configWithRules.auth.signUpRules]
543567
);
544568
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- TypeScript may not see these as optional due to type assertion
@@ -753,6 +777,7 @@ export default function PageClient() {
753777
key={entry.id}
754778
entry={entry}
755779
analytics={ruleAnalytics.get(entry.id)}
780+
isAnalyticsLoading={isAnalyticsLoading}
756781
isEditing={editingRuleId === entry.id}
757782
onEdit={() => {
758783
setEditingRuleId(entry.id);
@@ -789,6 +814,7 @@ export default function PageClient() {
789814
key={entry.id}
790815
entry={entry}
791816
analytics={ruleAnalytics.get(entry.id)}
817+
isAnalyticsLoading={isAnalyticsLoading}
792818
isEditing={editingRuleId === entry.id}
793819
onEdit={() => {
794820
setEditingRuleId(entry.id);

0 commit comments

Comments
 (0)