Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
36c32ac
feat: add new api cards
ogzhanolguncu Feb 26, 2025
04f26a9
fix: granularity issues
ogzhanolguncu Feb 26, 2025
2cc8a85
refactor: use same component for stat pages
ogzhanolguncu Feb 26, 2025
9054d30
Merge branch 'main' of github.com:unkeyed/unkey into apis-overview-v2
ogzhanolguncu Feb 26, 2025
6c3dd03
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 26, 2025
e54c387
feat: add pagiantion for overview cards
ogzhanolguncu Feb 27, 2025
9f5467f
feat: add proper clean up for search overview
ogzhanolguncu Feb 27, 2025
4886f27
fix: remove redundant fields
ogzhanolguncu Feb 27, 2025
a40c2d1
Merge branch 'apis-overview-v2' of github.com:unkeyed/unkey into apis…
ogzhanolguncu Feb 27, 2025
b1369d8
fix: dialog styles
ogzhanolguncu Feb 27, 2025
4243009
fix: use sql syntax to prevent injection
ogzhanolguncu Feb 27, 2025
3f1c95a
fix: use sql syntax instead of like
ogzhanolguncu Feb 27, 2025
e202155
Merge branch 'main' of github.com:unkeyed/unkey into apis-overview-v2
ogzhanolguncu Feb 27, 2025
8a5c77e
fix: remove filters
ogzhanolguncu Feb 27, 2025
7055195
chore: remove unused package
ogzhanolguncu Feb 27, 2025
2257ba6
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 27, 2025
ac2ce02
fix: audit timestamp
ogzhanolguncu Feb 27, 2025
848c315
Merge branch 'apis-overview-v2' of github.com:unkeyed/unkey into apis…
ogzhanolguncu Feb 27, 2025
bbddbf2
fix: pr comments
ogzhanolguncu Feb 27, 2025
04b7667
Merge branch 'main' into apis-overview-v2
ogzhanolguncu Feb 27, 2025
4a0f24b
Merge branch 'main' into apis-overview-v2
ogzhanolguncu Feb 27, 2025
95c1c37
Merge branch 'main' of github.com:unkeyed/unkey into apis-overview-v2
ogzhanolguncu Feb 28, 2025
1f6cb17
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 28, 2025
e7ac569
refactor: add auto submit to search component
ogzhanolguncu Feb 28, 2025
746e742
Merge branch 'apis-overview-v2' of github.com:unkeyed/unkey into apis…
ogzhanolguncu Feb 28, 2025
e379ef7
feat: add new search component
ogzhanolguncu Feb 28, 2025
88fbb72
refactor: use same same component to space out empty componetn
ogzhanolguncu Feb 28, 2025
99530b7
Merge branch 'main' into apis-overview-v2
ogzhanolguncu Mar 3, 2025
cb1b915
fix: grid config
ogzhanolguncu Mar 3, 2025
c3c36ae
Merge branch 'apis-overview-v2' of github.com:unkeyed/unkey into apis…
ogzhanolguncu Mar 3, 2025
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
61 changes: 61 additions & 0 deletions apps/dashboard/app/(app)/apis/_components/api-list-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";
import { StatsCard } from "@/components/stats-card";
import { StatsTimeseriesBarChart } from "@/components/stats-card/components/chart/stats-chart";
import { MetricStats } from "@/components/stats-card/components/metric-stats";
import type { ApiOverview } from "@/lib/trpc/routers/api/query-overview/schemas";
import { Key, ProgressBar } from "@unkey/icons";
import { useFetchVerificationTimeseries } from "./hooks/use-query-timeseries";

type Props = {
api: ApiOverview;
};

export const ApiListCard = ({ api }: Props) => {
const { timeseries, isLoading, isError } = useFetchVerificationTimeseries(api.keyspaceId);

const passed = timeseries?.reduce((acc, crr) => acc + crr.success, 0) ?? 0;
const blocked = timeseries?.reduce((acc, crr) => acc + crr.error, 0) ?? 0;

const keyCount = api.keys.reduce((acc, crr) => acc + crr.count, 0);
return (
<StatsCard
name={api.name}
secondaryId={api.id}
linkPath={`/apis/${api.id}`}
chart={
<StatsTimeseriesBarChart
data={timeseries}
isLoading={isLoading}
isError={isError}
config={{
success: {
label: "Valid",
color: "hsl(var(--accent-4))",
},
error: {
label: "Invalid",
color: "hsl(var(--orange-9))",
},
}}
/>
}
stats={
<>
<MetricStats
successCount={passed}
errorCount={blocked}
successLabel="VALID"
errorLabel="INVALID"
/>
<div className="flex items-center gap-2 min-w-0 max-w-[40%]">
<Key className="text-accent-11 flex-shrink-0" />
<div className="text-xs text-accent-9 truncate">
{keyCount > 0 ? `${keyCount} ${keyCount === 1 ? "Key" : "Keys"}` : "No data"}
</div>
</div>
</>
}
icon={<ProgressBar className="text-accent-11" />}
/>
);
};
84 changes: 84 additions & 0 deletions apps/dashboard/app/(app)/apis/_components/api-list-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client";

