Search & Filters
Search, filter, and inspect stored events.
Search & Filters uses the same OneMinute Logs API route you created for Live Events. No new backend endpoint is required.
Same API endpoint
If you already created the Live Events route, search and filters work through that same file. The SDK calls /api/oneminutelogs/logs for stored events and /api/oneminutelogs/stream for live events.
// Already created for Live Events
app/api/oneminutelogs/[type]/route.tsUse getLogs()
Pass filters to getLogs() and render the returned rows in your dashboard.
import { getLogs } from "@oneminutelogs/next";
const { data, isLoading, error } = getLogs({
type: "error",
appName: "billing",
search: "timeout",
limit: 200,
});Query syntax
The dashboard example accepts human-friendly query strings and turns them into supported filters like type, appName, env, and search.
Render results
Start with a simple table row, then add filters, details, and copy actions as your dashboard grows.
{results.map((event) => (
<tr key={event.keyId}>
<td>{event.timestamp}</td>
<td>{event.type}</td>
<td>{event.appName}</td>
<td>{event.message}</td>
</tr>
))}Complete dashboard
Copy the full Search & Filters UI.
This example follows the main dashboard query page: query bar, examples, results table, loading state, empty state, details drawer, raw JSON view, and footer stats.
Install Tailwind CSS and shadcn/ui first. This example uses shadcn/ui components like Input, Button, Tabs, and Table.
"use client";
import React from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import {
Table,
TableHeader,
TableHead,
TableRow,
TableBody,
TableCell,
} from "@/components/ui/table";
import { Play, XCircle, Search, Clipboard, Loader2 } from "lucide-react";
import { getLogs } from "@oneminutelogs/next";
type ResultRow = {
timestamp?: string;
type?: "error" | "warning" | "info" | "audit" | "metric";
appName?: string;
message?: string;
environment?: string;
importance?: number | string | null;
subsystem?: string | null;
operation?: string | null;
[key: string]: any;
};
export default function QueriesPage() {
const [filters, setFilters] = React.useState<{
limit?: number;
query?: string;
type?: string;
env?: string;
appName?: string;
search?: string;
}>({});
const { data, isLoading, error } = getLogs(filters);
const levelColor: Record<string, string> = {
info: "#60A5FA",
warning: "#FBBF24",
error: "#F87171",
debug: "#A78BFA",
success: "#34D399",
audit: "#34D399",
metric: "#22D3EE",
};
const [query, setQuery] = React.useState("");
const [selected, setSelected] = React.useState<ResultRow | null>(null);
const examples = [
"type:error",
"appName:api AND search:timeout",
"type:warning AND appName:billing",
];
const normalizeLogs = React.useCallback((input: any): ResultRow[] => {
if (!input) return [];
if (Array.isArray(input)) return input as ResultRow[];
const obj = input as { logs?: any };
if (Array.isArray(obj.logs)) return obj.logs as ResultRow[];
return [];
}, []);
const normalizeQuery = (q: string) =>
q
.replace(/\blevel:/gi, "type:")
.replace(/\bservice:/gi, "appName:")
.replace(/\bmessage:/gi, "search:")
.trim();
const parseQueryFilters = React.useCallback((q: string) => {
const out: {
type?: string;
env?: string;
appName?: string;
search?: string;
} = {};
const parts = q.trim().split(/\s+AND\s+|\s+/i);
for (const p of parts) {
const [rawKey, ...rest] = p.split(":");
if (!rawKey || rest.length === 0) continue;
const value = rest.join(":").trim();
const key = rawKey.trim().toLowerCase();
const mapped =
key === "level"
? "type"
: key === "service"
? "appName"
: key === "message"
? "search"
: key;
if (mapped === "type") out.type = value;
else if (mapped === "env" || mapped === "environment") out.env = value;
else if (mapped === "appname" || mapped === "app") out.appName = value;
else if (mapped === "search") out.search = value;
}
return out;
}, []);
const results: ResultRow[] = React.useMemo(() => {
const rows = normalizeLogs(data);
return rows ?? [];
}, [data, normalizeLogs]);
const runQuery = () => {
const parsed = parseQueryFilters(normalizeQuery(query));
setFilters((prev) => ({
...prev,
...parsed,
limit: 200,
}));
};
const clearQuery = () => {
setQuery("");
setFilters({ limit: 100 });
};
const stats = React.useMemo(() => {
const total = results.length;
const errors = results.filter(
(r) => String(r.type || "").toLowerCase() === "error",
).length;
const warnings = results.filter((r) => {
const t = String(r.type || "").toLowerCase();
return t === "warning" || t === "warn";
}).length;
return { total, errors, warnings };
}, [results]);
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Queries</h1>
<p className="text-sm text-muted-foreground">
Search, filter, and analyze events stored in your project.
</p>
</div>
<div className="rounded-xl border p-3">
<div className="flex gap-3">
<Input
placeholder="Enter query (e.g. type:error AND appName:billing)"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") runQuery();
}}
className="rounded-xl flex-1"
/>
<Button onClick={runQuery} className="rounded-xl">
<Play className="h-4 w-4" />
Run Query
</Button>
<Button
variant="ghost"
onClick={clearQuery}
className="rounded-xl hover:bg-white/2"
>
<XCircle className="h-4 w-4" />
Clear
</Button>
</div>
<div className="mt-2 flex items-center gap-2">
<Search className="h-4 w-4 opacity-60" />
<div className="text-xs text-muted-foreground">Examples:</div>
<div className="flex flex-wrap gap-2">
{examples.map((ex) => (
<button
key={ex}
onClick={() => setQuery(ex)}
className="text-xs rounded-full border px-2 py-1 text-muted-foreground hover:bg-white/3"
>
{ex}
</button>
))}
</div>
</div>
</div>
<div className="rounded-xl border">
<div className="flex items-center justify-between px-3 py-2">
<h3 className="text-sm font-medium">Query Results</h3>
<span className="text-xs text-muted-foreground flex items-center gap-2">
{isLoading && <Loader2 className="h-3 w-3 animate-spin" />}
{stats.total} rows
</span>
</div>
<div
style={{
fontFamily: "JetBrains Mono, monospace",
fontSize: "13px",
}}
>
{error ? (
<div className="flex items-center justify-center py-12 text-red-400">
Failed to load events: {String(error)}
</div>
) : results.length === 0 ? (
<div className="flex items-center justify-center py-12 text-muted-foreground">
<Search className="mr-2 h-4 w-4" />
No events found.
</div>
) : (
<Table className="min-w-full">
<TableHeader>
<TableRow>
<TableHead>Timestamp</TableHead>
<TableHead>Type</TableHead>
<TableHead>App</TableHead>
<TableHead>Message</TableHead>
<TableHead>Environment</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{results.map((r, idx) => (
<TableRow
key={String(r.timestamp ?? "t") + "-" + idx}
className="cursor-pointer"
onClick={() => setSelected(r)}
>
<TableCell className="text-white/60">
{r.timestamp ?? "-"}
</TableCell>
<TableCell
className="font-medium"
style={{
color:
levelColor[
String(
r.type || "",
).toLowerCase() as keyof typeof levelColor
] ?? "inherit",
}}
>
{String(r.type || "").toUpperCase() || "-"}
</TableCell>
<TableCell className="text-white/80">
{r.appName ?? "-"}
</TableCell>
<TableCell className="text-white/90">
{r.message ?? "-"}
</TableCell>
<TableCell className="text-white/70">
{r.environment ?? "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</div>
{selected && (
<div
className="fixed inset-0 z-50 bg-black/50"
onClick={() => setSelected(null)}
>
<div
className="fixed right-0 top-0 h-full w-105 border-l bg-background p-4"
style={{ background: "#0E1117" }}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">Log Details</h3>
<Button
variant="outline"
size="sm"
className="rounded-xl"
onClick={() =>
navigator.clipboard.writeText(
JSON.stringify(selected, null, 2),
)
}
>
<Clipboard className="mr-2 h-3.5 w-3.5" />
Copy JSON
</Button>
</div>
<Tabs defaultValue="formatted" className="mt-3">
<TabsList>
<TabsTrigger value="formatted">Formatted</TabsTrigger>
<TabsTrigger value="raw">Raw JSON</TabsTrigger>
</TabsList>
<TabsContent value="formatted" className="mt-3">
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Timestamp</span>
<span>{selected.timestamp ?? "-"}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Type</span>
<span
className="font-medium"
style={{
color:
levelColor[
String(
selected.type || "",
).toLowerCase() as keyof typeof levelColor
] ?? "inherit",
}}
>
{String(selected.type || "").toUpperCase() || "-"}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">App</span>
<span>{selected.appName ?? "-"}</span>
</div>
<div>
<div className="text-muted-foreground mb-1">Message</div>
<div className="rounded-lg border border-white/10 bg-black/20 p-2">
{selected.message ?? "-"}
</div>
</div>
<div>
<div className="text-muted-foreground mb-1">
Environment
</div>
<div className="rounded-lg border border-white/10 bg-black/20 p-2">
{selected.environment ?? "-"}
</div>
</div>
</div>
</TabsContent>
<TabsContent value="raw" className="mt-3">
<pre className="rounded-lg border border-white/10 bg-black/20 p-2 overflow-x-auto text-xs">
{JSON.stringify(selected, null, 2)}
</pre>
</TabsContent>
</Tabs>
</div>
</div>
)}
<div
className="flex items-center justify-between rounded-xl border px-3 py-2 text-xs"
style={{
background: "rgba(255,255,255,0.02)",
border: "1px solid rgba(255,255,255,0.05)",
}}
>
<div className="flex items-center gap-6">
<span className="text-muted-foreground">Results</span>
<span className="font-medium">{stats.total.toLocaleString()}</span>
<span className="text-muted-foreground">Errors</span>
<span className="font-medium">{stats.errors.toLocaleString()}</span>
<span className="text-muted-foreground">Warnings</span>
<span className="font-medium">{stats.warnings.toLocaleString()}</span>
</div>
<div className="text-muted-foreground">
{isLoading ? "Loading..." : ""}
</div>
</div>
</div>
);
}Common mistakes
- Do not create a second API route. Reuse app/api/oneminutelogs/[type]/route.ts.
- Initialize the logger before calling getLogs().
- Use supported filter keys: type, env, appName, search, and limit.