From 0839e7aafaf554ac54ff7ed7f666cb05512f1a78 Mon Sep 17 00:00:00 2001 From: Robin Wloka Date: Tue, 3 Mar 2026 17:44:07 +0100 Subject: [PATCH 1/3] Biome Formatter --- app/components.json | 46 +- app/drizzle.config.ts | 2 +- app/drizzle/meta/0000_snapshot.json | 62 +- app/drizzle/meta/_journal.json | 24 +- app/drizzle/relations.ts | 18 +- app/drizzle/schema.ts | 157 ++- app/next.config.js | 2 +- app/src/app/admin/costs/costs-client.tsx | 87 +- app/src/app/admin/costs/costs-table.tsx | 224 ++-- app/src/app/admin/costs/page.tsx | 12 +- app/src/app/admin/usage/page.tsx | 24 +- app/src/app/layout.tsx | 95 +- app/src/app/my-api-keys/api-keys-client.tsx | 43 +- app/src/app/my-api-keys/api-keys-table.tsx | 145 +- app/src/app/my-api-keys/page.tsx | 11 +- app/src/components/admin-usage-dashboard.tsx | 555 ++++---- app/src/components/app-sidebar.tsx | 130 +- app/src/components/sidebar-nav.tsx | 143 +- app/src/components/site-header.tsx | 112 +- app/src/components/theme-provider.tsx | 16 +- app/src/components/ui/button.tsx | 98 +- app/src/components/ui/card.tsx | 156 +-- app/src/components/ui/chart.tsx | 632 ++++----- app/src/components/ui/dialog.tsx | 234 ++-- app/src/components/ui/input.tsx | 30 +- app/src/components/ui/native-select.tsx | 79 +- app/src/components/ui/separator.tsx | 36 +- app/src/components/ui/sheet.tsx | 196 ++- app/src/components/ui/sidebar.tsx | 1247 +++++++++--------- app/src/components/ui/skeleton.tsx | 18 +- app/src/components/ui/sonner.tsx | 84 +- app/src/components/ui/table.tsx | 174 +-- app/src/components/ui/tabs.tsx | 126 +- app/src/components/ui/tooltip.tsx | 98 +- app/src/components/unauthorized.tsx | 36 +- app/src/hooks/use-mobile.ts | 28 +- app/src/lib/utils.ts | 6 +- app/src/server/api/routers/admin.ts | 69 +- app/src/server/api/routers/api-key.ts | 116 +- app/src/server/auth/config.ts | 143 +- app/src/server/db/index.ts | 9 +- app/src/server/db/schema.ts | 171 ++- app/src/styles/globals.css | 220 +-- 43 files changed, 3087 insertions(+), 2827 deletions(-) diff --git a/app/components.json b/app/components.json index e747c75..81d4ba0 100644 --- a/app/components.json +++ b/app/components.json @@ -1,25 +1,25 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "base-lyra", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "app/globals.css", - "baseColor": "gray", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "rtl": false, - "aliases": { - "components": "~/components", - "utils": "~/lib/utils", - "ui": "~/components/ui", - "lib": "~/lib", - "hooks": "~/hooks" - }, - "menuColor": "default", - "menuAccent": "subtle", - "registries": {} + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-lyra", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "gray", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} } diff --git a/app/drizzle.config.ts b/app/drizzle.config.ts index 01bbe99..8a20bb0 100644 --- a/app/drizzle.config.ts +++ b/app/drizzle.config.ts @@ -8,6 +8,6 @@ export default { dbCredentials: { url: env.DATABASE_URL, }, - strict: true + strict: true, //tablesFilter: ["analytics-app_*"], } satisfies Config; diff --git a/app/drizzle/meta/0000_snapshot.json b/app/drizzle/meta/0000_snapshot.json index 1648023..f67a6ac 100644 --- a/app/drizzle/meta/0000_snapshot.json +++ b/app/drizzle/meta/0000_snapshot.json @@ -1,32 +1,32 @@ { - "id": "00000000-0000-0000-0000-000000000000", - "prevId": "", - "version": "7", - "dialect": "postgresql", - "tables": {}, - "enums": {}, - "schemas": {}, - "sequences": { - "public.company_id_seq": { - "name": "company_id_seq", - "schema": "public", - "startWith": "1", - "minValue": "1", - "maxValue": "9223372036854775807", - "increment": "1", - "cycle": false, - "cache": "1" - } - }, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "tables": {} - } -} \ No newline at end of file + "id": "00000000-0000-0000-0000-000000000000", + "prevId": "", + "version": "7", + "dialect": "postgresql", + "tables": {}, + "enums": {}, + "schemas": {}, + "sequences": { + "public.company_id_seq": { + "name": "company_id_seq", + "schema": "public", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "increment": "1", + "cycle": false, + "cache": "1" + } + }, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {} + } +} diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index e8c7f81..a72ae83 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -1,13 +1,13 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1772187438831, - "tag": "0000_optimal_rachel_grey", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1772187438831, + "tag": "0000_optimal_rachel_grey", + "breakpoints": true + } + ] +} diff --git a/app/drizzle/relations.ts b/app/drizzle/relations.ts index 9417534..4111a88 100644 --- a/app/drizzle/relations.ts +++ b/app/drizzle/relations.ts @@ -1,29 +1,29 @@ import { relations } from "drizzle-orm/relations"; -import { apikeys, requests, users, company } from "./schema"; +import { apikeys, company, requests, users } from "./schema"; -export const requestsRelations = relations(requests, ({one}) => ({ +export const requestsRelations = relations(requests, ({ one }) => ({ apikey: one(apikeys, { fields: [requests.apiKeyId], - references: [apikeys.uuid] + references: [apikeys.uuid], }), })); -export const apikeysRelations = relations(apikeys, ({one, many}) => ({ +export const apikeysRelations = relations(apikeys, ({ one, many }) => ({ requests: many(requests), user: one(users, { fields: [apikeys.owner], - references: [users.id] + references: [users.id], }), })); -export const usersRelations = relations(users, ({one, many}) => ({ +export const usersRelations = relations(users, ({ one, many }) => ({ apikeys: many(apikeys), company: one(company, { fields: [users.companyId], - references: [company.id] + references: [company.id], }), })); -export const companyRelations = relations(company, ({many}) => ({ +export const companyRelations = relations(company, ({ many }) => ({ users: many(users), -})); \ No newline at end of file +})); diff --git a/app/drizzle/schema.ts b/app/drizzle/schema.ts index fef3438..30209d8 100644 --- a/app/drizzle/schema.ts +++ b/app/drizzle/schema.ts @@ -1,7 +1,19 @@ -import { pgTable, varchar, bigint, timestamp, text, jsonb, foreignKey, integer, boolean, primaryKey, date, char, pgEnum } from "drizzle-orm/pg-core" -import { sql } from "drizzle-orm" - - +import { sql } from "drizzle-orm"; +import { + bigint, + boolean, + char, + date, + foreignKey, + integer, + jsonb, + pgEnum, + pgTable, + primaryKey, + text, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; export const atlasSchemaRevisions = pgTable("atlas_schema_revisions", { version: varchar().primaryKey().notNull(), @@ -12,7 +24,10 @@ export const atlasSchemaRevisions = pgTable("atlas_schema_revisions", { applied: bigint({ mode: "number" }).default(0).notNull(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations total: bigint({ mode: "number" }).default(0).notNull(), - executedAt: timestamp("executed_at", { withTimezone: true, mode: 'string' }).notNull(), + executedAt: timestamp("executed_at", { + withTimezone: true, + mode: "string", + }).notNull(), // You can use { mode: "bigint" } if numbers are exceeding js number limitations executionTime: bigint("execution_time", { mode: "number" }).notNull(), error: text(), @@ -24,55 +39,79 @@ export const atlasSchemaRevisions = pgTable("atlas_schema_revisions", { export const company = pgTable("company", { // You can use { mode: "bigint" } if numbers are exceeding js number limitations - id: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity({ name: "company_id_seq", startWith: 1, increment: 1, minValue: 1, maxValue: 9223372036854775807, cache: 1 }), + id: bigint({ mode: "number" }).primaryKey().generatedAlwaysAsIdentity({ + name: "company_id_seq", + startWith: 1, + increment: 1, + minValue: 1, + maxValue: 9223372036854775807, + cache: 1, + }), companyName: varchar("company_name", { length: 255 }).notNull(), }); -export const requests = pgTable("requests", { - id: varchar({ length: 255 }).primaryKey().notNull(), - requestTime: timestamp("request_time", { withTimezone: true, mode: 'string' }).defaultNow(), - model: varchar({ length: 255 }), - apiKeyId: varchar("api_key_id", { length: 255 }).notNull(), - inputTokenCount: integer("input_token_count").default(0).notNull(), - cachedInputTokenCount: integer("cached_input_token_count").default(0).notNull(), - outputTokenCount: integer("output_token_count").default(0).notNull(), - snapshotVersion: varchar("snapshot_version", { length: 255 }), - isApproximated: boolean("is_approximated").default(false).notNull(), -}, (table) => [ - foreignKey({ +export const requests = pgTable( + "requests", + { + id: varchar({ length: 255 }).primaryKey().notNull(), + requestTime: timestamp("request_time", { + withTimezone: true, + mode: "string", + }).defaultNow(), + model: varchar({ length: 255 }), + apiKeyId: varchar("api_key_id", { length: 255 }).notNull(), + inputTokenCount: integer("input_token_count").default(0).notNull(), + cachedInputTokenCount: integer("cached_input_token_count") + .default(0) + .notNull(), + outputTokenCount: integer("output_token_count").default(0).notNull(), + snapshotVersion: varchar("snapshot_version", { length: 255 }), + isApproximated: boolean("is_approximated").default(false).notNull(), + }, + (table) => [ + foreignKey({ columns: [table.apiKeyId], foreignColumns: [apikeys.uuid], - name: "requests_api_key_id_fkey" + name: "requests_api_key_id_fkey", }), -]); + ], +); -export const apikeys = pgTable("apikeys", { - uuid: varchar({ length: 255 }).primaryKey().notNull(), - apikey: varchar({ length: 255 }).notNull(), - owner: varchar({ length: 255 }).notNull(), - aiapi: varchar({ length: 255 }), - description: varchar({ length: 255 }), -}, (table) => [ - foreignKey({ +export const apikeys = pgTable( + "apikeys", + { + uuid: varchar({ length: 255 }).primaryKey().notNull(), + apikey: varchar({ length: 255 }).notNull(), + owner: varchar({ length: 255 }).notNull(), + aiapi: varchar({ length: 255 }), + description: varchar({ length: 255 }), + }, + (table) => [ + foreignKey({ columns: [table.owner], foreignColumns: [users.id], - name: "apikeys_owner_fkey" + name: "apikeys_owner_fkey", }), -]); + ], +); -export const users = pgTable("users", { - id: varchar({ length: 255 }).primaryKey().notNull(), - // You can use { mode: "bigint" } if numbers are exceeding js number limitations - companyId: bigint("company_id", { mode: "number" }), - name: varchar({ length: 255 }), - isAdmin: boolean("is_admin"), -}, (table) => [ - foreignKey({ +export const users = pgTable( + "users", + { + id: varchar({ length: 255 }).primaryKey().notNull(), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + companyId: bigint("company_id", { mode: "number" }), + name: varchar({ length: 255 }), + isAdmin: boolean("is_admin"), + }, + (table) => [ + foreignKey({ columns: [table.companyId], foreignColumns: [company.id], - name: "users_company_id_fkey" + name: "users_company_id_fkey", }), -]); + ], +); export const models = pgTable("models", { id: varchar({ length: 255 }).primaryKey().notNull(), @@ -80,15 +119,29 @@ export const models = pgTable("models", { export const costUnit = pgEnum("cost_unit", ["1M", "1K"]); -export const costs = pgTable("costs", { - model: varchar({ length: 255 }).notNull(), - price: integer().notNull(), - validFrom: date("valid_from").defaultNow().notNull(), - tokenType: varchar("token_type", { length: 255 }).notNull(), - unitOfMessure: costUnit("unit_of_messure"), - isRegional: boolean("is_regional").notNull(), - backendName: varchar("backend_name", { length: 255 }).notNull(), - currency: char({ length: 3 }), -}, (table) => [ - primaryKey({ columns: [table.model, table.price, table.validFrom, table.tokenType, table.isRegional, table.backendName], name: "costs_pkey"}), -]); +export const costs = pgTable( + "costs", + { + model: varchar({ length: 255 }).notNull(), + price: integer().notNull(), + validFrom: date("valid_from").defaultNow().notNull(), + tokenType: varchar("token_type", { length: 255 }).notNull(), + unitOfMessure: costUnit("unit_of_messure"), + isRegional: boolean("is_regional").notNull(), + backendName: varchar("backend_name", { length: 255 }).notNull(), + currency: char({ length: 3 }), + }, + (table) => [ + primaryKey({ + columns: [ + table.model, + table.price, + table.validFrom, + table.tokenType, + table.isRegional, + table.backendName, + ], + name: "costs_pkey", + }), + ], +); diff --git a/app/next.config.js b/app/next.config.js index 3e34b6e..57a6828 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -6,7 +6,7 @@ import "./src/env.js"; /** @type {import("next").NextConfig} */ const config = { - output: "standalone", + output: "standalone", }; export default config; diff --git a/app/src/app/admin/costs/costs-client.tsx b/app/src/app/admin/costs/costs-client.tsx index 5c0fe71..6b1c155 100644 --- a/app/src/app/admin/costs/costs-client.tsx +++ b/app/src/app/admin/costs/costs-client.tsx @@ -1,14 +1,16 @@ "use client"; import { useMemo, useState } from "react"; - +import { toast } from "sonner"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; -import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select"; +import { + NativeSelect, + NativeSelectOption, +} from "~/components/ui/native-select"; +import { type CostUnit, costUnitOptions } from "~/lib/costs"; import { api } from "~/trpc/react"; import { CostsTable } from "./costs-table"; -import { toast } from "sonner"; -import { costUnitOptions, type CostUnit } from "~/lib/costs"; const defaultUnit = costUnitOptions[0]; const defaultBackend = "openai"; @@ -81,8 +83,8 @@ export function CostsClient() { return (
-

Kostenverwaltung

-

+

Kostenverwaltung

+

Neue Preise anlegen oder bestehende Einträge aktualisieren.

@@ -91,55 +93,66 @@ export function CostsClient() {
-

Neuen Eintrag anlegen

-

- Standardwerte: Backend {defaultBackend}, Einheit {defaultUnit}, Währung {defaultCurrency}. +

Neuen Eintrag anlegen

+

+ Standardwerte: Backend {defaultBackend}, Einheit {defaultUnit}, + Währung {defaultCurrency}.

- + setModel(event.target.value)} placeholder="gpt-4.1" + value={model} />
- + setTokenType(event.target.value)} placeholder="input" + value={tokenType} />
- + setPrice(event.target.value)} placeholder="0" - min={0} + type="number" + value={price} />
- + setValidFrom(event.target.value)} type="date" value={validFrom} - onChange={(event) => setValidFrom(event.target.value)} />
- + setUnitOfMessure(event.target.value as CostUnit) } - className="w-full" + value={unitOfMessure} > {costUnitOptions.map((unit) => ( @@ -149,31 +162,37 @@ export function CostsClient() {
- + setBackendName(event.target.value)} placeholder="openai" + value={backendName} />
- + setCurrency(event.target.value.toUpperCase())} - placeholder="EUR" maxLength={3} + onChange={(event) => + setCurrency(event.target.value.toUpperCase()) + } + placeholder="EUR" + value={currency} />
setIsRegional(event.target.checked)} className="h-4 w-4 rounded border-border" + id="isRegional" + onChange={(event) => setIsRegional(event.target.checked)} + type="checkbox" /> -
@@ -197,7 +216,7 @@ export function CostsClient() { {createCost.isPending ? "Speichern..." : "Eintrag erstellen"} {createCost.error ? ( - + Speichern fehlgeschlagen. ) : null} @@ -208,9 +227,9 @@ export function CostsClient() { updateCost.mutate(payload)} onUpdatePricing={(payload) => updatePricing.mutate(payload)} - isMutating={updateCost.isPending || updatePricing.isPending} />
diff --git a/app/src/app/admin/costs/costs-table.tsx b/app/src/app/admin/costs/costs-table.tsx index 4f410e0..c54a864 100644 --- a/app/src/app/admin/costs/costs-table.tsx +++ b/app/src/app/admin/costs/costs-table.tsx @@ -1,6 +1,5 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; import { type ColumnDef, flexRender, @@ -10,7 +9,8 @@ import { type SortingState, useReactTable, } from "@tanstack/react-table"; -import { ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"; +import { ChevronDown, ChevronsUpDown, ChevronUp } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; import { Button } from "~/components/ui/button"; import { @@ -23,6 +23,10 @@ import { DialogTrigger, } from "~/components/ui/dialog"; import { Input } from "~/components/ui/input"; +import { + NativeSelect, + NativeSelectOption, +} from "~/components/ui/native-select"; import { Table, TableBody, @@ -31,8 +35,7 @@ import { TableHeader, TableRow, } from "~/components/ui/table"; -import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select"; -import { costUnitOptions, type CostUnit } from "~/lib/costs"; +import { type CostUnit, costUnitOptions } from "~/lib/costs"; export type CostRow = { model: string; @@ -108,7 +111,7 @@ function CostEditDialog({ const [unitOfMessure, setUnitOfMessure] = useState( costUnitOptions.includes((row.unitOfMessure ?? "") as CostUnit) ? ((row.unitOfMessure ?? defaultUnit) as CostUnit) - : defaultUnit + : defaultUnit, ); const [isRegional, setIsRegional] = useState(row.isRegional); const [backendName, setBackendName] = useState(row.backendName); @@ -132,7 +135,7 @@ function CostEditDialog({ backendName: row.backendName, currency: row.currency ?? null, }), - [row] + [row], ); const today = useMemo(() => todayString(), []); @@ -140,7 +143,6 @@ function CostEditDialog({ return ( { setOpen(nextOpen); if (nextOpen) { @@ -155,6 +157,7 @@ function CostEditDialog({ setCurrency(row.currency ?? ""); } }} + open={open} > }> Bearbeiten @@ -163,68 +166,83 @@ function CostEditDialog({ Kosten anpassen - Preisaktualisierung erstellt einen neuen Eintrag mit dem heutigen Datum. + Preisaktualisierung erstellt einen neuen Eintrag mit dem heutigen + Datum.
- +
- + setModel(event.target.value)} placeholder="gpt-4.1" + value={model} />
- + setTokenType(event.target.value)} placeholder="input" + value={tokenType} />
- + setPrice(event.target.value)} type="number" value={price} - onChange={(event) => setPrice(event.target.value)} - min={0} />
- + setValidFrom(event.target.value)} type="date" value={effectiveValidFrom} - onChange={(event) => setValidFrom(event.target.value)} - disabled={mode === "update"} /> {mode === "update" ? ( - + Neuer Eintrag ab {formatDate(today)} ) : null}
- + - setUnitOfMessure(event.target.value as CostUnit) - } className="w-full" + onChange={(event) => + setUnitOfMessure(event.target.value as CostUnit) + } + value={unitOfMessure} > {costUnitOptions.map((unit) => ( @@ -234,35 +252,39 @@ function CostEditDialog({
- + setBackendName(event.target.value)} placeholder="openai" + value={backendName} />
- + setCurrency(event.target.value.toUpperCase()) } placeholder="EUR" - maxLength={3} + value={currency} />
setIsRegional(event.target.checked)} className="h-4 w-4 rounded border-border" + id={`costs-regional-${row.model}-${row.tokenType}-${row.price}`} + onChange={(event) => setIsRegional(event.target.checked)} + type="checkbox" /> @@ -301,7 +323,7 @@ function CostEditDialog({ {disabled ? "Speichern..." : "Speichern"}
- +
); @@ -334,16 +356,28 @@ export function CostsTable({ const filteredData = useMemo(() => { const search = globalFilter.trim().toLowerCase(); return data.filter((row) => { - if (modelFilter && !row.model.toLowerCase().includes(modelFilter.toLowerCase())) { + if ( + modelFilter && + !row.model.toLowerCase().includes(modelFilter.toLowerCase()) + ) { return false; } - if (backendFilter && !row.backendName.toLowerCase().includes(backendFilter.toLowerCase())) { + if ( + backendFilter && + !row.backendName.toLowerCase().includes(backendFilter.toLowerCase()) + ) { return false; } - if (tokenFilter && !row.tokenType.toLowerCase().includes(tokenFilter.toLowerCase())) { + if ( + tokenFilter && + !row.tokenType.toLowerCase().includes(tokenFilter.toLowerCase()) + ) { return false; } - if (currencyFilter && (row.currency ?? "").toLowerCase() !== currencyFilter.toLowerCase()) { + if ( + currencyFilter && + (row.currency ?? "").toLowerCase() !== currencyFilter.toLowerCase() + ) { return false; } if (!search) return true; @@ -361,7 +395,14 @@ export function CostsTable({ .map((value) => String(value).toLowerCase()) .some((value) => value.includes(search)); }); - }, [backendFilter, currencyFilter, data, globalFilter, modelFilter, tokenFilter]); + }, [ + backendFilter, + currencyFilter, + data, + globalFilter, + modelFilter, + tokenFilter, + ]); const columns = useMemo[]>( () => [ @@ -415,15 +456,15 @@ export function CostsTable({ header: "Aktion", cell: ({ row }) => ( ), }, ], - [isMutating, onUpdateCost, onUpdatePricing] + [isMutating, onUpdateCost, onUpdatePricing], ); const table = useReactTable({ @@ -442,58 +483,71 @@ export function CostsTable({ useEffect(() => { setPagination((prev) => ({ ...prev, pageIndex: 0 })); - }, [globalFilter, modelFilter, backendFilter, tokenFilter, currencyFilter, data.length]); + }, [ + globalFilter, + modelFilter, + backendFilter, + tokenFilter, + currencyFilter, + data.length, + ]); const totalRows = table.getFilteredRowModel().rows.length; - const startRow = totalRows === 0 ? 0 : pagination.pageIndex * pagination.pageSize + 1; - const endRow = Math.min(totalRows, (pagination.pageIndex + 1) * pagination.pageSize); + const startRow = + totalRows === 0 ? 0 : pagination.pageIndex * pagination.pageSize + 1; + const endRow = Math.min( + totalRows, + (pagination.pageIndex + 1) * pagination.pageSize, + ); return (
-

Preisliste

-

+

Preisliste

+

Filtere und durchsuche alle Kosten-Einträge.

-
+
{totalRows} Einträge
setGlobalFilter(event.target.value)} placeholder="Suche" value={globalFilter} - onChange={(event) => setGlobalFilter(event.target.value)} /> setModelFilter(event.target.value)} placeholder="Modell" value={modelFilter} - onChange={(event) => setModelFilter(event.target.value)} /> setBackendFilter(event.target.value)} placeholder="Backend" value={backendFilter} - onChange={(event) => setBackendFilter(event.target.value)} /> setTokenFilter(event.target.value)} placeholder="Token-Typ" value={tokenFilter} - onChange={(event) => setTokenFilter(event.target.value)} /> setCurrencyFilter(event.target.value)} placeholder="Währung" value={currencyFilter} - onChange={(event) => setCurrencyFilter(event.target.value)} />
- {globalFilter || modelFilter || backendFilter || tokenFilter || currencyFilter ? ( + {globalFilter || + modelFilter || + backendFilter || + tokenFilter || + currencyFilter ? (
@@ -509,25 +565,25 @@ export function CostsTable({
- + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const sorted = header.column.getIsSorted(); return ( {header.isPlaceholder ? null : (
-
-
+
+
Zeilen pro Seite setPagination((prev) => ({ ...prev, @@ -591,8 +647,8 @@ export function CostsTable({ pageIndex: 0, })) } - className="w-[90px]" size="sm" + value={String(pagination.pageSize)} > {[10, 20, 50].map((size) => ( @@ -602,39 +658,39 @@ export function CostsTable({
- + {startRow}-{endRow} von {totalRows}
diff --git a/app/src/app/admin/costs/page.tsx b/app/src/app/admin/costs/page.tsx index d775611..245357f 100644 --- a/app/src/app/admin/costs/page.tsx +++ b/app/src/app/admin/costs/page.tsx @@ -1,13 +1,13 @@ -import { redirect } from "next/navigation" +import { redirect } from "next/navigation"; -import { auth } from "~/server/auth" -import { CostsClient } from "./costs-client" +import { auth } from "~/server/auth"; +import { CostsClient } from "./costs-client"; export default async function AdminCostsPage() { - const session = await auth() + const session = await auth(); if (!session?.user?.isAdmin) { - redirect("/") + redirect("/"); } - return + return ; } diff --git a/app/src/app/admin/usage/page.tsx b/app/src/app/admin/usage/page.tsx index 29c0ea4..eb0cc23 100644 --- a/app/src/app/admin/usage/page.tsx +++ b/app/src/app/admin/usage/page.tsx @@ -1,27 +1,27 @@ -import { redirect } from "next/navigation" +import { redirect } from "next/navigation"; -import { AdminUsageDashboard } from "~/components/admin-usage-dashboard" -import { auth } from "~/server/auth" -import { api } from "~/trpc/server" +import { AdminUsageDashboard } from "~/components/admin-usage-dashboard"; +import { auth } from "~/server/auth"; +import { api } from "~/trpc/server"; -const validRanges = new Set(["24h", "7d", "30d", "all"]) +const validRanges = new Set(["24h", "7d", "30d", "all"]); export default async function AdminUsagePage({ searchParams, }: { - searchParams?: Promise<{ range?: string }> + searchParams?: Promise<{ range?: string }>; }) { - const resolvedSearchParams = await searchParams - const session = await auth() + const resolvedSearchParams = await searchParams; + const session = await auth(); if (!session?.user?.isAdmin) { - redirect("/") + redirect("/"); } const range = validRanges.has(resolvedSearchParams?.range ?? "") ? (resolvedSearchParams?.range as "24h" | "7d" | "30d" | "all") - : "24h" + : "24h"; - const stats = await api.admin.getUsageStats({ range }) + const stats = await api.admin.getUsageStats({ range }); - return + return ; } diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx index d48792c..8ed1c28 100644 --- a/app/src/app/layout.tsx +++ b/app/src/app/layout.tsx @@ -1,60 +1,59 @@ import "~/styles/globals.css"; -import type {Metadata} from "next"; -import {Geist} from "next/font/google"; -import {redirect} from "next/navigation"; +import type { Metadata } from "next"; +import { Geist } from "next/font/google"; +import { redirect } from "next/navigation"; -import {AppSidebar} from "~/components/app-sidebar"; -import {SiteHeader} from "~/components/site-header"; -import {SidebarInset, SidebarProvider} from "~/components/ui/sidebar"; -import {TooltipProvider} from "~/components/ui/tooltip"; -import {auth} from "~/server/auth"; -import {TRPCReactProvider} from "~/trpc/react"; -import {Toaster} from "~/components/ui/sonner"; -import {ThemeProvider} from "~/components/theme-provider"; +import { AppSidebar } from "~/components/app-sidebar"; +import { SiteHeader } from "~/components/site-header"; +import { ThemeProvider } from "~/components/theme-provider"; +import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar"; +import { Toaster } from "~/components/ui/sonner"; +import { TooltipProvider } from "~/components/ui/tooltip"; +import { auth } from "~/server/auth"; +import { TRPCReactProvider } from "~/trpc/react"; export const metadata: Metadata = { - title: "RobadsAI API-Proxy Verwaltung", - description: "Verwaltung und Auswertung der API-Proxy-Nutzung.", - icons: [{rel: "icon", url: "/favicon.ico"}], + title: "RobadsAI API-Proxy Verwaltung", + description: "Verwaltung und Auswertung der API-Proxy-Nutzung.", + icons: [{ rel: "icon", url: "/favicon.ico" }], }; const geist = Geist({ - subsets: ["latin"], - variable: "--font-geist-sans", + subsets: ["latin"], + variable: "--font-geist-sans", }); export default async function RootLayout({ - children, - }: Readonly<{ children: React.ReactNode }>) { - const session = await auth(); - if (!session?.user) { - redirect("/api/auth/signin"); - } - return ( - - - - - - - - - - - {children} - - - - - - - - - ); + children, +}: Readonly<{ children: React.ReactNode }>) { + const session = await auth(); + if (!session?.user) { + redirect("/api/auth/signin"); + } + return ( + + + + + + + + + + + {children} + + + + + + + + ); } diff --git a/app/src/app/my-api-keys/api-keys-client.tsx b/app/src/app/my-api-keys/api-keys-client.tsx index f528d5c..80244bc 100644 --- a/app/src/app/my-api-keys/api-keys-client.tsx +++ b/app/src/app/my-api-keys/api-keys-client.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; - +import { toast } from "sonner"; import { Button } from "~/components/ui/button"; import { Dialog, @@ -15,7 +15,6 @@ import { import { Input } from "~/components/ui/input"; import { api } from "~/trpc/react"; import { ApiKeysTable } from "./api-keys-table"; -import { toast } from "sonner"; export function ApiKeysClient() { const utils = api.useUtils(); @@ -43,7 +42,6 @@ export function ApiKeysClient() { return (
{ setDialogOpen(open); if (!open) { @@ -54,11 +52,19 @@ export function ApiKeysClient() { createApiKey.reset(); } }} + open={dialogOpen} > {isLoading ? ( -
Schlüssel werden geladen...
+
+ Schlüssel werden geladen... +
) : ( }> + API-Key erstellen + + } data={data.map((row) => ({ kind: "key", id: row.id, @@ -79,7 +85,6 @@ export function ApiKeysClient() { currency: modelRow.currency ?? null, })), }))} - action={}>API-Key erstellen} /> )} @@ -91,31 +96,29 @@ export function ApiKeysClient() {
setDescription(event.target.value)} placeholder="Beschreibung (optional)" value={description} - onChange={(event) => setDescription(event.target.value)} /> {token ? (
-
+
Warnung

- Dieser Schlüssel wird nur einmal angezeigt. Bitte jetzt sicher speichern. + Dieser Schlüssel wird nur einmal angezeigt. Bitte jetzt sicher + speichern.

{createdId ? ( - + ID: {createdId} ) : null}
{copied ? ( - Kopiert. + + Kopiert. + ) : null}
@@ -145,11 +153,14 @@ export function ApiKeysClient() { }) } > - {createApiKey.isPending ? "Wird erstellt..." : "Schlüssel erstellen"} + {createApiKey.isPending + ? "Wird erstellt..." + : "Schlüssel erstellen"} {createApiKey.error ? ( - - Schlüssel konnte nicht erstellt werden. Bitte erneut versuchen. + + Schlüssel konnte nicht erstellt werden. Bitte erneut + versuchen. ) : null}
diff --git a/app/src/app/my-api-keys/api-keys-table.tsx b/app/src/app/my-api-keys/api-keys-table.tsx index 977479b..ee7e6ce 100644 --- a/app/src/app/my-api-keys/api-keys-table.tsx +++ b/app/src/app/my-api-keys/api-keys-table.tsx @@ -1,23 +1,26 @@ "use client"; -import { type ReactNode, useEffect, useMemo, useState } from "react"; import { type ColumnDef, + type ExpandedState, flexRender, getCoreRowModel, getExpandedRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, - type ExpandedState, type SortingState, useReactTable, } from "@tanstack/react-table"; -import { ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"; +import { ChevronDown, ChevronsUpDown, ChevronUp } from "lucide-react"; +import { type ReactNode, useEffect, useMemo, useState } from "react"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; -import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select"; +import { + NativeSelect, + NativeSelectOption, +} from "~/components/ui/native-select"; import { Table, TableBody, @@ -93,10 +96,10 @@ export function ApiKeysTable({ cell: ({ row }) => row.getCanExpand() ? ( @@ -250,25 +260,25 @@ export function ApiKeysTable({
- + {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { const sorted = header.column.getIsSorted(); return ( {header.isPlaceholder ? null : header.column.getCanSort() ? ( ) : ( - flexRender(header.column.columnDef.header, header.getContext()) + flexRender( + header.column.columnDef.header, + header.getContext(), + ) )} ); @@ -290,17 +303,17 @@ export function ApiKeysTable({ {table.getRowModel().rows.map((row) => ( 0 ? "bg-muted/20" : ""}`} + key={row.id} > {row.getVisibleCells().map((cell) => ( 0 ? "pl-10" : "" }`} + key={cell.id} > {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -310,8 +323,8 @@ export function ApiKeysTable({ {table.getRowModel().rows.length === 0 ? ( Keine API-Keys passen zu diesem Filter. @@ -319,11 +332,11 @@ export function ApiKeysTable({ ) : null}
-
-
+
+
Zeilen pro Seite setPagination((prev) => ({ ...prev, @@ -331,8 +344,8 @@ export function ApiKeysTable({ pageIndex: 0, })) } - className="w-[90px]" size="sm" + value={String(pagination.pageSize)} > {[10, 20, 50].map((size) => ( @@ -342,46 +355,46 @@ export function ApiKeysTable({
- + {startRow}-{endRow} von {totalRows} -
- - - - -
+
+ + + +
+
); } diff --git a/app/src/app/my-api-keys/page.tsx b/app/src/app/my-api-keys/page.tsx index f69ee96..15e7814 100644 --- a/app/src/app/my-api-keys/page.tsx +++ b/app/src/app/my-api-keys/page.tsx @@ -1,17 +1,18 @@ -import { ApiKeysClient } from "~/app/my-api-keys/api-keys-client" +import { ApiKeysClient } from "~/app/my-api-keys/api-keys-client"; export default async function MyApiKeysPage() { return (
-

Meine API-Keys

-

- Nutzung nach Schlüssel verfolgen, Ergebnisse filtern und nach Token-Summen sortieren. +

Meine API-Keys

+

+ Nutzung nach Schlüssel verfolgen, Ergebnisse filtern und nach + Token-Summen sortieren.

- ) + ); } diff --git a/app/src/components/admin-usage-dashboard.tsx b/app/src/components/admin-usage-dashboard.tsx index 1139c66..7c917dd 100644 --- a/app/src/components/admin-usage-dashboard.tsx +++ b/app/src/components/admin-usage-dashboard.tsx @@ -1,303 +1,304 @@ -"use client" +"use client"; -import { useEffect, useMemo, useState } from "react" -import { Pie, PieChart } from "recharts" -import { useRouter } from "next/navigation" +import { useRouter } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; +import { Pie, PieChart } from "recharts"; -import { Button } from "~/components/ui/button" +import { Button } from "~/components/ui/button"; import { - ChartContainer, - ChartLegend, - ChartLegendContent, - ChartTooltip, - ChartTooltipContent, - type ChartConfig, -} from "~/components/ui/chart" + type ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from "~/components/ui/chart"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "~/components/ui/table" -import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs" -import { NativeSelect, NativeSelectOption } from "~/components/ui/native-select" + NativeSelect, + NativeSelectOption, +} from "~/components/ui/native-select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table"; +import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs"; type UsageModel = { - model: string - tokens: number -} + model: string; + tokens: number; +}; type UsageUser = { - id: string - name: string - inputTokens: number - cachedTokens: number - outputTokens: number - lastActivity: string | null -} + id: string; + name: string; + inputTokens: number; + cachedTokens: number; + outputTokens: number; + lastActivity: string | null; +}; type UsageStats = { - totalTokens: number - modelUsage: UsageModel[] - users: UsageUser[] -} + totalTokens: number; + modelUsage: UsageModel[]; + users: UsageUser[]; +}; const ranges = [ - { value: "24h", label: "24 Stunden" }, - { value: "7d", label: "7 Tage" }, - { value: "30d", label: "30 Tage" }, - { value: "all", label: "Allzeit" }, -] as const + { value: "24h", label: "24 Stunden" }, + { value: "7d", label: "7 Tage" }, + { value: "30d", label: "30 Tage" }, + { value: "all", label: "Allzeit" }, +] as const; const chartColors = [ - "var(--chart-1)", - "var(--chart-2)", - "var(--chart-3)", - "var(--chart-4)", - "var(--chart-5)", -] + "var(--chart-1)", + "var(--chart-2)", + "var(--chart-3)", + "var(--chart-4)", + "var(--chart-5)", +]; export function AdminUsageDashboard({ - range, - stats, + range, + stats, }: { - range: (typeof ranges)[number]["value"] - stats: UsageStats + range: (typeof ranges)[number]["value"]; + stats: UsageStats; }) { - const router = useRouter() - const numberFormatter = new Intl.NumberFormat("de-DE") - const dateFormatter = new Intl.DateTimeFormat("de-DE", { - dateStyle: "medium", - timeStyle: "short", - }) - const [pageIndex, setPageIndex] = useState(0) - const [pageSize, setPageSize] = useState(10) + const router = useRouter(); + const numberFormatter = new Intl.NumberFormat("de-DE"); + const dateFormatter = new Intl.DateTimeFormat("de-DE", { + dateStyle: "medium", + timeStyle: "short", + }); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); - useEffect(() => { - setPageIndex(0) - }, [stats.users.length]) + useEffect(() => { + setPageIndex(0); + }, [stats.users.length]); - const pieData = stats.modelUsage.map((item, index) => { - const key = `model-${index}` - return { - key, - model: item.model, - tokens: item.tokens, - fill: `var(--color-${key})`, - } - }) + const pieData = stats.modelUsage.map((item, index) => { + const key = `model-${index}`; + return { + key, + model: item.model, + tokens: item.tokens, + fill: `var(--color-${key})`, + }; + }); - const chartConfig = pieData.reduce((acc, item, index) => { - acc[item.key] = { - label: item.model, - color: chartColors[index % chartColors.length], - } - return acc - }, {}) + const chartConfig = pieData.reduce((acc, item, index) => { + acc[item.key] = { + label: item.model, + color: chartColors[index % chartColors.length], + }; + return acc; + }, {}); - const totalUsers = stats.users.length - const totalPages = Math.max(1, Math.ceil(totalUsers / pageSize)) - const clampedPageIndex = Math.min(pageIndex, totalPages - 1) - const startRow = totalUsers === 0 ? 0 : clampedPageIndex * pageSize + 1 - const endRow = Math.min(totalUsers, (clampedPageIndex + 1) * pageSize) - const pagedUsers = useMemo(() => { - const start = clampedPageIndex * pageSize - return stats.users.slice(start, start + pageSize) - }, [clampedPageIndex, pageSize, stats.users]) + const totalUsers = stats.users.length; + const totalPages = Math.max(1, Math.ceil(totalUsers / pageSize)); + const clampedPageIndex = Math.min(pageIndex, totalPages - 1); + const startRow = totalUsers === 0 ? 0 : clampedPageIndex * pageSize + 1; + const endRow = Math.min(totalUsers, (clampedPageIndex + 1) * pageSize); + const pagedUsers = useMemo(() => { + const start = clampedPageIndex * pageSize; + return stats.users.slice(start, start + pageSize); + }, [clampedPageIndex, pageSize, stats.users]); - return ( -
-
-
-

Nutzungsstatistiken

- router.push(`/admin/usage?range=${value}`)} - > - - {ranges.map((item) => ( - - {item.label} - - ))} - - -
-
-
- Verbrauchte Tokens -
-
- Verbrauchte Tokens: {numberFormatter.format(stats.totalTokens)} -
-
-
+ return ( +
+
+
+

Nutzungsstatistiken

+ + router.push(`/admin/usage?range=${value}`) + } + value={range} + > + + {ranges.map((item) => ( + + {item.label} + + ))} + + +
+
+
+ Verbrauchte Tokens +
+
+ Verbrauchte Tokens: {numberFormatter.format(stats.totalTokens)} +
+
+
-
-
-
- Nutzungsverteilung nach Modell -
-
- {pieData.length ? ( - - - } - /> - - } - /> - - - ) : ( -
- Keine Nutzungsdaten für diesen Zeitraum. -
- )} -
-
+
+
+
+ Nutzungsverteilung nach Modell +
+
+ {pieData.length ? ( + + + } + /> + + } /> + + + ) : ( +
+ Keine Nutzungsdaten für diesen Zeitraum. +
+ )} +
+
-
-
- Nutzung nach Benutzer -
-
- - - - - Benutzername - - - Input-Tokens - - - Cache-Tokens - - - Output-Tokens - - - Letzte Aktivität - - - - - {pagedUsers.map((user) => ( - - - {user.name} - - - {numberFormatter.format(user.inputTokens)} - - - {numberFormatter.format(user.cachedTokens)} - - - {numberFormatter.format(user.outputTokens)} - - - {user.lastActivity - ? dateFormatter.format(new Date(user.lastActivity)) - : "—"} - - - ))} - {!stats.users.length ? ( - - - Keine Benutzer gefunden. - - - ) : null} - -
-
-
- Zeilen pro Seite - { - setPageSize(Number(event.target.value)) - setPageIndex(0) - }} - className="w-[80px]" - size="sm" - > - {[10, 20, 50].map((size) => ( - - {size} - - ))} - -
-
- - {startRow}-{endRow} von {totalUsers} - -
- - - - -
-
-
-
-
-
-
- ) +
+
+ Nutzung nach Benutzer +
+
+ + + + + Benutzername + + + Input-Tokens + + + Cache-Tokens + + + Output-Tokens + + + Letzte Aktivität + + + + + {pagedUsers.map((user) => ( + + + {user.name} + + + {numberFormatter.format(user.inputTokens)} + + + {numberFormatter.format(user.cachedTokens)} + + + {numberFormatter.format(user.outputTokens)} + + + {user.lastActivity + ? dateFormatter.format(new Date(user.lastActivity)) + : "—"} + + + ))} + {!stats.users.length ? ( + + + Keine Benutzer gefunden. + + + ) : null} + +
+
+
+ Zeilen pro Seite + { + setPageSize(Number(event.target.value)); + setPageIndex(0); + }} + size="sm" + value={String(pageSize)} + > + {[10, 20, 50].map((size) => ( + + {size} + + ))} + +
+
+ + {startRow}-{endRow} von {totalUsers} + +
+ + + + +
+
+
+
+
+
+
+ ); } diff --git a/app/src/components/app-sidebar.tsx b/app/src/components/app-sidebar.tsx index c54effd..cd3258d 100644 --- a/app/src/components/app-sidebar.tsx +++ b/app/src/components/app-sidebar.tsx @@ -1,71 +1,69 @@ +import { LogOut } from "lucide-react"; +import Link from "next/link"; +import { SidebarNav } from "~/components/sidebar-nav"; import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, -} from "~/components/ui/sidebar" -import { LogOut } from "lucide-react" -import Link from "next/link" -import { auth } from "~/server/auth" -import { SidebarNav } from "~/components/sidebar-nav" + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, +} from "~/components/ui/sidebar"; +import { auth } from "~/server/auth"; export async function AppSidebar() { - const session = await auth(); + const session = await auth(); - return ( - - - - - - - - - {session?.user ? ( -
- - {session.user.image ? ( - - ) : ( -
- {(session.user.name ?? "U").slice(0, 1).toUpperCase()} -
- )} -
-
- {session.user.name ?? "Benutzer"} -
- {session.user.email ? ( -
- {session.user.email} -
- ) : null} -
-
-
- - - Abmelden - -
-
- ) : ( - - Anmelden - - )} -
-
- ) + return ( + + + + + + + {session?.user ? ( +
+ + {session.user.image ? ( + + ) : ( +
+ {(session.user.name ?? "U").slice(0, 1).toUpperCase()} +
+ )} +
+
+ {session.user.name ?? "Benutzer"} +
+ {session.user.email ? ( +
+ {session.user.email} +
+ ) : null} +
+
+
+ + + Abmelden + +
+
+ ) : ( + + Anmelden + + )} +
+
+ ); } diff --git a/app/src/components/sidebar-nav.tsx b/app/src/components/sidebar-nav.tsx index 0967ca8..569750a 100644 --- a/app/src/components/sidebar-nav.tsx +++ b/app/src/components/sidebar-nav.tsx @@ -1,25 +1,25 @@ -"use client" +"use client"; -import { BarChart3, Coins, KeyRound } from "lucide-react" -import Link from "next/link" -import { usePathname } from "next/navigation" +import { BarChart3, Coins, KeyRound } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; import { - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "~/components/ui/sidebar" + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "~/components/ui/sidebar"; const userItems = [ - { - href: "/my-api-keys", - label: "Meine API-Schlüssel", - icon: KeyRound, - }, -] + { + href: "/my-api-keys", + label: "Meine API-Schlüssel", + icon: KeyRound, + }, +]; const adminItems = [ { @@ -32,57 +32,66 @@ const adminItems = [ label: "Kosten", icon: Coins, }, -] +]; export function SidebarNav({ isAdmin }: { isAdmin: boolean }) { - const pathname = usePathname() + const pathname = usePathname(); - return ( - <> - - Meine Daten - - - {userItems.map((item) => { - const isActive = pathname === item.href - const Icon = item.icon - return ( - - - - - {item.label} - - - - ) - })} - - - - {isAdmin ? ( - - Admin - - - {adminItems.map((item) => { - const isActive = pathname === item.href - const Icon = item.icon - return ( - - - - - {item.label} - - - - ) - })} - - - - ) : null} - - ) + return ( + <> + + Meine Daten + + + {userItems.map((item) => { + const isActive = pathname === item.href; + const Icon = item.icon; + return ( + + + + + {item.label} + + + + ); + })} + + + + {isAdmin ? ( + + Admin + + + {adminItems.map((item) => { + const isActive = pathname === item.href; + const Icon = item.icon; + return ( + + + + + {item.label} + + + + ); + })} + + + + ) : null} + + ); } diff --git a/app/src/components/site-header.tsx b/app/src/components/site-header.tsx index 284d226..ba1a078 100644 --- a/app/src/components/site-header.tsx +++ b/app/src/components/site-header.tsx @@ -1,59 +1,67 @@ -"use client" +"use client"; -import { MoonIcon, SunIcon } from "lucide-react" -import { useEffect, useState } from "react" +import { MoonIcon, SunIcon } from "lucide-react"; +import { useEffect, useState } from "react"; -import { Button } from "~/components/ui/button" -import { Separator } from "~/components/ui/separator" -import { SidebarTrigger } from "~/components/ui/sidebar" +import { Button } from "~/components/ui/button"; +import { Separator } from "~/components/ui/separator"; +import { SidebarTrigger } from "~/components/ui/sidebar"; export function SiteHeader() { - const [isDark, setIsDark] = useState(false) + const [isDark, setIsDark] = useState(false); - useEffect(() => { - const stored = localStorage.getItem("theme") - const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches - const initial = stored ? stored === "dark" : prefersDark - setIsDark(initial) - document.documentElement.classList.toggle("dark", initial) - }, []) + useEffect(() => { + const stored = localStorage.getItem("theme"); + const prefersDark = window.matchMedia( + "(prefers-color-scheme: dark)", + ).matches; + const initial = stored ? stored === "dark" : prefersDark; + setIsDark(initial); + document.documentElement.classList.toggle("dark", initial); + }, []); - const toggleTheme = () => { - const next = !isDark - setIsDark(next) - document.documentElement.classList.toggle("dark", next) - localStorage.setItem("theme", next ? "dark" : "light") - } + const toggleTheme = () => { + const next = !isDark; + setIsDark(next); + document.documentElement.classList.toggle("dark", next); + localStorage.setItem("theme", next ? "dark" : "light"); + }; - return ( -
-
- - -

RobadsAI API Proxy Mananagement App

-
- - -
-
-
- ) + return ( +
+
+ + +

+ RobadsAI API Proxy Mananagement App +

+
+ + +
+
+
+ ); } diff --git a/app/src/components/theme-provider.tsx b/app/src/components/theme-provider.tsx index 1466952..7c8090f 100644 --- a/app/src/components/theme-provider.tsx +++ b/app/src/components/theme-provider.tsx @@ -1,11 +1,11 @@ -"use client" +"use client"; -import * as React from "react" -import { ThemeProvider as NextThemesProvider } from "next-themes" +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import type * as React from "react"; export function ThemeProvider({ - children, - ...props - }: React.ComponentProps) { - return {children} -} \ No newline at end of file + children, + ...props +}: React.ComponentProps) { + return {children}; +} diff --git a/app/src/components/ui/button.tsx b/app/src/components/ui/button.tsx index c0a358f..4e60368 100644 --- a/app/src/components/ui/button.tsx +++ b/app/src/components/ui/button.tsx @@ -1,58 +1,58 @@ -"use client" +"use client"; -import { Button as ButtonPrimitive } from "@base-ui/react/button" -import { cva, type VariantProps } from "class-variance-authority" +import { Button as ButtonPrimitive } from "@base-ui/react/button"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; const buttonVariants = cva( - "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:ring-1 aria-invalid:ring-1 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", - destructive: - "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", - icon: "size-8", - "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-7 rounded-none", - "icon-lg": "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) + "group/button inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-none border border-transparent bg-clip-padding font-medium text-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 dark:hover:bg-destructive/30", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", + icon: "size-8", + "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-7 rounded-none", + "icon-lg": "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); function Button({ - className, - variant = "default", - size = "default", - ...props + className, + variant = "default", + size = "default", + ...props }: ButtonPrimitive.Props & VariantProps) { - return ( - - ) + return ( + + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/app/src/components/ui/card.tsx b/app/src/components/ui/card.tsx index 5d9f21d..3ecf5a4 100644 --- a/app/src/components/ui/card.tsx +++ b/app/src/components/ui/card.tsx @@ -1,103 +1,103 @@ -import * as React from "react" +import type * as React from "react"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; function Card({ - className, - size = "default", - ...props + className, + size = "default", + ...props }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { - return ( -
img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none", - className - )} - {...props} - /> - ) + return ( +
img:first-child]:pt-0 has-data-[slot=card-footer]:pb-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none", + className, + )} + data-size={size} + data-slot="card" + {...props} + /> + ); } function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ); } export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/app/src/components/ui/chart.tsx b/app/src/components/ui/chart.tsx index 139fe96..ab7e330 100644 --- a/app/src/components/ui/chart.tsx +++ b/app/src/components/ui/chart.tsx @@ -1,356 +1,356 @@ -"use client" +"use client"; -import * as React from "react" -import * as RechartsPrimitive from "recharts" +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; -import { cn } from "~/lib/utils" +import { cn } from "~/lib/utils"; // Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const +const THEMES = { light: "", dark: ".dark" } as const; export type ChartConfig = { - [k in string]: { - label?: React.ReactNode - icon?: React.ComponentType - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record } - ) -} + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; type ChartContextProps = { - config: ChartConfig -} + config: ChartConfig; +}; -const ChartContext = React.createContext(null) +const ChartContext = React.createContext(null); function useChart() { - const context = React.useContext(ChartContext) + const context = React.useContext(ChartContext); - if (!context) { - throw new Error("useChart must be used within a ") - } + if (!context) { + throw new Error("useChart must be used within a "); + } - return context + return context; } function ChartContainer({ - id, - className, - children, - config, - ...props + id, + className, + children, + config, + ...props }: React.ComponentProps<"div"> & { - config: ChartConfig - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"] + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"]; }) { - const uniqueId = React.useId() - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` - - return ( - -
- - - {children} - -
-
- ) + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + + {children} + +
+
+ ); } const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([, config]) => config.theme || config.color - ) - - if (!colorConfig.length) { - return null - } - - return ( -