import { EmptyComponentSpacer } from "@/components/empty-component-spacer";
import type {
ApiOverview,
ApisOverviewResponse,
} from "@/lib/trpc/routers/api/query-overview/schemas";
import { BookBookmark } from "@unkey/icons";
import { Button, Empty } from "@unkey/ui";
import { useState } from "react";
import { ApiListGrid } from "./api-list-grid";
import { ApiListControlCloud } from "./control-cloud";
import { ApiListControls } from "./controls";
import { CreateApiButton } from "./create-api-button";

export const ApiListClient = ({
initialData,
unpaid,
}: {
initialData: ApisOverviewResponse;
unpaid: boolean;
}) => {
const [isSearching, setIsSearching] = useState<boolean>(false);
const [apiList, setApiList] = useState<ApiOverview[]>(initialData.apiList);

if (unpaid) {
return (
<EmptyComponentSpacer>
<Empty className="border border-gray-6 rounded-lg bg-gray-1">
<Empty.Title className="text-xl">Upgrade your plan</Empty.Title>
<Empty.Description>
Team workspaces is a paid feature. Please switch to a paid plan to continue using it.
</Empty.Description>
<Empty.Actions className="mt-4 ">
<a href="/settings/billing" target="_blank" rel="noopener noreferrer">
<Button>
<BookBookmark />
Subscribe
</Button>
</a>
</Empty.Actions>
</Empty>
</EmptyComponentSpacer>
);
}

return (
<div className="flex flex-col">
<ApiListControls apiList={apiList} onApiListChange={setApiList} onSearch={setIsSearching} />
<ApiListControlCloud />
{initialData.apiList.length > 0 ? (
<ApiListGrid
isSearching={isSearching}
initialData={initialData}
setApiList={setApiList}
apiList={apiList}
/>
) : (
<EmptyComponentSpacer>
<Empty className="m-0 p-0">
<Empty.Icon />
<Empty.Title>No APIs found</Empty.Title>
<Empty.Description>
You haven&apos;t created any APIs yet. Create one to get started.
</Empty.Description>
<Empty.Actions className="mt-4 ">
<CreateApiButton />
<a
href="https://www.unkey.com/docs/introduction"
target="_blank"
rel="noopener noreferrer"
>
<Button>
<BookBookmark />
Documentation
</Button>
</a>
</Empty.Actions>
</Empty>
</EmptyComponentSpacer>
)}
</div>
);
};
68 changes: 68 additions & 0 deletions apps/dashboard/app/(app)/apis/_components/api-list-grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { EmptyComponentSpacer } from "@/components/empty-component-spacer";
import type {
ApiOverview,
ApisOverviewResponse,
} from "@/lib/trpc/routers/api/query-overview/schemas";
import { ChevronDown } from "@unkey/icons";
import { Button, Empty } from "@unkey/ui";
import type { Dispatch, SetStateAction } from "react";
import { ApiListCard } from "./api-list-card";
import { useFetchApiOverview } from "./hooks/use-fetch-api-overview";

