Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"@sveltejs/kit": "catalog:",
"@sveltejs/vite-plugin-svelte": "catalog:",
"@tailwindcss/vite": "catalog:",
"@types/d3-scale": "^4.0.9",
"@types/d3-shape": "^3.1.7",
"@types/node": "catalog:",
"dotenv-cli": "catalog:",
"eslint": "catalog:",
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/lib/components/EventsList.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script lang="ts">
import { useQuery } from "$lib/zero/client.svelte";
import { recentEvents } from "$lib/zero/queries";
import type { Event } from "$lib/zero/schema";
import { formatTimestamp, formatProperties } from "$lib/utils/formatters";

let { projectId }: { projectId: string } = $props();

// Fetch a larger set and paginate client-side
// Use getter function so query re-runs if projectId changes
const eventsQuery = useQuery<Event>(() => recentEvents(projectId, 500));
// Types are inferred automatically from the query
const eventsQuery = useQuery(() => recentEvents(projectId, 500));

// Client-side pagination
let displayLimit = $state(100);
Expand Down
7 changes: 4 additions & 3 deletions apps/dashboard/src/lib/components/FlagsList.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script lang="ts">
import { useQuery, getZero } from "$lib/zero/client.svelte";
import { flagsForProject } from "$lib/zero/queries";
import type { Flag } from "$lib/zero/schema";
import { formatTimestamp } from "$lib/utils/formatters";

let { projectId }: { projectId: string } = $props();

const zero = getZero();
// Use getter function so query re-runs if projectId changes
const flagsQuery = useQuery<Flag>(() => flagsForProject(projectId));
// Types inferred automatically from the query
const flagsQuery = useQuery(() => flagsForProject(projectId));

// Form state
let newFlagKey = $state("");
Expand Down Expand Up @@ -98,11 +98,12 @@
<td class="p-4">
<button
onclick={() => toggleFlag(flag.id, flag.enabled)}
aria-label={flag.enabled ? `Disable ${flag.name}` : `Enable ${flag.name}`}
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {flag.enabled ? 'bg-rp-pine' : 'bg-rp-overlay'}"
>
<span
class="inline-block h-4 w-4 transform rounded-full bg-rp-text transition-transform {flag.enabled ? 'translate-x-6' : 'translate-x-1'}"
/>
></span>
</button>
</td>
<td class="p-4 text-rp-subtle text-sm">
Expand Down
10 changes: 5 additions & 5 deletions apps/dashboard/src/lib/components/OverviewDashboard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
enabledFlags,
recentEvents,
} from "$lib/zero/queries";
import type { Event, Session, Flag } from "$lib/zero/schema";
import EventsOverTimeChart from "./charts/EventsOverTimeChart.svelte";
import SessionsTrendChart from "./charts/SessionsTrendChart.svelte";
import TopEventsChart from "./charts/TopEventsChart.svelte";
Expand All @@ -18,10 +17,11 @@
let days = $state(7);

// Reactive synced queries - use getter functions so queries re-run when params change
const eventsQuery = useQuery<Event>(() => eventsInWindow(projectId, days));
const sessionsQuery = useQuery<Session>(() => sessionsInWindow(projectId, days));
const flagsQuery = useQuery<Flag>(() => enabledFlags(projectId));
const recentQuery = useQuery<Event>(() => recentEvents(projectId, 10));
// Types are inferred automatically from the queries
const eventsQuery = useQuery(() => eventsInWindow(projectId, days));
const sessionsQuery = useQuery(() => sessionsInWindow(projectId, days));
const flagsQuery = useQuery(() => enabledFlags(projectId));
const recentQuery = useQuery(() => recentEvents(projectId, 10));

