Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1598…
Browse files Browse the repository at this point in the history
…-creating-a-key-in-the-ui-with-remaining-forces-you-to-choose
  • Loading branch information
MichaelUnkey committed Jan 9, 2025
2 parents 4307ecc + a4d0af6 commit 6007ef7
Show file tree
Hide file tree
Showing 15 changed files with 324 additions and 29 deletions.
6 changes: 6 additions & 0 deletions apps/dashboard/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @unkey/web

## 0.1.40

### Patch Changes

- @unkey/ratelimit@0.5.1

## 0.1.39

### Patch Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export default async function APIKeyDetailPage(props: {
.then((res) => res.val?.at(0)?.time ?? 0),
]);

// Sort all verifications by time first
const sortedVerifications = verifications.val!.sort((a, b) => a.time - b.time);

const successOverTime: { x: string; y: number }[] = [];
const ratelimitedOverTime: { x: string; y: number }[] = [];
const usageExceededOverTime: { x: string; y: number }[] = [];
Expand All @@ -119,30 +122,47 @@ export default async function APIKeyDetailPage(props: {
const expiredOverTime: { x: string; y: number }[] = [];
const forbiddenOverTime: { x: string; y: number }[] = [];

for (const d of verifications.val!.sort((a, b) => a.time - b.time)) {
// Get all unique timestamps
const uniqueDates = [...new Set(sortedVerifications.map((d) => d.time))].sort((a, b) => a - b);

// Ensure each array has entries for all timestamps with zero counts
for (const timestamp of uniqueDates) {
const x = new Date(timestamp).toISOString();
successOverTime.push({ x, y: 0 });
ratelimitedOverTime.push({ x, y: 0 });
usageExceededOverTime.push({ x, y: 0 });
disabledOverTime.push({ x, y: 0 });
insufficientPermissionsOverTime.push({ x, y: 0 });
expiredOverTime.push({ x, y: 0 });
forbiddenOverTime.push({ x, y: 0 });
}

for (const d of sortedVerifications) {
const x = new Date(d.time).toISOString();
const index = uniqueDates.indexOf(d.time);

switch (d.outcome) {
case "":
case "VALID":
successOverTime.push({ x, y: d.count });
successOverTime[index] = { x, y: d.count };
break;
case "RATE_LIMITED":
ratelimitedOverTime.push({ x, y: d.count });
ratelimitedOverTime[index] = { x, y: d.count };
break;
case "USAGE_EXCEEDED":
usageExceededOverTime.push({ x, y: d.count });
usageExceededOverTime[index] = { x, y: d.count };
break;
case "DISABLED":
disabledOverTime.push({ x, y: d.count });
disabledOverTime[index] = { x, y: d.count };
break;
case "INSUFFICIENT_PERMISSIONS":
insufficientPermissionsOverTime.push({ x, y: d.count });
insufficientPermissionsOverTime[index] = { x, y: d.count };
break;
case "EXPIRED":
expiredOverTime.push({ x, y: d.count });
expiredOverTime[index] = { x, y: d.count };
break;
case "FORBIDDEN":
forbiddenOverTime.push({ x, y: d.count });
forbiddenOverTime[index] = { x, y: d.count };
break;
}
}
Expand Down Expand Up @@ -209,6 +229,7 @@ export default async function APIKeyDetailPage(props: {
stats.forbidden += v.count;
}
});

const roleTee = key.workspace.roles.map((role) => {
const nested: NestedPermissions = {};
for (const permission of key.workspace.permissions) {
Expand Down Expand Up @@ -328,7 +349,7 @@ export default async function APIKeyDetailPage(props: {
<Metric label="Valid" value={formatNumber(stats.valid)} />
<Metric label="Ratelimited" value={formatNumber(stats.ratelimited)} />
<Metric label="Usage Exceeded" value={formatNumber(stats.usageExceeded)} />
<Metric label="Disabled" value={formatNumber(stats.valid)} />
<Metric label="Disabled" value={formatNumber(stats.disabled)} />
<Metric
label="Insufficient Permissions"
value={formatNumber(stats.insufficientPermissions)}
Expand Down
65 changes: 60 additions & 5 deletions apps/dashboard/components/dashboard/charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ type ColorName = "primary" | "warn" | "danger";
export const useColors = (colorNames: Array<ColorName>) => {
const { resolvedTheme } = useTheme();

const colors: { light: Record<ColorName, string>; dark: Record<ColorName, string> } = {
const colors: {
light: Record<ColorName, string>;
dark: Record<ColorName, string>;
} = {
light: {
primary: "#1c1917",
warn: "#FFCD07",
Expand Down Expand Up @@ -133,9 +136,9 @@ export const LineChart: React.FC<{
tooltip={{
formatter: (datum) => ({
name: datum.category,
value: `${Intl.NumberFormat(undefined, { notation: "compact" }).format(
Number(datum.y),
)} ms`,
value: `${Intl.NumberFormat(undefined, {
notation: "compact",
}).format(Number(datum.y))} ms`,
}),
}}
/>
Expand Down Expand Up @@ -202,6 +205,53 @@ export const StackedColumnChart: React.FC<{
colors: Array<ColorName>;
}> = ({ data, timeGranularity, colors }) => {
const { axisColor } = useColors(colors);

const formatDate = (date: string) => {
const d = new Date(date);
if (Number.isNaN(d.getTime())) {
return date;
}

switch (timeGranularity) {
case "minute":
return d.toLocaleString(undefined, {
hour: "numeric",
minute: "2-digit",
hour12: true,
month: "short",
day: "numeric",
});
case "hour":
return d.toLocaleString(undefined, {
hour: "numeric",
hour12: true,
month: "short",
day: "numeric",
year: "numeric",
});
case "day":
return d.toLocaleString(undefined, {
weekday: "short",
month: "short",
day: "numeric",
year: "numeric",
});
case "month":
return d.toLocaleString(undefined, {
month: "long",
year: "numeric",
});
default:
return d.toLocaleString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
}
};

return (
<Column
isStack={true}
Expand Down Expand Up @@ -276,9 +326,14 @@ export const StackedColumnChart: React.FC<{
},
}}
tooltip={{
title: formatDate,
formatter: (datum) => ({
name: datum.category,
value: Intl.NumberFormat(undefined, { notation: "compact" }).format(Number(datum.y)),
value: Intl.NumberFormat(undefined, {
notation: "compact",
maximumFractionDigits: 1,
compactDisplay: "short",
}).format(Number(datum.y)),
}),
}}
/>
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@unkey/dashboard",
"version": "0.1.39",
"version": "0.1.40",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
151 changes: 151 additions & 0 deletions apps/engineering/content/docs/contributing/client-structure.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
title: Client-Side Structure
description: How to structure your PR when contributing to Unkey dashboard
---

# Client-Side Structure Contribution Guidelines

## Overview

When contributing to the Unkey dashboard or any client app in the Unkey repository, we follow a feature-based architecture. This guide will help you understand how to structure your code to maintain consistency across the project.

## Directory Structure

Each feature (typically corresponding to a Next.js page) should be organized as a self-contained module with the following structure:

```
feature-name/
├── components/ # Feature-specific React components
│ ├── component-name/ # Complex components get their own directory
│ │ ├── index.tsx
│ │ └── sub-component.tsx
│ └── simple-component.tsx
├── hooks/ # Custom hooks for the feature
│ ├── queries/ # API query hooks
│ │ ├── use-feature-list.ts
│ │ └── use-feature-details.ts
│ └── use-feature-logic.ts
├── actions/ # Server actions and API calls
│ └── feature-actions.ts
├── types/ # TypeScript types and interfaces
│ └── feature.ts
├── schemas/ # Validation schemas
│ └── feature.ts
├── utils/ # Helper functions
│ └── feature-helpers.ts
├── constants.ts # Feature-specific constants
└── page.tsx # Main page component
```

## Key Principles

1. **Feature Isolation**

- Keep all related code within the feature directory
- Don't import feature-specific components into other features
- Use shared components from the global `/components` directory or `unkey/ui` package for common UI elements

2. **Component Organization**

- Simple components can be single files **(No need to break everything into 50 different files, follow your common sense)**
- Complex components should have their own directory with an index.tsx
- Keep component-specific styles, tests, and utilities close to the component

3. **Code Colocation**
- Place related code as close as possible to where it's used
- If a utility is only used by one component, keep it in the component's directory

## Example Page Structure

Here's an example of how to structure a typical page component:

```typescript
import { Navbar } from "@/components/navbar"; // Global shared component
import { PageContent } from "@/components/page-content";
import { FeatureComponent } from "./components/feature-component";

export default function FeaturePage() {
// Page implementation
// This is also we where we do our server side data fetching.
return (
<div>
<Navbar>{/* Navigation content */}</Navbar>
<PageContent>
{/* Entry to our actual component. This one is usually a client-side component */}
<FeatureComponent />
</PageContent>
</div>
);
}
```

## Best Practices

1. **File Naming**

- Use kebab-case for directory and file names
- The directory structure itself provides context, so explicit suffixes are optional
- If you choose to use suffixes for additional clarity, common patterns include:
- `auth.schema.ts` or just `auth-schema.ts` for validation schemas
- `auth.type.ts` or just `auth-types.ts` for type definitions
- `.client.tsx` for client-specific components
- `.server.ts` for server-only code
- `.action.ts` for server actions

2. **Code Organization**

- Keep files focused and single-purpose
- Use index.ts files to expose public API of complex components
- Colocate tests with the code they test
- Place shared types in the feature's `types` directory

3. **Imports and Exports**
- Use absolute imports for shared components (`@/components`)
- Never use default exports unless it's absolutely necessary
- Use relative imports within a feature
- Export complex components through index files
- Avoid circular dependencies

## Shared Code

Global shared code should be placed in root-level directories:

```
/components # Shared React components
/hooks # Shared custom hooks
/utils # Shared utilities
/types # Shared TypeScript types
/constants # Global constants
```

Only place code in these directories if it's used across multiple features.

## Example Feature Implementation

Here's a practical example of how to structure a feature:

```typescript
// /feature/components/feature-list/index.tsx
export function FeatureList() {
// Component implementation
}

// /feature/hooks/queries/use-features.ts
export function useFeatures() {
// Hook implementation
}

// /feature/actions/feature-actions.ts
export async function createFeature() {
// Server action implementation
}

// /feature/types/feature.ts
export interface Feature {
// Type definitions
}
```

## Questions?

If you're unsure about where to place certain code or how to structure your feature, please don't hesitate to ask in our Discord community or in your pull request. We're here to help!
2 changes: 1 addition & 1 deletion apps/engineering/content/docs/contributing/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"description": "oss/acc",
"icon": "GitPullRequest",
"root": true,
"pages": ["index", "sdk-development", "testing", "versions"]
"pages": ["index", "sdk-development", "testing", "versions", "client-structure"]
}
6 changes: 6 additions & 0 deletions packages/api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @unkey/api

## 0.30.0

### Minor Changes

- 0746b33: Add the types for error codes

## 0.29.0

### Minor Changes
Expand Down
13 changes: 10 additions & 3 deletions packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@unkey/api",
"version": "0.29.0",
"version": "0.30.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
Expand All @@ -9,12 +9,19 @@
"publishConfig": {
"access": "public"
},
"keywords": ["unkey", "client", "api"],
"keywords": [
"unkey",
"client",
"api"
],
"bugs": {
"url": "https://github.com/unkeyed/unkey/issues"
},
"homepage": "https://github.com/unkeyed/unkey#readme",
"files": ["./dist/**", "README.md"],
"files": [
"./dist/**",
"README.md"
],
"author": "Andreas Thomas <[email protected]>",
"scripts": {
"generate": "openapi-typescript https://api.unkey.dev/openapi.json -o ./src/openapi.d.ts",
Expand Down
Loading

0 comments on commit 6007ef7

Please sign in to comment.