export const ApiListGrid = ({
initialData,
setApiList,
apiList,
isSearching,
}: {
initialData: ApisOverviewResponse;
apiList: ApiOverview[];
setApiList: Dispatch<SetStateAction<ApiOverview[]>>;
isSearching?: boolean;
}) => {
const { total, loadMore, isLoading, hasMore } = useFetchApiOverview(initialData, setApiList);

if (apiList.length === 0) {
return (
<EmptyComponentSpacer>
<Empty className="m-0 p-0">
<Empty.Icon />
<Empty.Title>No APIs found</Empty.Title>
<Empty.Description>
No APIs match your search criteria. Try a different search term.
</Empty.Description>
</Empty>
</EmptyComponentSpacer>
);
}

return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 md:gap-5 w-full p-5">
{apiList.map((api) => (
<ApiListCard api={api} key={api.id} />
))}
</div>
<div className="flex flex-col items-center justify-center mt-8 space-y-4">
<div className="text-center text-sm text-accent-11">
Showing {apiList.length} of {total} APIs
</div>
{!isSearching && hasMore && (
<Button onClick={loadMore} disabled={isLoading}>
{isLoading ? (
<div className="flex items-center space-x-2">
<div className="animate-spin h-4 w-4 border-2 border-gray-7 border-t-transparent rounded-full" />
<span>Loading...</span>
</div>
) : (
<div className="flex items-center space-x-2">
<ChevronDown />
<span>Load more</span>
</div>
)}
</Button>
)}
</div>
</>
);
};
1 change: 1 addition & 0 deletions apps/dashboard/app/(app)/apis/_components/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_OVERVIEW_FETCH_LIMIT = 9;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { HISTORICAL_DATA_WINDOW } from "@/components/logs/constants";
import { ControlCloud } from "@/components/logs/control-cloud";
import { useFilters } from "../hooks/use-filters";

const formatFieldName = (field: string): string => {
switch (field) {
case "startTime":
return "Start time";
case "endTime":
return "End time";
case "since":
return "";
default:
return field.charAt(0).toUpperCase() + field.slice(1);
}
};

export const ApiListControlCloud = () => {
const { filters, updateFilters, removeFilter } = useFilters();
return (
<ControlCloud
historicalWindow={HISTORICAL_DATA_WINDOW}
formatFieldName={formatFieldName}
filters={filters}
removeFilter={removeFilter}
updateFilters={updateFilters}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { DatetimePopover } from "@/components/logs/datetime/datetime-popover";
import { cn } from "@/lib/utils";
import { Calendar } from "@unkey/icons";
import { Button } from "@unkey/ui";
import { useEffect, useState } from "react";
import { useFilters } from "../../../hooks/use-filters";

export const LogsDateTime = () => {
const [title, setTitle] = useState<string | null>(null);
const { filters, updateFilters } = useFilters();

useEffect(() => {
if (!title) {
setTitle("Last 12 hours");
}
}, [title]);

const timeValues = filters
.filter((f) => ["startTime", "endTime", "since"].includes(f.field))
.reduce(
(acc, f) => ({
// biome-ignore lint/performance/noAccumulatingSpread: it's safe to spread
...acc,
[f.field]: f.value,
}),
{},
);

return (
<DatetimePopover
initialTimeValues={timeValues}
onDateTimeChange={(startTime, endTime, since) => {
const activeFilters = filters.filter(
(f) => !["endTime", "startTime", "since"].includes(f.field),
);
if (since !== undefined) {
updateFilters([
...activeFilters,
{
field: "since",
value: since,
id: crypto.randomUUID(),
operator: "is",
},
]);
return;
}
if (since === undefined && startTime) {
activeFilters.push({
field: "startTime",
value: startTime,
id: crypto.randomUUID(),
operator: "is",
});
if (endTime) {
activeFilters.push({
field: "endTime",
value: endTime,
id: crypto.randomUUID(),
operator: "is",
});
}
}
updateFilters(activeFilters);
}}
initialTitle={title ?? ""}
onSuggestionChange={setTitle}
>
<div className="group">
<Button
variant="ghost"
className={cn(
"group-data-[state=open]:bg-gray-4 px-2",
!title ? "opacity-50" : "",
title !== "Last 12 hours" ? "bg-gray-4" : "",
)}
aria-label="Filter logs by time"
aria-haspopup="true"
title="Press 'T' to toggle filters"
disabled={!title}
>
<Calendar className="text-gray-9 size-4" />
<span className="text-gray-12 font-medium text-[13px]">{title ?? "Loading..."}</span>
</Button>
</div>
</DatetimePopover>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { RefreshButton } from "@/components/logs/refresh-button";
import { trpc } from "@/lib/trpc/client";
import { useRouter } from "next/navigation";
import { useFilters } from "../../hooks/use-filters";

export const LogsRefresh = () => {
const { filters } = useFilters();
const { api } = trpc.useUtils();
const { refresh } = useRouter();
const hasRelativeFilter = filters.find((f) => f.field === "since");

const handleRefresh = () => {
api.logs.queryVerificationTimeseries.invalidate();
refresh();
};

return <RefreshButton onRefresh={handleRefresh} isEnabled={Boolean(hasRelativeFilter)} />;
};
Loading