// Computed values
const loading = $derived(
Expand Down
6 changes: 3 additions & 3 deletions apps/dashboard/src/lib/components/SessionDetail.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<script lang="ts">
import { useQuery, useQueryOne } from "$lib/zero/client.svelte";
import { sessionById, eventsForSession } from "$lib/zero/queries";
import type { Session, Event } from "$lib/zero/schema";
import { formatTimestamp, formatProperties } from "$lib/utils/formatters";

let { sessionId }: { sessionId: string } = $props();

// Use getter functions so queries re-run if sessionId changes
const sessionQuery = useQueryOne<Session>(() => sessionById(sessionId));
const eventsQuery = useQuery<Event>(() => eventsForSession(sessionId));
// Types are inferred automatically from the queries
const sessionQuery = useQueryOne(() => sessionById(sessionId));
const eventsQuery = useQuery(() => eventsForSession(sessionId));

const loading = $derived(sessionQuery.loading || eventsQuery.loading);
const session = $derived(sessionQuery.data);
Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/lib/components/SessionsList.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script lang="ts">
import { useQuery } from "$lib/zero/client.svelte";
import { recentSessions } from "$lib/zero/queries";
import type { Session } from "$lib/zero/schema";
import { formatTimestamp, formatRelativeTime } from "$lib/utils/formatters";

let { projectId }: { projectId: string } = $props();

// Use getter function so query re-runs if projectId changes
const sessionsQuery = useQuery<Session>(() => recentSessions(projectId, 100));
// Types inferred automatically from the query
const sessionsQuery = useQuery(() => recentSessions(projectId, 100));
</script>

<div class="bg-rp-surface rounded-lg border border-rp-overlay">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script lang="ts">
import { Chart, Svg, Axis, Area, Spline, Tooltip, Highlight } from "layerchart";
import { Chart, Svg, Axis, Area, Spline, Highlight } from "layerchart";
import { scaleTime, scaleLinear } from "d3-scale";
import { curveMonotoneX } from "d3-shape";
import type { Event } from "$lib/zero/schema";

let { events = [], days = 7 }: { events?: Event[]; days?: number } = $props();
// Accept any event-like object with timestamp field
type EventLike = { timestamp?: number };
let { events = [], days = 7 }: { events?: EventLike[]; days?: number } = $props();

// Aggregate events by day
const data = $derived.by(() => {
Expand Down Expand Up @@ -80,16 +81,6 @@
<Spline class="stroke-2 stroke-rp-iris" curve={curveMonotoneX} />
<Highlight points={{ class: "fill-rp-iris" }} lines />
</Svg>
<Tooltip.Root let:data>
{#if data}
<Tooltip.Header>
{data.date.toLocaleDateString()}
</Tooltip.Header>
<Tooltip.List>
<Tooltip.Item label="Events" value={data.count} />
</Tooltip.List>
{/if}
</Tooltip.Root>
</Chart>
{:else}
<div class="h-full flex items-center justify-center text-rp-muted">
Expand Down
17 changes: 4 additions & 13 deletions apps/dashboard/src/lib/components/charts/SessionsTrendChart.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script lang="ts">
import { Chart, Svg, Axis, Spline, Tooltip, Highlight } from "layerchart";
import { Chart, Svg, Axis, Spline, Highlight } from "layerchart";
import { scaleTime, scaleLinear } from "d3-scale";
import { curveMonotoneX } from "d3-shape";
import type { Session } from "$lib/zero/schema";

let { sessions = [], days = 7 }: { sessions?: Session[]; days?: number } = $props();
// Accept any session-like object with started_at field
type SessionLike = { started_at?: number };
let { sessions = [], days = 7 }: { sessions?: SessionLike[]; days?: number } = $props();

// Aggregate sessions by day
const data = $derived.by(() => {
Expand Down Expand Up @@ -75,16 +76,6 @@
<Spline class="stroke-2 stroke-rp-foam" curve={curveMonotoneX} />
<Highlight points={{ class: "fill-rp-foam" }} lines />
</Svg>
<Tooltip.Root let:data>
{#if data}
<Tooltip.Header>
{data.date.toLocaleDateString()}
</Tooltip.Header>
<Tooltip.List>
<Tooltip.Item label="Sessions" value={data.count} />
</Tooltip.List>
{/if}
</Tooltip.Root>
</Chart>
{:else}
<div class="h-full flex items-center justify-center text-rp-muted">
Expand Down
15 changes: 4 additions & 11 deletions apps/dashboard/src/lib/components/charts/TopEventsChart.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script lang="ts">
import { Chart, Svg, Axis, Bars, Tooltip, Highlight } from "layerchart";
import { Chart, Svg, Axis, Bars, Highlight } from "layerchart";
import { scaleBand, scaleLinear } from "d3-scale";
import type { Event } from "$lib/zero/schema";

let { events = [], limit = 5 }: { events?: Event[]; limit?: number } = $props();
// Accept any event-like object with event_name field
type EventLike = { event_name?: string };
let { events = [], limit = 5 }: { events?: EventLike[]; limit?: number } = $props();

// Count events by name and get top N
const data = $derived.by(() => {
Expand Down Expand Up @@ -46,14 +47,6 @@
/>
<Highlight area />
</Svg>
<Tooltip.Root let:data>
{#if data}
<Tooltip.Header>{data.name}</Tooltip.Header>
<Tooltip.List>
<Tooltip.Item label="Count" value={data.count} />
</Tooltip.List>
{/if}
</Tooltip.Root>
</Chart>
{:else}
<div class="h-full flex items-center justify-center text-rp-muted">
Expand Down
7 changes: 3 additions & 4 deletions apps/dashboard/src/lib/utils/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ export function formatTimestamp(ts: number): string {

/**
* Format a properties object as pretty-printed JSON.
* Accepts Zero's ReadonlyJSONValue type (string, number, boolean, null, array, or object).
*/
export function formatProperties(
props: Record<string, unknown> | null | undefined,
): string {
if (!props) return "{}";
export function formatProperties(props: unknown): string {
if (props === null || props === undefined) return "{}";
try {
return JSON.stringify(props, null, 2);
} catch {
Expand Down
53 changes: 33 additions & 20 deletions apps/dashboard/src/lib/zero/client.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,30 +96,23 @@ export interface QueryResult<T> {
readonly loading: boolean;
}

/**
* Query input type - either a query object or a getter function for reactive params.
*/
type QueryInput<TTable extends keyof Schema["tables"] & string, TReturn> =
| Query<Schema, TTable, TReturn>
| (() => Query<Schema, TTable, TReturn>);

/**
* Create a reactive query that automatically updates when data changes.
* Handles subscription lifecycle and cleanup automatically.
*
* Works with synced queries. Pass either a query directly or a getter function
* for reactive parameters:
* for reactive parameters. Types are inferred automatically from the query.
*
* @example
* ```svelte
* <script>
* import { useQuery } from '$lib/zero/client.svelte';
* import { recentEvents } from '$lib/zero/queries';
*
* // Static query (no reactive params):
* // Static query - types inferred automatically
* const events = useQuery(recentEvents('project-123', 100));
*
* // Reactive query (re-runs when projectId or limit changes):
* // Reactive query - re-runs when projectId or limit changes
* const events = useQuery(() => recentEvents(projectId, limit));
* </script>
*
Expand All @@ -128,10 +121,21 @@ type QueryInput<TTable extends keyof Schema["tables"] & string, TReturn> =
* {/each}
* ```
*/
export function useQuery<
TTable extends keyof Schema["tables"] & string,
TReturn,
>(queryInput: QueryInput<TTable, TReturn>): QueryResult<TReturn> {
/** Table names in the schema */
type TableName = keyof Schema["tables"] & string;

// Overload: direct query (infers types from query)
export function useQuery<TTable extends TableName, TReturn>(
query: Query<Schema, TTable, TReturn>,
): QueryResult<TReturn>;
// Overload: getter function for reactive params (infers types from query)
export function useQuery<TTable extends TableName, TReturn>(
queryGetter: () => Query<Schema, TTable, TReturn>,
): QueryResult<TReturn>;
// Implementation
export function useQuery<TTable extends TableName, TReturn>(
queryInput: Query<Schema, TTable, TReturn> | (() => Query<Schema, TTable, TReturn>),
): QueryResult<TReturn> {
const zero = getZero();

let data = $state<TReturn[]>([]);
Expand Down Expand Up @@ -193,17 +197,18 @@ export interface QueryOneResult<T> {
/**
* Create a reactive query that returns a single item.
* Similar to useQuery but expects .one() query results.
* Types are inferred automatically from the query.
*
* @example
* ```svelte
* <script>
* import { useQueryOne } from '$lib/zero/client.svelte';
* import { projectById } from '$lib/zero/queries';
*
* // Static query:
* // Static query - types inferred automatically
* const project = useQueryOne(projectById('project-123'));
*
* // Reactive query (re-runs when projectId changes):
* // Reactive query - re-runs when projectId changes
* const project = useQueryOne(() => projectById(projectId));
* </script>
*
Expand All @@ -212,10 +217,18 @@ export interface QueryOneResult<T> {
* {/if}
* ```
*/
export function useQueryOne<
TTable extends keyof Schema["tables"] & string,
TReturn,
>(queryInput: QueryInput<TTable, TReturn>): QueryOneResult<TReturn> {
// Overload: direct query (infers types from query)
export function useQueryOne<TTable extends TableName, TReturn>(
query: Query<Schema, TTable, TReturn>,
): QueryOneResult<TReturn>;
// Overload: getter function for reactive params (infers types from query)
export function useQueryOne<TTable extends TableName, TReturn>(
queryGetter: () => Query<Schema, TTable, TReturn>,
): QueryOneResult<TReturn>;
// Implementation
export function useQueryOne<TTable extends TableName, TReturn>(
queryInput: Query<Schema, TTable, TReturn> | (() => Query<Schema, TTable, TReturn>),
): QueryOneResult<TReturn> {
const zero = getZero();

let data = $state<TReturn | undefined>(undefined);
Expand Down
Loading