Co-authored-by: Saga4 <sarthak.saga@gmail.com>
This commit is contained in:
HeshamHM28 2025-05-27 16:34:13 +03:00 committed by GitHub
parent c6d08a3ca7
commit b09b74e02b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 2950 additions and 111 deletions

View file

@ -49,6 +49,7 @@ class AuthBearer(HttpBearer):
raise HttpError(403, "Invalid API key")
request.user = api_key_instance.user_id
request.tier = api_key_instance.tier
request.api_key_id = api_key_instance.id
request.should_log_features = await check_subscription_status(user_id=request.user, tier=request.tier)
return token

View file

@ -52,3 +52,17 @@ class Subscriptions(models.Model):
class Meta:
managed = False
db_table = "subscriptions"
# Users model
class Users(models.Model):
user_id = models.CharField(primary_key=True, max_length=255)
github_username = models.CharField(max_length=255, unique=True)
email = models.EmailField(null=True, blank=True)
name = models.CharField(max_length=255, null=True, blank=True)
onboarding_completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
managed = False
db_table = "users"

View file

@ -0,0 +1,8 @@
from django.core.exceptions import ObjectDoesNotExist
from .models import Users
def get_user_by_id(user_id):
try:
return Users.objects.aget(user_id=user_id)
except ObjectDoesNotExist:
return None

View file

@ -0,0 +1,33 @@
from datetime import datetime, timezone
from log_features.models import Repositories, OptimizationEvents
def get_repository(owner, repo_name):
try:
if owner and repo_name:
return Repositories.objects.aget(full_name=f"{owner}/{repo_name}")
return None
except Repositories.DoesNotExist:
return None
def log_optimization_event(
event_type=None,
user_id=None,
repository_id=None,
trace_id=None,
api_key_id=None,
metadata=None,
current_username=None,
):
return OptimizationEvents.objects.acreate(
event_type=event_type,
user_id=user_id,
repository_id=repository_id,
trace_id=trace_id,
api_key_id=api_key_id,
metadata=metadata,
created_at=datetime.now(timezone.utc),
current_username=current_username,
)

View file

@ -1,3 +1,4 @@
import uuid
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MinLengthValidator
from django.db import models
@ -37,3 +38,37 @@ class OptimizationFeatures(models.Model):
class Meta:
managed = False
db_table = "optimization_features"
class OptimizationEvents(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
event_type = models.CharField(max_length=64) # 'pr_created', 'pr_merged', 'pr_closed'
user_id = models.CharField(max_length=255, null=True, blank=True)
repository_id = models.CharField(max_length=255, null=True, blank=True)
trace_id = models.CharField(max_length=36, null=True, blank=True)
pr_id = models.CharField(max_length=255, null=True, blank=True, unique=True)
api_key_id = models.IntegerField(null=True, blank=True)
metadata = models.JSONField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
current_username = models.CharField(max_length=255, null=True, blank=True) # for the current user who did this event
class Meta:
managed = False
db_table = "optimization_events"
class Repositories(models.Model):
id = models.CharField(max_length=255, primary_key=True)
github_repo_id = models.CharField(max_length=255, unique=True)
installation_id = models.IntegerField()
name = models.CharField(max_length=255)
full_name = models.CharField(max_length=255)
is_private = models.BooleanField()
is_active = models.BooleanField(default=True)
has_github_action = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
last_optimized = models.DateTimeField(null=True, blank=True)
optimizations_limit = models.IntegerField(null=True, blank=True)
optimizations_used = models.IntegerField(default=0)
class Meta:
managed = False
db_table = "repositories"

View file

@ -16,6 +16,8 @@ from aiservice.env_specific import (
debug_log_sensitive_data_from_callable,
)
from aiservice.models.aimodels import OPTIMIZE_MODEL
from authapp.user import get_user_by_id
from log_features.log_event import get_repository, log_optimization_event
from log_features.log_features import log_features
from ninja import NinjaAPI, Schema
from openai import OpenAIError
@ -139,12 +141,17 @@ class OptimizeSchema(Schema):
python_version: str
experiment_metadata: dict[str, str] | None = None
codeflash_version: str | None = None
current_username: str | None = None
repo_owner : str | None = None
repo_name : str | None = None
class OptimizeResponseItemSchema(Schema):
source_code: str
explanation: str
optimization_id: str
optimization_event_id: str | None = None
class OptimizeResponseSchema(Schema):
@ -196,7 +203,26 @@ async def optimize(request, data: OptimizeSchema) -> tuple[int, OptimizeResponse
processed_optimizations: list[CodeExplanationAndID] = optimizations_postprocessing_pipeline(
data.source_code, traced_optimizations
)
try:
repository = await get_repository(data.repo_owner, data.repo_name)
except Exception:
repository = None
if data.current_username is None:
user = await get_user_by_id(request.user)
data.current_username = user.github_username
event = await log_optimization_event(
event_type="no-pr",
user_id=request.user,
current_username=data.current_username ,
repository_id=repository.id if repository else None,
trace_id=data.trace_id,
api_key_id=request.api_key_id,
metadata={
"codeflash_version": data.codeflash_version,
"num_optimizations": len(optimized_code_and_explanations),
"experiment_metadata": data.experiment_metadata,
}
)
if hasattr(request, "should_log_features") and request.should_log_features:
await log_features(
trace_id=data.trace_id,
@ -212,7 +238,7 @@ async def optimize(request, data: OptimizeSchema) -> tuple[int, OptimizeResponse
response = OptimizeResponseSchema(
optimizations=[
OptimizeResponseItemSchema(
source_code=ce.cst_module.code, explanation=ce.explanation, optimization_id=ce.id
source_code=ce.cst_module.code, explanation=ce.explanation, optimization_id=ce.id, optimization_event_id=str(event.id) if event else None,
)
for ce in processed_optimizations
]

View file

@ -1,3 +1,4 @@
import * as Sentry from "@sentry/node"
import { fileDiffsToMap, isDiffContentsWellFormed } from "../diff_utils.js"
import { type FileDiffContent } from "@codeflash-ai/code-suggester/build/src/types.js"
import { userNickname } from "../auth0-mgmt.js"
@ -16,6 +17,13 @@ import {
import { posthog } from "../analytics.js"
import { PrismaClient } from "@prisma/client"
import { Request, Response } from "express"
import {
createAppInstallation,
createRepositoryMember,
getAppInstallationByInstalltionId,
upsertRepository,
} from "@codeflash-ai/common"
import { AuthorizedUserReq } from "types.js"
import { requestApproval, requiresApproval } from "../github/optimization_approval.js"
const prisma = new PrismaClient()
@ -237,7 +245,18 @@ export async function triggerCreatePr(
generatedTests,
coverage,
)
try {
await prisma.optimization_events.update({
where: { trace_id: traceId },
data: {
pr_id: String(newPrData.data.id),
is_optimization_found: true,
event_type: "pr_created",
},
})
} catch (eventError) {
console.error("Failed to update optimization event:", eventError)
}
await addLabelToPullRequest(installationOctokit, owner, repo, newPrData.data.number)
// Assign the user who initiated the create-pr as the reviewer
@ -293,6 +312,81 @@ export async function triggerCreatePr(
}
}
// Endpoint to manually add repositories, similar to webhook logic
export async function addRepositoryManually(req: AuthorizedUserReq, res: Response): Promise<void> {
try {
const { repositories_added, installation } = req.body
// Check if required fields are missing
if (!repositories_added || !installation?.id) {
res.status(400).send("Missing required fields")
return
}
// Check if the installation exists, if not, create it
const installationExists = await getAppInstallationByInstalltionId(installation.id)
if (!installationExists) {
// Create the installation if it doesn't exist
await createAppInstallation({
installation_id: installation.id,
account_id: installation.account.id,
account_login: installation.account.login,
account_type: installation.account.type,
})
console.log(`Installation created for ID: ${installation.id}`)
}
// Process each repository in the list of added repositories
for (const repo of repositories_added) {
try {
// Upsert logic for repository creation or update
await prisma.repositories.upsert({
where: { github_repo_id: String(repo.id) },
update: {
name: repo.name,
full_name: repo.full_name,
is_private: repo.private,
installation_id: installation.id,
},
create: {
github_repo_id: String(repo.id),
installation_id: installation.id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.private,
is_active: true,
},
})
const savedRepo = await upsertRepository({
github_repo_id: String(repo.id),
installation_id: installation.id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.private,
})
await createRepositoryMember({
repository_id: savedRepo.id,
user_id: req.userId,
role: "",
})
console.log(`Repository upserted: ${repo.full_name}`)
} catch (error) {
console.error(`Failed to add/reactivate repository ${repo.full_name}:`, error)
Sentry.captureException(error)
}
}
res.status(200).send("Repositories added successfully")
} catch (error) {
console.error(`Error adding repositories: ${error}`)
if (error instanceof Error) {
res.status(500).send(`Error adding repositories: ${error.message}`)
} else {
res.status(500).send("Error adding repositories")
}
}
}
async function getUserNickname(userId: string): Promise<string> {
const nickname = await userNickname(userId)
if (!nickname) throw new Error("Unauthorized - could not get nickname")

View file

@ -0,0 +1,25 @@
import { prisma } from "@codeflash-ai/common"
export async function optimizationSuccess(req: any, res: any): Promise<void> {
const { trace_id, is_optimization_found } = req.body
if (typeof trace_id === "undefined" || typeof is_optimization_found !== "boolean") {
return res
.status(400)
.json({ error: "Invalid input: trace_id and is_optimization_found(boolean) are required." })
}
try {
const result = await prisma.optimization_events.updateMany({
where: { trace_id: trace_id },
data: { is_optimization_found: is_optimization_found },
})
if (result.count === 0) {
return res.status(404).json({ error: "Optimization event not found." })
}
return res.status(200).json({ message: "Optimization status updated." })
} catch (error) {
console.error("Error in markOptimizationSuccess:", error)
return res.status(500).json({ error: "Internal server error." })
}
}

View file

@ -27,9 +27,25 @@ const slackNotificationConfig = {
"albumentations-team": ["albumentations"],
"Skyvern-AI": ["skyvern"],
roboflow: ["inference"],
"gdsfactory": ["gdsfactory"],
gdsfactory: ["gdsfactory"],
}
// Utility function to update optimization event in the database
async function updateOptimizationEvent(traceId: string, prId?: string) {
if (traceId !== "") {
try {
await prisma.optimization_events.update({
where: { trace_id: traceId },
data: {
...(prId ? { pr_id: String(prId) } : {}),
is_optimization_found: true,
event_type: prId ? "pr_created" : "no-pr",
},
})
} catch (eventError) {
console.error("Failed to update optimization event:", eventError)
}
}
}
export async function suggestPrChanges(
req: AuthorizedUserReq,
res: Response,
@ -340,6 +356,8 @@ export async function triggerSuggestPrChanges(
})
}
}
await updateOptimizationEvent(traceId, newPrData.data.id)
res.json(newPrData.data.number)
// For backward compatibility
if (res) {
@ -466,6 +484,8 @@ export async function triggerSuggestPrChanges(
})
}
}
await updateOptimizationEvent(traceId)
res.json(review.data.id)
// For backward compatibility, handle the response object if provided
if (res) {

View file

@ -8,6 +8,14 @@ import {
import { posthog } from "../analytics.js"
import * as Sentry from "@sentry/node"
import {
createAppInstallation,
createRepositoryMember,
getAppInstallationByInstalltionId,
prisma,
upsertRepository,
} from "@codeflash-ai/common"
import { createNodeMiddleware } from "@octokit/webhooks"
const APP_ID: string = process.env.GH_APP_ID ?? "" // GitHub App ID
@ -73,6 +81,20 @@ githubApp.webhooks.on("pull_request.edited", async ({ octokit, payload }) => {
githubApp.webhooks.on("pull_request.closed", async ({ octokit, payload }) => {
if (payload.pull_request) {
const prId = String(payload.pull_request.id)
try {
await prisma.optimization_events.updateMany({
where: { pr_id: prId },
data: {
event_type: payload.pull_request.merged ? "pr_merged" : "pr_closed",
},
})
console.log(
`Updated optimization_event for PR ID ${prId} to ${payload.pull_request.merged ? "pr_merged" : "pr_closed"}`,
)
} catch (err) {
console.error(`Failed to update optimization_event for PR ID ${prId}:`, err)
}
console.log(
`Received a pull request closed event. PR #${payload.pull_request.number} ` +
`by ${payload.pull_request.user.login} was closed.`,
@ -287,6 +309,83 @@ githubApp.webhooks.onError(error => {
Sentry.captureException(error)
})
githubApp.webhooks.on("installation_repositories", async ({ payload }) => {
const { repositories_added, installation, sender } = payload
// Check if required fields are missing
if (!repositories_added || !installation?.id) {
return
}
const account = installation.account
let accountLogin: string | undefined
// Check if the account is a user or an organization
if ("login" in account) {
// It's a user account, use `login`
accountLogin = account.login
} else if ("slug" in account) {
// It's an organization account, use `slug`
accountLogin = account.slug
}
let accountType: string | undefined
if ("type" in account) {
accountType = account.type
} else {
accountType = "Organization" // fallback assumption
}
if (!accountLogin) {
console.error("Error: Account login or slug not found")
return
}
// Check if the installation exists, if not, create it
const installationExists = await getAppInstallationByInstalltionId(installation.id)
if (!installationExists) {
// Create the installation if it doesn't exist
await createAppInstallation({
installation_id: installation.id,
account_id: installation.account.id,
account_login: accountLogin,
account_type: accountType,
})
console.log(`Installation created for ID: ${installation.id}`)
}
// Process each repository in the list of added repositories
for (const repo of repositories_added) {
try {
// Upsert logic for repository creation or update
const savedRepo = await upsertRepository({
github_repo_id: String(repo.id),
installation_id: installation.id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.private,
})
// Get the GitHub user ID of the sender
const githubUserId = sender?.id
if (githubUserId) {
console.log(`GitHub User ID: ${githubUserId} triggered the event`)
} else {
console.error("GitHub User ID not found in sender.")
}
await createRepositoryMember({
repository_id: savedRepo.id,
user_id: `github|${githubUserId}`,
role: "",
})
console.log(`Repository upserted: ${repo.full_name}`)
} catch (error) {
console.error(`Failed to add/reactivate repository ${repo.full_name}:`, error)
Sentry.captureException(error)
}
}
})
export const ghAppPathPrefix: string = "/cfapi/github"
export const ghAppMiddleware = createNodeMiddleware(githubApp.webhooks, {

View file

@ -10,7 +10,7 @@ import { AsyncExpressApp } from "./types.js"
import * as console from "console"
import { posthog } from "./analytics.js"
import { suggestPrChanges } from "./endpoints/suggest-pr-changes.js"
import { createPr } from "./endpoints/create-pr.js"
import { addRepositoryManually, createPr } from "./endpoints/create-pr.js"
import { isGitHubAppInstalled } from "./endpoints/is-github-app-installed.js"
import { logRequestBody, logRequestDetails } from "./middlewares/logRequestDetails.js"
import { healthcheck } from "./endpoints/healthcheck.js"
@ -24,6 +24,7 @@ import { stripeWebhookHandler } from "./endpoints/stripe-webhook.js"
import { trackUsage } from "./middlewares/track-usage.js"
import { testSentry } from "./endpoints/sentry-test.js"
import { idLimiter } from "./middlewares/rate-limit.js"
import { optimizationSuccess } from "endpoints/optimiaztion-success.js"
import { handleSlackEvents } from "./endpoints/slack-events.js"
const port = process.env.PORT ?? 3001
// Define a custom type for the wrapped Express app
@ -137,6 +138,9 @@ appExpress.get("/cfapi/cli-get-user", getUser)
// Return the list of ALL repositories where Codeflash is installed
// Seems to not be used anywhere; scheduled for deletion by June 30, 2024
// appExpress.getAsync("/cfapi/installed_repositories", installedRepositories)
appExpress.postAsync("/cfapi/test-repo", addRepositoryManually)
appExpress.postAsync("/cfapi/mark-as-success", optimizationSuccess)
appExpress.postAsync("/cfapi/suggest-pr-changes", suggestPrChanges)

View file

@ -13,7 +13,7 @@
"@azure/keyvault-keys": "^4.7.2",
"@azure/keyvault-secrets": "^4.7.0",
"@codeflash-ai/code-suggester": "^5.0.3",
"@codeflash-ai/common": "1.0.13",
"@codeflash-ai/common": "^1.0.14",
"@notionhq/client": "^2.2.14",
"@octokit/app": "^16.0.1",
"@octokit/auth-app": "^8.0.1",
@ -1059,9 +1059,9 @@
"license": "ISC"
},
"node_modules/@codeflash-ai/common": {
"version": "1.0.13",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.13/02410ec4f9ab78ed1bd81ec3ba30d50312154421",
"integrity": "sha512-jaVTCmszA3WIfTWhYFGSzMlbA2yCUAKpgIFJ9uZwZwZpUnDI9FVsb3IQmvtr52JuGfCxI+p38d4PxNprhZGVNg==",
"version": "1.0.14",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.14/916e58ad761bd6134a9cb66fb4a7c8b470b6be0b",
"integrity": "sha512-KnYLzPmOdnU33wKMsR7LaS/w31J805/zGy01tfxhvJw1lMiHqJjg7AUYnErJk7/8qkW3c9x8ElzyhAtG5T79Ew==",
"dependencies": {
"@azure/identity": "^4.2.0",
"@azure/keyvault-secrets": "^4.8.0",
@ -4213,7 +4213,6 @@
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.8.2.tgz",
"integrity": "sha512-5II+vbyzv4si6Yunwgkj0qT/iY0zyspttoDrL3R4BYgLdp42/d2C8xdi9vqkrYtKt9H32oFIukvyw3Koz5JoDg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},

View file

@ -28,7 +28,7 @@
"@azure/keyvault-keys": "^4.7.2",
"@azure/keyvault-secrets": "^4.7.0",
"@codeflash-ai/code-suggester": "^5.0.3",
"@codeflash-ai/common": "1.0.13",
"@codeflash-ai/common": "^1.0.14",
"@notionhq/client": "^2.2.14",
"@octokit/app": "^16.0.1",
"@octokit/auth-app": "^8.0.1",

View file

@ -5,6 +5,9 @@ let nextConfig = {
typescript: {
ignoreBuildErrors: false,
},
images: {
domains: ["avatars.githubusercontent.com", "github.com"],
},
}
// module.exports = nextConfig

View file

@ -10,7 +10,7 @@
"dependencies": {
"@auth0/nextjs-auth0": "^3.3.0",
"@azure/msal-node": "^2.6.6",
"@codeflash-ai/common": "1.0.13",
"@codeflash-ai/common": "^1.0.14",
"@hookform/resolvers": "^3.3.2",
"@prisma/client": "^6.2.1",
"@radix-ui/react-dialog": "^1.0.5",
@ -29,18 +29,21 @@
"@types/pg": "^8.10.9",
"@types/react": "^18",
"@types/react-dom": "^18",
"chart.js": "^4.4.9",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
"framer-motion": "^12.12.1",
"github-markdown-css": "^5.4.0",
"lucide-react": "^0.381.0",
"next": "^14.2.24",
"next": "^14.2.25",
"next-themes": "^0.3.0",
"pg": "^8.11.3",
"postcss": "^8",
"posthog-js": "^1.108.3",
"posthog-node": "^4.0.1",
"react": "^18",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18",
"react-hook-form": "^7.48.2",
"react-markdown": "^9.0.1",
@ -711,9 +714,9 @@
}
},
"node_modules/@codeflash-ai/common": {
"version": "1.0.13",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.13/02410ec4f9ab78ed1bd81ec3ba30d50312154421",
"integrity": "sha512-jaVTCmszA3WIfTWhYFGSzMlbA2yCUAKpgIFJ9uZwZwZpUnDI9FVsb3IQmvtr52JuGfCxI+p38d4PxNprhZGVNg==",
"version": "1.0.14",
"resolved": "https://npm.pkg.github.com/download/@codeflash-ai/common/1.0.14/916e58ad761bd6134a9cb66fb4a7c8b470b6be0b",
"integrity": "sha512-KnYLzPmOdnU33wKMsR7LaS/w31J805/zGy01tfxhvJw1lMiHqJjg7AUYnErJk7/8qkW3c9x8ElzyhAtG5T79Ew==",
"dependencies": {
"@azure/identity": "^4.2.0",
"@azure/keyvault-secrets": "^4.8.0",
@ -1651,11 +1654,15 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
},
"node_modules/@next/env": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.24.tgz",
"integrity": "sha512-LAm0Is2KHTNT6IT16lxT+suD0u+VVfYNQqM+EJTKuFRRuY2z+zj01kueWXPCxbMBDt0B5vONYzabHGUNbZYAhA==",
"license": "MIT"
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.28.tgz",
"integrity": "sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "14.2.3",
@ -1701,13 +1708,12 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.24.tgz",
"integrity": "sha512-7Tdi13aojnAZGpapVU6meVSpNzgrFwZ8joDcNS8cJVNuP3zqqrLqeory9Xec5TJZR/stsGJdfwo8KeyloT3+rQ==",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.28.tgz",
"integrity": "sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -1717,13 +1723,12 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.24.tgz",
"integrity": "sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz",
"integrity": "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -1733,13 +1738,12 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.24.tgz",
"integrity": "sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz",
"integrity": "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1749,13 +1753,12 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.24.tgz",
"integrity": "sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz",
"integrity": "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1765,13 +1768,12 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.24.tgz",
"integrity": "sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz",
"integrity": "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1781,13 +1783,12 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.24.tgz",
"integrity": "sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz",
"integrity": "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@ -1797,13 +1798,12 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.24.tgz",
"integrity": "sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz",
"integrity": "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -1813,13 +1813,12 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.24.tgz",
"integrity": "sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz",
"integrity": "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -1829,13 +1828,12 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.24.tgz",
"integrity": "sha512-cXcJ2+x0fXQ2CntaE00d7uUH+u1Bfp/E0HsNQH79YiLaZE5Rbm7dZzyAYccn3uICM7mw+DxoMqEfGXZtF4Fgaw==",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.28.tgz",
"integrity": "sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -4341,14 +4339,12 @@
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/helpers": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"tslib": "^2.4.0"
@ -5999,6 +5995,17 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chart.js": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/check-error": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
@ -6109,8 +6116,7 @@
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
"version": "2.1.1",
@ -6345,6 +6351,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -10932,12 +10947,11 @@
"peer": true
},
"node_modules/next": {
"version": "14.2.24",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.24.tgz",
"integrity": "sha512-En8VEexSJ0Py2FfVnRRh8gtERwDRaJGNvsvad47ShkC2Yi8AXQPXEA2vKoDJlGFSj5WE5SyF21zNi4M5gyi+SQ==",
"license": "MIT",
"version": "14.2.28",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.28.tgz",
"integrity": "sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA==",
"dependencies": {
"@next/env": "14.2.24",
"@next/env": "14.2.28",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
@ -10952,15 +10966,15 @@
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.24",
"@next/swc-darwin-x64": "14.2.24",
"@next/swc-linux-arm64-gnu": "14.2.24",
"@next/swc-linux-arm64-musl": "14.2.24",
"@next/swc-linux-x64-gnu": "14.2.24",
"@next/swc-linux-x64-musl": "14.2.24",
"@next/swc-win32-arm64-msvc": "14.2.24",
"@next/swc-win32-ia32-msvc": "14.2.24",
"@next/swc-win32-x64-msvc": "14.2.24"
"@next/swc-darwin-arm64": "14.2.28",
"@next/swc-darwin-x64": "14.2.28",
"@next/swc-linux-arm64-gnu": "14.2.28",
"@next/swc-linux-arm64-musl": "14.2.28",
"@next/swc-linux-x64-gnu": "14.2.28",
"@next/swc-linux-x64-musl": "14.2.28",
"@next/swc-win32-arm64-msvc": "14.2.28",
"@next/swc-win32-ia32-msvc": "14.2.28",
"@next/swc-win32-x64-msvc": "14.2.28"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@ -12238,6 +12252,15 @@
"node": ">=0.10.0"
}
},
"node_modules/react-chartjs-2": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@ -13600,7 +13623,6 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
},

View file

@ -20,7 +20,7 @@
"dependencies": {
"@auth0/nextjs-auth0": "^3.3.0",
"@azure/msal-node": "^2.6.6",
"@codeflash-ai/common": "1.0.13",
"@codeflash-ai/common": "^1.0.14",
"@hookform/resolvers": "^3.3.2",
"@prisma/client": "^6.2.1",
"@radix-ui/react-dialog": "^1.0.5",
@ -39,18 +39,21 @@
"@types/pg": "^8.10.9",
"@types/react": "^18",
"@types/react-dom": "^18",
"chart.js": "^4.4.9",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
"framer-motion": "^12.12.1",
"github-markdown-css": "^5.4.0",
"lucide-react": "^0.381.0",
"next": "^14.2.24",
"next": "^14.2.25",
"next-themes": "^0.3.0",
"pg": "^8.11.3",
"postcss": "^8",
"posthog-js": "^1.108.3",
"posthog-node": "^4.0.1",
"react": "^18",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18",
"react-hook-form": "^7.48.2",
"react-markdown": "^9.0.1",

View file

@ -0,0 +1,16 @@
<svg width="665" height="90" viewBox="0 0 665 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M133.849 79.4871C127.682 79.4871 125.148 77.4063 125.148 72.8666C125.148 71.6559 125.375 70.2184 125.715 68.5538L132.865 34.5811C134.416 27.0905 137.708 23.6478 145.652 23.6478H185.035L182.16 37.1915H150.722C148.301 37.1915 147.62 37.7589 147.09 40.1802L142.361 62.4629C142.248 63.0304 142.134 63.4465 142.134 63.9005C142.134 65.1111 142.777 65.5651 144.669 65.5651H176.107L173.232 79.4492H133.849V79.4871Z" fill="white"/>
<path d="M193.622 79.4871C187.455 79.4871 184.921 77.4063 184.921 72.8666C184.921 71.6559 185.148 70.2184 185.488 68.5538L192.638 34.5811C194.189 27.0905 197.481 23.6478 205.425 23.6478H243.37C249.764 23.6478 252.185 25.7285 252.185 30.2683C252.185 31.4789 251.958 32.9165 251.618 34.5811L244.467 68.5538C242.803 76.0444 239.625 79.4871 231.567 79.4871H193.622ZM234.972 40.1045C235.085 39.537 235.199 39.1209 235.199 38.7804C235.199 37.4563 234.329 37.0023 232.21 37.0023H210.381C208.074 37.0023 207.279 37.6454 206.863 40.1045L202.134 62.8034C202.02 63.3709 201.907 63.787 201.907 64.241C201.907 65.3381 202.55 65.7921 204.328 65.7921H226.157C228.805 65.7921 229.789 65.0354 230.243 62.8034L234.972 40.1045Z" fill="white"/>
<path d="M263.422 79.487C257.255 79.487 254.72 77.4063 254.72 72.8665C254.72 71.6559 254.947 70.2183 255.288 68.5537L262.438 34.581C263.989 27.0904 268.264 23.6477 275.225 23.6477H301.594L298.718 37.0023H280.295C277.987 37.0023 277.192 37.5697 276.663 40.1044L271.934 62.6898C271.82 63.2573 271.707 63.787 271.707 64.1274C271.707 65.338 272.35 65.792 274.242 65.792H298.151L312.035 0.305713H327.584L310.825 79.487H263.422Z" fill="white"/>
<path d="M332.238 79.4871C325.958 79.4871 323.423 77.4063 323.423 72.8666C323.423 71.656 323.65 70.2184 323.991 68.5538L331.141 34.5811C332.692 27.0905 335.983 23.6478 344.042 23.6478H381.759C387.396 23.6478 389.591 25.7285 389.591 30.0413C389.591 31.2519 389.477 32.576 389.137 34.1271L384.521 56.0694H347.371L349.262 46.8007H371.772L373.437 39.31C373.55 38.7426 373.55 38.3264 373.55 37.8724C373.55 36.7753 372.983 36.3213 371.242 36.3213H349.073C346.538 36.3213 345.971 36.9645 345.441 39.4235L340.258 63.4465C340.145 64.0897 340.031 64.6571 340.031 65.1111C340.031 66.4352 340.675 66.7757 342.566 66.7757H380.171L377.522 79.4492H332.314L332.238 79.4871Z" fill="white"/>
<path d="M390.801 79.487L404.799 13.0928C406.88 3.71055 410.852 0.305713 418.04 0.305713H437.22L434.686 12.3361H423.109C421.142 12.3361 420.234 13.0928 419.818 15.2113L418.04 23.6856H431.811L429.276 35.8295H415.505L406.237 79.487H390.801Z" fill="white"/>
<path d="M429.161 79.487L445.92 0.305713H461.356L444.596 79.487H429.161Z" fill="white"/>
<path d="M465.782 79.4871C460.145 79.4871 457.838 77.4063 457.838 73.0935C457.838 71.9964 458.064 70.5588 458.405 69.0077L461.167 55.7667C462.945 47.3681 465.593 45.0604 472.63 45.0604H500.965L498.998 54.5561H478.72C476.64 54.5561 475.959 55.1236 475.505 57.4313L474.294 63.2574C474.181 63.9005 474.067 64.3545 474.067 64.8084C474.067 65.9056 474.635 66.246 476.148 66.246H501.949L507.775 38.8939C507.889 38.4399 507.889 38.0238 507.889 37.6833C507.889 36.4727 507.245 35.7917 505.24 35.7917H467.409L469.944 23.6478H515.606C522 23.6478 524.421 25.7285 524.421 30.2683C524.421 31.4789 524.194 32.9165 523.853 34.5811L514.358 79.4492H465.744L465.782 79.4871Z" fill="white"/>
<path d="M525.746 79.487L528.394 66.6999H566.642C568.949 66.6999 569.517 66.1325 569.933 64.0517L571.144 58.5283C571.257 58.1879 571.257 57.8852 571.257 57.6582C571.257 56.6746 570.69 56.2206 569.063 56.2206H539.176C534.22 56.2206 532.026 54.1399 532.026 50.6972C532.026 49.9406 532.139 49.1461 532.366 48.276L535.469 33.3704C536.793 26.7499 540.084 23.6855 547.802 23.6855H593.88L591.232 36.1321H553.212C550.904 36.1321 549.996 36.8887 549.58 39.1208L548.596 43.7362C548.483 44.0767 548.483 44.3794 548.483 44.7199C548.483 45.9305 549.126 46.4979 550.79 46.4979H581.018C586.087 46.4979 588.281 48.6922 588.281 52.1348C588.281 52.778 588.168 53.5724 588.054 54.3291L584.763 69.7643C583.325 76.3848 580.034 79.487 572.316 79.487H525.784H525.746Z" fill="white"/>
<path d="M639.127 79.487L647.412 40.1044C647.525 39.537 647.639 39.1208 647.639 38.7803C647.639 37.4562 646.768 37.0023 644.877 37.0023H621.838C619.643 37.0023 618.735 37.6454 618.206 40.1044L609.921 79.487H594.485L611.245 0.305713H626.68L620.4 29.7386C623.275 25.5393 626.566 23.6856 631.333 23.6856H655.81C661.75 23.6856 664.398 25.5771 664.398 30.4196C664.398 31.6302 664.284 33.0678 663.944 34.6189L654.448 79.487H639.127Z" fill="white"/>
<path d="M24.8853 51.8125L0.00537109 51.8388L27.1447 9.06742H52.0509L24.8853 51.8125Z" fill="#FFC143"/>
<path d="M88.3331 21.4679H53.1282L61.0099 9.06735H96.2148L88.3331 21.4679Z" fill="white"/>
<path d="M95.7944 48.8436H60.5894L69.9161 34.1311H105.147L95.7944 48.8436Z" fill="white"/>
<path d="M71.9656 73.9075H44.6423L52.524 61.507H79.8473L71.9656 73.9075Z" fill="white"/>
<path d="M25.857 89.4869H0.977173L36.1295 34.1311H61.0094L25.857 89.4869Z" fill="#FFC143"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1,291 @@
"use server"
import {
prisma,
getActiveRepoCountLast30Days,
getOptimizationEventCountByRepoAndUser,
getOptimizationEventCountByUserId,
} from "@codeflash-ai/common"
import { eachDayOfInterval, startOfDay } from "date-fns"
export interface RepositoryWithUsage {
id: string
github_repo_id: string
name: string
full_name: string
is_private: boolean
is_active: boolean
has_github_action: boolean
created_at: Date
last_optimized: Date | null
optimizations_limit: number | null
optimizations_used: number
organization: string
membersCount?: number // Count from repository_members
avatarUrl?: string
}
export async function getAllRepositories(
userId: string,
username: string,
): Promise<RepositoryWithUsage[]> {
try {
const repositories = await prisma.repositories.findMany({
where: {
OR: [
{
repository_members: {
some: {
user_id: userId,
},
},
},
{
optimization_events: {
some: {
current_username: username,
},
},
},
],
},
})
console.log(repositories.length)
return repositories.map((repo): RepositoryWithUsage => {
const organization = repo.full_name.split("/")[0]
return {
id: repo.id,
github_repo_id: repo.github_repo_id,
name: repo.name,
full_name: repo.full_name,
is_private: repo.is_private,
is_active: repo.is_active,
has_github_action: repo.has_github_action,
created_at: repo.created_at,
last_optimized: repo.last_optimized,
optimizations_limit: repo.optimizations_limit,
optimizations_used: repo.optimizations_used,
organization,
avatarUrl: `https://github.com/${organization}.png`,
}
})
} catch (error) {
console.error("Failed to fetch repositories:", error)
return []
}
}
// New wrapper functions that call the imported functions
export async function getUserOptimizationCount(userId: string, username: string) {
return getOptimizationEventCountByUserId(userId, username)
}
export async function getUserOptimizationSuccessfulCount(userId: string, username: string) {
return prisma.optimization_events.count({
where: {
is_optimization_found: true,
OR: [{ user_id: userId }, { current_username: username }],
},
})
}
export async function getRepoUserOptimizationCount(
repoId: string,
userId: string,
username: string,
) {
return getOptimizationEventCountByRepoAndUser(repoId, userId, username)
}
export async function getActiveRepositoriesLast30Days(userId: string, username: string) {
return getActiveRepoCountLast30Days(userId, username)
}
export async function getOptimizationsTimeSeriesData(
userId: string,
username: string,
onlySuccessful?: boolean,
) {
try {
const data = await prisma.optimization_events.findMany({
where: {
OR: [{ user_id: userId }, { current_username: username }],
...(onlySuccessful === true ? { is_optimization_found: true } : {}),
},
select: {
created_at: true,
},
})
const groupedByDay: Record<string, number> = {}
data.forEach(item => {
// Use the user's local time zone to format the date as YYYY-MM-DD
const day = item.created_at
.toLocaleDateString(undefined, {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
})
.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$1-$2") // Convert MM/DD/YYYY to YYYY-MM-DD
groupedByDay[day] = (groupedByDay[day] || 0) + 1
})
const allDates = eachDayOfInterval({
start: new Date(Object.keys(groupedByDay).sort()[0]),
end: startOfDay(new Date()),
}).map(d =>
d
.toLocaleDateString(undefined, {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
})
.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$1-$2"),
)
let cumulativeCount = 0
const completeData = allDates.map(date => {
cumulativeCount += groupedByDay[date] || 0
return { date, count: cumulativeCount }
})
return completeData
} catch (error) {
console.error("Failed to fetch optimization time series data:", error)
return []
}
}
export async function getPullRequestEventTimeSeriesData(
userId: string,
username: string,
year: number,
) {
try {
const eventTypes = ["pr_created", "pr_merged", "pr_closed"]
const data = await prisma.optimization_events.findMany({
where: {
OR: [{ user_id: userId }, { current_username: username }],
event_type: { in: eventTypes },
created_at: {
gte: new Date(`${year}-01-01T00:00:00.000Z`),
lt: new Date(`${year + 1}-01-01T00:00:00.000Z`),
},
},
select: {
event_type: true,
created_at: true,
},
})
const groupedByMonth: Record<string, Record<string, number>> = {}
// Initialize the months of the year
for (let month = 1; month <= 12; month++) {
const monthKey = `${year}-${month.toString().padStart(2, "0")}`
groupedByMonth[monthKey] = { pr_created: 0, pr_merged: 0, pr_closed: 0 }
}
data.forEach(item => {
const month = item.created_at.getMonth() + 1 // JavaScript months are 0-indexed
const monthKey = `${year}-${month.toString().padStart(2, "0")}`
if (groupedByMonth[monthKey]) {
groupedByMonth[monthKey][item.event_type] += 1
}
})
const completeData = Object.keys(groupedByMonth).map(monthKey => ({
month: monthKey,
pr_created: groupedByMonth[monthKey].pr_created,
pr_merged: groupedByMonth[monthKey].pr_merged,
pr_closed: groupedByMonth[monthKey].pr_closed,
}))
return completeData
} catch (error) {
console.error("Failed to fetch pull request event time series data:", error)
return []
}
}
export async function getActiveUserLeaderboardLast30Days(
userId: string,
username: string,
): Promise<{ username: string; eventCount: number; avatarUrl: string }[]> {
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
// Get the repository IDs the user has access to or has optimization events in
const memberRepos = await prisma.repositories.findMany({
where: {
OR: [
{
repository_members: {
some: {
user_id: userId,
},
},
},
{
is_private: false,
optimization_events: {
some: {
current_username: username,
},
},
},
],
},
select: {
id: true,
},
})
const repoIds = memberRepos.map(r => r.id)
if (repoIds.length === 0) return []
const groupedCounts = await prisma.optimization_events.groupBy({
by: ["repository_id", "current_username"],
where: {
AND: [
{
OR: [
{ user_id: userId },
{
repository_id: {
in: repoIds,
},
},
],
},
{
created_at: {
gte: since,
},
},
{
current_username: {
not: null,
},
},
],
},
_count: {
id: true,
},
orderBy: {
_count: {
id: "desc",
},
},
})
return groupedCounts.map(entry => ({
username: entry.current_username!,
eventCount: entry._count.id,
avatarUrl: `https://github.com/${entry.current_username}.png`,
}))
}

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,70 @@
"use client"
import { Sidebar } from "@/components/dashboard/sidebar"
import { type JSX } from "react"
import { type JSX, useState, useEffect } from "react"
import Image from "next/image"
import { useUser } from "@auth0/nextjs-auth0/client"
export default function AppRootLayout({ children }: { children: React.ReactNode }): JSX.Element {
const { user, error, isLoading } = useUser()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [theme, setTheme] = useState<"light" | "dark">("light")
// Initialize theme from localStorage or system preference
useEffect(() => {
const savedTheme = localStorage.getItem("theme") as "light" | "dark" | null
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
if (savedTheme) {
setTheme(savedTheme)
document.documentElement.classList.toggle("dark", savedTheme === "dark")
} else if (prefersDark) {
setTheme("dark")
document.documentElement.classList.add("dark")
}
}, [])
return (
<>
<div className="flex ">
<div className="flex bg-background text-foreground">
<div className="max-w-xs">
<Sidebar className="h-screen border-r fixed top-0 left-0 w-60" />
{/* <text>CODEFLASH!</text> */}
<Sidebar className="h-screen border-r border-border/30 fixed top-0 left-0 w-60" />
</div>
{/* Page Content */}
<div className="px-6 py-6 relative left-60 w-[calc(100%-240px)]">{children}</div>
{/* Avatar Profile */}
<div className="fixed top-4 right-4 z-50 group">
<div className="flex items-center gap-2 bg-muted hover:bg-muted rounded-md px-3 py-2 cursor-pointer transition-colors shadow-sm">
{isLoading ? (
<div className="w-8 h-8 rounded-full bg-muted-foreground/20 flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 w-full h-full animate-pulse opacity-70"></div>
<div className="h-1.5 w-1.5 bg-muted-foreground/50 rounded-full animate-bounce"></div>
</div>
) : error ? (
<div className="w-8 h-8 rounded-full bg-destructive/20 flex items-center justify-center">
<span className="text-destructive text-xs">Error</span>
</div>
) : user ? (
<>
<div className="relative transition-transform group-hover:scale-105 duration-300">
<Image
src={user.picture || "/default-avatar.png"}
alt="Avatar"
width={32}
height={32}
className="rounded-full ring-2 ring-primary/20 hover:ring-primary/40 transition-all"
/>
</div>
<span className="text-sm font-medium">
{user.nickname || user.email?.split("@")[0] || "User"}
</span>
</>
) : (
<></>
)}
</div>
</div>
<div className="px-4 py-4 relative left-60 w-[calc(100%-240px)]">{children}</div>
</div>
</>
)

View file

@ -0,0 +1,527 @@
"use client"
import React, { useState, useMemo, useEffect } from "react"
import {
Clock,
GitPullRequest,
Search,
ChevronDown,
X,
RefreshCw,
Filter,
ArrowUpDown,
BookOpen,
} from "lucide-react"
import Image from "next/image"
import { Card } from "@/components/ui/card"
import { getUserIdAndUsername } from "@/app/utils/auth"
import { RepositoryWithUsage, getAllRepositories } from "../home/action"
// Custom hook for detecting clicks outside of an element
const useOutsideClick = (callback: () => void) => {
const ref = React.useRef<HTMLDivElement>(null)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback()
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [callback])
return ref
}
// Enhanced search component
const SearchBar = ({
searchQuery,
setSearchQuery,
}: {
searchQuery: string
setSearchQuery: (value: string) => void
}) => {
return (
<div className="relative flex-1 group">
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Search
size={18}
className="text-muted-foreground/70 group-focus-within:text-primary transition-colors"
/>
</div>
<input
type="text"
placeholder="Search repositories..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="block w-full rounded-xl border border-border bg-background/60 p-3 pl-10 text-foreground focus:border-primary focus:ring-1 focus:ring-primary transition-all duration-200"
/>
{searchQuery && (
<button
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground"
onClick={() => setSearchQuery("")}
>
<X size={18} className="opacity-70 hover:opacity-100" />
</button>
)}
</div>
</div>
)
}
// GitHub-style Repository Card Component
const RepositoryCard = ({ repo }: { repo: RepositoryWithUsage }) => (
<Card
key={repo.id}
className="bg-card bg-muted/5 rounded-xl border border-border hover:border-primary/30 hover:shadow-md hover:shadow-primary/5 transition-all duration-300 overflow-hidden group"
>
<div className="p-5">
<div className="flex items-start">
{/* Circular avatar for organization */}
<div className="mr-3 flex-shrink-0">
{repo.avatarUrl ? (
<div className="w-9 h-9 sm:w-11 sm:h-11 rounded-full overflow-hidden border-2 border-border/50 group-hover:border-primary/20 transition-colors">
<Image
src={repo.avatarUrl}
alt={`${repo.organization} avatar`}
width={44}
height={44}
className="object-cover w-full h-full"
/>
</div>
) : (
<div className="w-9 h-9 sm:w-11 sm:h-11 rounded-full bg-gradient-to-br from-primary/10 to-primary/30 flex items-center justify-center border-2 border-border group-hover:from-primary/20 group-hover:to-primary/40 transition-colors">
<span className="text-primary font-semibold">
{repo.name.substring(0, 1).toUpperCase()}
</span>
</div>
)}
</div>
<div className="flex-1 min-w-0">
{/* Repository name with visibility badge */}
<div className="flex items-center flex-wrap gap-1">
<h3 className="text-sm sm:text-base font-semibold text-primary hover:underline truncate">
{repo.name}
</h3>
<span
className={`ml-1 px-1.5 sm:px-2 py-0.5 text-xs font-medium rounded-full ${repo.is_private ? "bg-amber-100 text-amber-700" : "bg-emerald-100 text-emerald-700"}`}
>
{repo.is_private ? "Private" : "Public"}
</span>
</div>
{/* Organization/full name */}
<p className="text-xs sm:text-sm text-muted-foreground mb-2">{repo.full_name}</p>
{/* Repository stats - matching schema data */}
<div className="flex items-center flex-wrap gap-1.5 sm:gap-2">
{/* Active status */}
<span
className={`inline-flex items-center px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-full text-xs ${repo.is_active ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600"}`}
>
<span
className={`inline-block w-1.5 sm:w-2 h-1.5 sm:h-2 rounded-full ${repo.is_active ? "bg-green-500" : "bg-gray-400"} mr-1 sm:mr-1.5`}
></span>
<span>{repo.is_active ? "Active" : "Inactive"}</span>
</span>
{/* GitHub Action */}
{repo.has_github_action && (
<span className="inline-flex items-center px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-full bg-blue-50 text-xs text-blue-700">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
width="10"
height="10"
className="mr-1 fill-current sm:w-3 sm:h-3"
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
</svg>
Action
</span>
)}
{/* Members count if available */}
{repo.membersCount !== undefined && repo.membersCount > 0 && (
<span className="inline-flex items-center px-1.5 sm:px-2 py-0.5 sm:py-1 rounded-full bg-indigo-50 text-xs text-indigo-700">
<svg
xmlns="http://www.w3.org/2000/svg"
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-1 sm:w-3 sm:h-3"
>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
{repo.membersCount}
</span>
)}
</div>
</div>
</div>
{/* Last optimized date */}
{repo.last_optimized && (
<div className="mt-3 sm:mt-4 text-xs text-muted-foreground flex items-center">
<Clock size={10} className="mr-1 sm:w-3 sm:h-3" />
Last optimized: {new Date(repo.last_optimized).toLocaleDateString()}
</div>
)}
</div>
</Card>
)
// Loading State Component
const RepositoriesLoading = () => (
<div className="flex flex-col items-center justify-center h-[70vh]">
<div className="animate-spin rounded-full h-10 w-10 sm:h-12 sm:w-12 border-t-2 border-b-2 border-primary mb-4"></div>
<p className="text-muted-foreground animate-pulse">Loading repositories...</p>
</div>
)
// Page Header Component
const PageHeader = ({ totalCount }: { totalCount: number }) => (
<div className="mb-6 sm:mb-8">
<div className="flex items-center gap-3 mb-2">
<h1 className="text-xl sm:text-2xl font-bold">Repositories</h1>
<div className="px-2 py-0.5 sm:px-2.5 sm:py-1 bg-primary/10 text-primary rounded-full text-xs sm:text-sm font-medium">
{totalCount} total
</div>
</div>
</div>
)
// Main component for repository list with filters
const RepositoryList = ({ repositories }: { repositories: RepositoryWithUsage[] }) => {
const [searchQuery, setSearchQuery] = useState("")
const [filter, setFilter] = useState<"all" | "active" | "public" | "private">("all")
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false)
const [sortBy, setSortBy] = useState<"name">("name")
const [isSortDropdownOpen, setIsSortDropdownOpen] = useState(false)
const filterDropdownRef = useOutsideClick(() => setIsFilterDropdownOpen(false))
const sortDropdownRef = useOutsideClick(() => setIsSortDropdownOpen(false))
const getSortLabel = (sortType: string) => {
switch (sortType) {
case "name":
return "Name"
default:
return "Last Optimized"
}
}
const filteredRepositories = useMemo(() => {
let repos = repositories.filter(repo => {
// Search in name and full_name
const matchesSearch =
searchQuery === "" ||
repo.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
repo.full_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
repo.organization.toLowerCase().includes(searchQuery.toLowerCase())
if (!matchesSearch) return false
switch (filter) {
case "active":
return repo.is_active
case "public":
return !repo.is_private
case "private":
return repo.is_private
default:
return true
}
})
// Sort repositories
switch (sortBy) {
case "name":
repos = repos.sort((a, b) => a.name.localeCompare(b.name))
break
}
return repos
}, [repositories, searchQuery, filter, sortBy])
return (
<>
<div className="flex flex-col md:flex-row gap-3 sm:gap-4 mb-5 sm:mb-6">
<SearchBar searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
<div className="flex gap-2">
<div className="relative" ref={filterDropdownRef}>
<button
onClick={() => setIsFilterDropdownOpen(!isFilterDropdownOpen)}
className="flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-2.5 sm:py-3 text-xs sm:text-sm bg-background border border-border rounded-xl hover:border-primary/50 transition-colors focus:outline-none focus:ring-1 focus:ring-primary"
>
<Filter size={14} className="text-muted-foreground sm:w-4 sm:h-4" />
<span>
{filter === "all"
? "All"
: filter.charAt(0).toUpperCase() + filter.slice(1).replace(/-/g, " ")}
</span>
<ChevronDown
size={14}
className={`transition-transform text-muted-foreground sm:w-4 sm:h-4 ${isFilterDropdownOpen ? "rotate-180" : ""}`}
/>
</button>
{isFilterDropdownOpen && (
<div className="absolute z-10 mt-2 w-48 sm:w-52 bg-card rounded-xl shadow-lg overflow-hidden border border-border animate-in fade-in-50 slide-in-from-top-5">
<div className="py-1">
<button
onClick={() => {
setFilter("all")
setIsFilterDropdownOpen(false)
}}
className={`w-full px-3 sm:px-4 py-2 sm:py-2.5 text-left hover:bg-muted flex items-center ${filter === "all" ? "bg-primary/10 text-primary font-medium" : ""}`}
>
<span className="w-4 sm:w-5 h-4 sm:h-5 mr-1.5 sm:mr-2 flex items-center justify-center">
{filter === "all" && (
<span className="w-1.5 h-1.5 rounded-full bg-primary"></span>
)}
</span>
All repositories
</button>
</div>
<div className="border-t border-border py-1">
<button
onClick={() => {
setFilter("active")
setIsFilterDropdownOpen(false)
}}
className={`w-full px-3 sm:px-4 py-2 sm:py-2.5 text-left hover:bg-muted flex items-center ${filter === "active" ? "bg-primary/10 text-primary font-medium" : ""}`}
>
<span className="w-4 sm:w-5 h-4 sm:h-5 mr-1.5 sm:mr-2 flex items-center justify-center">
{filter === "active" && (
<span className="w-1.5 h-1.5 rounded-full bg-primary"></span>
)}
</span>
Active
</button>
</div>
<div className="border-t border-border py-1">
<button
onClick={() => {
setFilter("public")
setIsFilterDropdownOpen(false)
}}
className={`w-full px-3 sm:px-4 py-2 sm:py-2.5 text-left hover:bg-muted flex items-center ${filter === "public" ? "bg-primary/10 text-primary font-medium" : ""}`}
>
<span className="w-4 sm:w-5 h-4 sm:h-5 mr-1.5 sm:mr-2 flex items-center justify-center">
{filter === "public" && (
<span className="w-1.5 h-1.5 rounded-full bg-primary"></span>
)}
</span>
Public
</button>
<button
onClick={() => {
setFilter("private")
setIsFilterDropdownOpen(false)
}}
className={`w-full px-3 sm:px-4 py-2 sm:py-2.5 text-left hover:bg-muted flex items-center ${filter === "private" ? "bg-primary/10 text-primary font-medium" : ""}`}
>
<span className="w-4 sm:w-5 h-4 sm:h-5 mr-1.5 sm:mr-2 flex items-center justify-center">
{filter === "private" && (
<span className="w-1.5 h-1.5 rounded-full bg-primary"></span>
)}
</span>
Private
</button>
</div>
</div>
)}
</div>
<div className="relative" ref={sortDropdownRef}>
<button
onClick={() => setIsSortDropdownOpen(!isSortDropdownOpen)}
className="flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-2.5 sm:py-3 text-xs sm:text-sm bg-background border border-border rounded-xl hover:border-primary/50 transition-colors focus:outline-none focus:ring-1 focus:ring-primary"
>
<ArrowUpDown size={14} className="text-muted-foreground sm:w-4 sm:h-4" />
<span>Sort: {getSortLabel(sortBy)}</span>
<ChevronDown
size={14}
className={`transition-transform text-muted-foreground sm:w-4 sm:h-4 ${isSortDropdownOpen ? "rotate-180" : ""}`}
/>
</button>
{isSortDropdownOpen && (
<div className="absolute right-0 z-10 mt-2 w-48 sm:w-52 bg-card rounded-xl shadow-lg overflow-hidden border border-border animate-in fade-in-50 slide-in-from-top-5">
<div className="py-1">
<button
onClick={() => {
setSortBy("name")
setIsSortDropdownOpen(false)
}}
className={`w-full px-3 sm:px-4 py-2 sm:py-2.5 text-left hover:bg-muted flex items-center ${sortBy === "name" ? "bg-primary/10 text-primary font-medium" : ""}`}
>
<span className="w-4 sm:w-5 h-4 sm:h-5 mr-1.5 sm:mr-2 flex items-center justify-center">
{sortBy === "name" && (
<span className="w-1.5 h-1.5 rounded-full bg-primary"></span>
)}
</span>
Name
</button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Search indicator */}
{searchQuery && (
<div className="flex items-center mb-4 sm:mb-5 ml-1">
<span className="text-xs sm:text-sm text-muted-foreground mr-1 sm:mr-2">
Searching for:
</span>
<div className="bg-primary/10 text-primary px-2 sm:px-3 py-1 sm:py-1.5 rounded-full text-xs sm:text-sm flex items-center gap-1 sm:gap-1.5">
<span>{searchQuery}</span>
<button
onClick={() => setSearchQuery("")}
className="text-primary hover:text-primary/80"
>
<X size={12} className="sm:w-4 sm:h-4" />
</button>
</div>
</div>
)}
{filteredRepositories.length === 0 ? (
<div className="text-center py-16 sm:py-20 bg-card/50 rounded-xl border border-dashed border-border">
<div className="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-muted/40 mb-3 sm:mb-4">
<Search size={24} className="text-muted-foreground sm:w-7 sm:h-7" />
</div>
<h3 className="text-base sm:text-lg font-medium mb-1 sm:mb-2">No repositories found</h3>
<p className="text-xs sm:text-sm text-muted-foreground max-w-md mx-auto">
{
"We couldn't find any repositories matching your search criteria. Try adjusting your filters or search term."
}
</p>
<button
onClick={() => {
setSearchQuery("")
setFilter("all")
}}
className="mt-4 sm:mt-5 px-4 sm:px-5 py-2 sm:py-2.5 bg-primary text-primary-foreground rounded-lg sm:rounded-xl text-xs sm:text-sm hover:bg-primary/90 transition-colors"
>
Clear filters
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-5">
{filteredRepositories.map(repo => (
<RepositoryCard key={repo.id} repo={repo} />
))}
</div>
)}
</>
)
}
// Main page component
export default function RepositoriesPage() {
const [repositories, setRepositories] = useState<RepositoryWithUsage[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchRepositories = async () => {
try {
setLoading(true)
const data = await getUserIdAndUsername()
const repos = await getAllRepositories(data.userId, data.username)
setRepositories(repos)
} catch (err) {
console.error("Failed to fetch repositories:", err)
setError("Failed to load repositories. Please try again later.")
} finally {
setLoading(false)
}
}
fetchRepositories()
}, [])
if (loading) {
return <RepositoriesLoading />
}
if (error) {
return (
<div className="flex justify-center items-center h-[70vh]">
<div className="bg-red-50 text-red-800 p-6 sm:p-8 rounded-xl max-w-md border border-red-200">
<h3 className="text-base sm:text-lg font-medium mb-2 sm:mb-3">
Unable to Load Repositories
</h3>
<p className="mb-3 sm:mb-4 text-sm sm:text-base">{error}</p>
<button
onClick={() => window.location.reload()}
className="flex items-center gap-1 sm:gap-2 w-full justify-center px-3 sm:px-4 py-2 sm:py-2.5 bg-red-100 hover:bg-red-200 text-red-800 rounded-lg text-xs sm:text-sm font-medium transition-colors"
>
<RefreshCw size={14} className="sm:w-4 sm:h-4" /> Try Again
</button>
</div>
</div>
)
}
return (
<div className="flex-1 bg-background min-h-screen">
<div className="py-6 sm:py-8 px-4 sm:px-6 max-w-[1400px] mx-auto">
<PageHeader totalCount={repositories.length} />
<div className="bg-card p-4 sm:p-6 rounded-xl border border-border">
<div className="flex justify-between items-center mb-4 sm:mb-6">
<h2 className="text-base sm:text-lg font-semibold flex items-center">
<BookOpen size={18} className="mr-2 text-primary" />
Repository List
</h2>
<button
onClick={() => window.location.reload()}
className="flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 rounded-lg text-xs sm:text-sm border border-border bg-background hover:bg-muted/50 transition-colors"
>
<RefreshCw size={12} className="text-muted-foreground sm:w-4 sm:h-4" /> Refresh
</button>
</div>
{repositories.length === 0 ? (
<div className="flex justify-center items-center min-h-[300px] sm:min-h-[400px] w-full">
<div className="text-center py-12 sm:py-16 bg-muted/10 rounded-xl border border-dashed border-border max-w-md w-full px-4 sm:px-6">
<div className="inline-flex items-center justify-center w-12 h-12 sm:w-16 sm:h-16 rounded-full bg-muted/20 mb-3 sm:mb-4">
<GitPullRequest size={20} className="text-muted-foreground sm:w-6 sm:h-6" />
</div>
<h3 className="text-base sm:text-lg font-medium mb-1 sm:mb-2">
No repositories found
</h3>
<p className="text-xs sm:text-sm text-muted-foreground mx-auto mb-4 sm:mb-6">
{"We couldn't find any repositories connected to your account."}
</p>
</div>
</div>
) : (
<RepositoryList repositories={repositories} />
)}
</div>
</div>
</div>
)
}

View file

@ -5,3 +5,7 @@ export async function getUserId(): Promise<string | null> {
const session = await getSession()
return session?.user?.sub || null
}
export async function getUserIdAndUsername(): Promise<{ username: string; userId: string }> {
const session = await getSession()
return { userId: session?.user?.sub, username: session?.user?.nickname }
}

View file

@ -1,12 +1,54 @@
import { type JSX } from "react"
import Image from "next/image"
import { useEffect, useState } from "react"
export default function LogoBox(): JSX.Element {
const [isDarkMode, setIsDarkMode] = useState<boolean>(false)
const [mounted, setMounted] = useState(false)
// Set up a theme listener that will update in real-time
useEffect(() => {
// Initial check
setMounted(true)
checkTheme()
// Set up the MutationObserver to watch for class changes on the html element
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.attributeName === "class") {
checkTheme()
}
})
})
// Start observing the html element for class changes
observer.observe(document.documentElement, { attributes: true })
// Clean up the observer when component unmounts
return () => observer.disconnect()
}, [])
// Function to check the current theme
const checkTheme = () => {
const isDark = document.documentElement.classList.contains("dark")
setIsDarkMode(isDark)
}
// Avoid hydration mismatch
if (!mounted) {
return (
<div className="block h-11">
<div className="flex justify-start items-center h-full px-4 mt-1">
<div className="h-full w-60" />
</div>
</div>
)
}
return (
<div className="block h-11">
<div className="flex justify-start items-center h-full px-4 mt-1">
<Image
src="/images/codeflash_light.svg"
src={isDarkMode ? "/images/codeflash_darkmode.svg" : "/images/codeflash_light.svg"}
alt="Codeflash Logo"
className="h-full w-60"
width={20}

View file

@ -2,14 +2,51 @@
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import LogoBox from "@/components/dashboard/logo-box"
import { SignOut } from "@/components/ui/SignOut"
import Link from "next/link"
import { usePathname } from "next/navigation"
import React, { type JSX } from "react"
import { BookText, KeyRound, CirclePlay } from "lucide-react"
import React, { useEffect, useState, type JSX } from "react"
import {
FolderGit2,
KeyRound,
CirclePlay,
Home,
CreditCard,
Moon,
Sun,
BookText,
} from "lucide-react"
import { SignOut } from "../ui/SignOut"
export function Sidebar({ className }: { className: string }): JSX.Element {
const currentRoute = usePathname()
const [isDarkMode, setIsDarkMode] = useState(false)
useEffect(() => {
// Check for saved preference or use system preference
const savedTheme = localStorage.getItem("theme")
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
const shouldBeDark = savedTheme === "dark" || (!savedTheme && prefersDark)
setIsDarkMode(shouldBeDark)
if (shouldBeDark) {
document.documentElement.classList.add("dark")
} else {
document.documentElement.classList.remove("dark")
}
}, [])
const toggleTheme = () => {
const newMode = !isDarkMode
setIsDarkMode(newMode)
if (newMode) {
document.documentElement.classList.add("dark")
localStorage.setItem("theme", "dark")
} else {
document.documentElement.classList.remove("dark")
localStorage.setItem("theme", "light")
}
}
return (
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
<div className={cn("flex flex-col h-screen pt-3 pb-6 max-w-xs", className)}>
@ -17,6 +54,24 @@ export function Sidebar({ className }: { className: string }): JSX.Element {
<div className="space-y-4 py-4 grow">
<div className="px-3 py-2">
<div className="space-y-2 grid gap-y-1">
<Link href="/app/home">
<Button
variant={currentRoute === "/app/home" ? "secondary" : "ghost"}
className="w-full justify-start"
>
<Home size={16} className="mr-2 h-4 w-4" />
Home
</Button>
</Link>
<Link href="/app/repositories">
<Button
variant={currentRoute === "/app/repositories" ? "secondary" : "ghost"}
className="w-full justify-start"
>
<FolderGit2 size={16} className="mr-2 h-4 w-4" />
Repositories
</Button>
</Link>
<Link href="/app/gettingstarted">
<Button
variant={currentRoute === "/app/gettingstarted" ? "secondary" : "ghost"}
@ -47,15 +102,24 @@ export function Sidebar({ className }: { className: string }): JSX.Element {
variant={currentRoute === "/app/billing" ? "secondary" : "ghost"}
className="w-full justify-start"
>
<KeyRound size={16} className="mr-2 h-4 w-4" />
<CreditCard size={16} className="mr-2 h-4 w-4" />
Billing
</Button>
</Link>
</div>
</div>
</div>
<div className="align-bottom flex items-center justify-center">
<SignOut className="w-28 items-center mx-auto" />
<div className="align-bottom px-3 space-y-2 border-t border-gray-200 dark:border-gray-800 pt-4">
<Button variant="ghost" className="w-full justify-start" onClick={toggleTheme}>
{isDarkMode ? (
<Moon size={16} className="mr-2 h-4 w-4" />
) : (
<Sun size={16} className="mr-2 h-4 w-4" />
)}
{isDarkMode ? "Dark Mode" : "Light Mode"}
</Button>
<SignOut />
</div>
</div>
)

View file

@ -67,8 +67,8 @@ const ProgressSteps: FC<ProgressStepsProps> = ({
isCompleted
? "bg-amber-400 text-black shadow-md hover:bg-amber-500"
: isCurrent
? "border-2 border-amber-400 bg-white text-gray-700 shadow-md"
: "bg-white text-gray-400 border-2 border-gray-200"
? "border-2 border-amber-400 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 shadow-md"
: "bg-white dark:bg-gray-800 text-gray-400 border-2 border-gray-200 dark:border-gray-700"
}
${isClickable ? "cursor-pointer" : "cursor-not-allowed"}
`}
@ -101,10 +101,10 @@ const ProgressSteps: FC<ProgressStepsProps> = ({
className={`text-sm transition-colors duration-300
${
isCurrent
? "text-gray-900 font-medium"
? "text-gray-900 dark:text-gray-100 font-medium"
: isCompleted
? "text-gray-700"
: "text-gray-400"
? "text-gray-700 dark:text-gray-300"
: "text-gray-400 dark:text-gray-500"
}`}
>
{step.title}
@ -115,7 +115,7 @@ const ProgressSteps: FC<ProgressStepsProps> = ({
{/* Line after step (if not the last step) */}
{index < steps.length - 1 && (
<div className="w-24 md:w-40 h-[2px] mx-2 transition-all duration-300">
<div className="bg-gray-200 h-full w-full relative overflow-hidden">
<div className="bg-gray-200 dark:bg-gray-700 h-full w-full relative overflow-hidden">
<div
className="absolute top-0 left-0 h-full bg-amber-400 transition-all duration-500"
style={{

View file

@ -1,18 +1,20 @@
"use client"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { LogOut } from "lucide-react"
import Link from "next/link"
import * as React from "react"
import { type JSX } from "react"
export function SignOut({
className,
...props
}: { className: string } & React.ComponentPropsWithRef<typeof Button>): JSX.Element {
export function SignOut(): JSX.Element {
return (
<a href="/api/auth/logout">
<Button className={cn("", className)} {...props}>
Sign Out
<Link href="/api/auth/logout">
<Button
variant="ghost"
className="w-full justify-start text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
>
<LogOut size={16} className="mr-2 h-4 w-4" />
Log out
</Button>
</a>
</Link>
)
}

View file

@ -1,7 +1,6 @@
node_modules
dist
*.tgz
*.sql
/.npmrc
.npmrc

View file

@ -1,6 +1,6 @@
{
"name": "@codeflash-ai/common",
"version": "1.0.13",
"version": "1.0.14",
"lockfileVersion": 3,
"requires": true,
"packages": {

View file

@ -1,6 +1,6 @@
{
"name": "@codeflash-ai/common",
"version": "1.0.13",
"version": "1.0.14",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"repository": {

View file

@ -0,0 +1,111 @@
-- CreateTable
CREATE TABLE "cf_app_installations" (
"id" TEXT NOT NULL,
"installation_id" INTEGER NOT NULL,
"account_id" INTEGER NOT NULL,
"account_login" TEXT NOT NULL,
"account_type" TEXT NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "cf_app_installations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "repositories" (
"id" TEXT NOT NULL,
"github_repo_id" TEXT NOT NULL,
"installation_id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"full_name" TEXT NOT NULL,
"is_private" BOOLEAN NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"has_github_action" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"last_optimized" TIMESTAMP(3),
"optimizations_limit" INTEGER,
"optimizations_used" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "repositories_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "repository_members" (
"id" TEXT NOT NULL,
"repository_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"role" TEXT NOT NULL,
"added_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"added_by" TEXT,
CONSTRAINT "repository_members_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "optimization_events" (
"id" TEXT NOT NULL,
"event_type" TEXT NOT NULL,
"user_id" TEXT,
"repository_id" TEXT,
"trace_id" TEXT NOT NULL,
"pr_id" TEXT,
"api_key_id" INTEGER,
"metadata" JSONB,
"is_optimization_found" BOOLEAN,
"current_username" TEXT,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "optimization_events_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "cf_app_installations_installation_id_key" ON "cf_app_installations"("installation_id");
-- CreateIndex
CREATE UNIQUE INDEX "repositories_github_repo_id_key" ON "repositories"("github_repo_id");
-- CreateIndex
CREATE UNIQUE INDEX "repositories_full_name_key" ON "repositories"("full_name");
-- CreateIndex
CREATE INDEX "repository_members_user_id_idx" ON "repository_members"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "repository_members_repository_id_user_id_key" ON "repository_members"("repository_id", "user_id");
-- CreateIndex
CREATE UNIQUE INDEX "optimization_events_trace_id_key" ON "optimization_events"("trace_id");
-- CreateIndex
CREATE UNIQUE INDEX "optimization_events_pr_id_key" ON "optimization_events"("pr_id");
-- CreateIndex
CREATE INDEX "optimization_events_event_type_created_at_idx" ON "optimization_events"("event_type", "created_at");
-- CreateIndex
CREATE INDEX "optimization_events_repository_id_event_type_idx" ON "optimization_events"("repository_id", "event_type");
-- CreateIndex
CREATE INDEX "optimization_events_user_id_event_type_idx" ON "optimization_events"("user_id", "event_type");
-- CreateIndex
CREATE INDEX "optimization_events_trace_id_idx" ON "optimization_events"("trace_id");
-- AddForeignKey
ALTER TABLE "repositories" ADD CONSTRAINT "repositories_installation_id_fkey" FOREIGN KEY ("installation_id") REFERENCES "cf_app_installations"("installation_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "repository_members" ADD CONSTRAINT "repository_members_repository_id_fkey" FOREIGN KEY ("repository_id") REFERENCES "repositories"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "repository_members" ADD CONSTRAINT "repository_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "optimization_events" ADD CONSTRAINT "optimization_events_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("user_id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "optimization_events" ADD CONSTRAINT "optimization_events_repository_id_fkey" FOREIGN KEY ("repository_id") REFERENCES "repositories"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "optimization_events" ADD CONSTRAINT "optimization_events_api_key_id_fkey" FOREIGN KEY ("api_key_id") REFERENCES "cf_api_keys"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
provider = "postgresql"

View file

@ -17,6 +17,8 @@ model cf_api_keys {
user_id String?
tier String?
user users? @relation(fields: [user_id], references: [user_id])
optimization_events optimization_events[]
}
model users {
@ -28,6 +30,8 @@ model users {
created_at DateTime @default(now()) @db.Timestamptz(6)
api_keys cf_api_keys[]
subscriptions subscriptions?
optimization_events optimization_events[]
repository_members repository_members[]
referral_source String? @db.VarChar(255)
additional_comments String? @db.Text
}
@ -98,3 +102,67 @@ model django_migrations {
name String @db.VarChar(255)
applied DateTime @db.Timestamptz(6)
}
model cf_app_installations {
id String @id @default(uuid())
installation_id Int @unique
account_id Int
account_login String
account_type String // 'User' or 'Organization'
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @updatedAt
is_active Boolean @default(true)
repositories repositories[]
}
model repositories {
id String @id @default(uuid())
github_repo_id String @unique
installation_id Int
name String
full_name String @unique
is_private Boolean
is_active Boolean @default(true)
has_github_action Boolean @default(false)
created_at DateTime @default(now()) @db.Timestamptz(6)
last_optimized DateTime?
installation_info cf_app_installations @relation(fields: [installation_id], references: [installation_id], onDelete: Cascade)
optimizations_limit Int? // Custom limit for this repository
optimizations_used Int @default(0) // Track usage
optimization_events optimization_events[]
repository_members repository_members[]
}
model repository_members {
id String @id @default(uuid())
repository_id String
user_id String
role String // 'owner', 'admin', 'member'
added_at DateTime @default(now()) @db.Timestamptz(6)
added_by String? // The user_id of who added this member
repository repositories @relation(fields: [repository_id], references: [id], onDelete: Cascade)
user users @relation(fields: [user_id], references: [user_id], onDelete: Cascade)
@@unique([repository_id, user_id])
@@index([user_id])
}
model optimization_events {
id String @id @default(uuid())
event_type String // 'pr_created', 'pr_merged', 'pr_closed', 'no-per'
user_id String? // Can be null for system events
repository_id String?
trace_id String @unique
pr_id String? @unique
api_key_id Int?
metadata Json?
is_optimization_found Boolean?
current_username String? //for the current user who did this event, we create this to know the active user in github action
created_at DateTime @default(now()) @db.Timestamptz(6)
// Relations
user users? @relation(fields: [user_id], references: [user_id], onDelete: SetNull)
repository repositories? @relation(fields: [repository_id], references: [id], onDelete: SetNull)
api_key cf_api_keys? @relation(fields: [api_key_id], references: [id], onDelete: SetNull)
// Indexes for querying
@@index([event_type, created_at])
@@index([repository_id, event_type])
@@index([user_id, event_type])
@@index([trace_id])
}

View file

@ -0,0 +1,44 @@
import { PrismaClient, Prisma } from "@prisma/client"
const prisma = new PrismaClient()
// Create a new app installation
export async function createAppInstallation(data: Prisma.cf_app_installationsCreateInput) {
return await prisma.cf_app_installations.create({ data })
}
// Read (get) a single app installation by ID
export async function getAppInstallationById(id: string) {
return await prisma.cf_app_installations.findUnique({
where: { id },
})
}
// Read (get) a single app installation by ID
export async function getAppInstallationByInstalltionId(installation_id: number) {
return await prisma.cf_app_installations.findUnique({
where: { installation_id: installation_id },
})
}
// Read all app installations
export async function getAllAppInstallations() {
return await prisma.cf_app_installations.findMany()
}
// Update an app installation by ID
export async function updateAppInstallation(
id: string,
data: Prisma.cf_app_installationsUpdateInput,
) {
return await prisma.cf_app_installations.update({
where: { id },
data,
})
}
// Delete an app installation by ID
export async function deleteAppInstallation(id: string) {
return await prisma.cf_app_installations.delete({
where: { id },
})
}

View file

@ -17,4 +17,7 @@ export { prisma } from "./prisma-client"
export * from "./subscription-functions"
export * from "./subscription-config"
export * from "./stripe-client"
export * from "./optimization-event"
export * from "./cf-app-installations-functions"
export * from "./repositories-functions"
export * from "./user-referral-functions"

View file

@ -0,0 +1,90 @@
import { prisma } from "./prisma-client"
/**
* Get total optimization event count for a user.
*/
export async function getOptimizationEventCountByUserId(userId: string, username: string) {
return prisma.optimization_events.count({
where: {
OR: [{ user_id: userId }, { current_username: username }],
},
})
}
/**
* Get total optimization event count by repository and user.
*/
export async function getOptimizationEventCountByRepoAndUser(
repoId: string,
userId: string,
username: string,
) {
return prisma.optimization_events.count({
where: {
repository_id: repoId,
OR: [{ user_id: userId }, { current_username: username }],
},
})
}
/**
* Get count of distinct active repositories in the last 30 days.
*/
export async function getActiveRepoCountLast30Days(userId: string, username: string) {
const since = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
const activeRepos = await prisma.optimization_events.findMany({
where: {
created_at: {
gte: since,
},
OR: [
{
repository: {
repository_members: {
some: {
user_id: userId,
},
},
},
},
{
current_username: username,
},
],
},
distinct: ["repository_id"],
select: {
repository_id: true,
},
})
return activeRepos.length
}
/**
* Mark an optimization event as successful or not.
*/
export async function markOptimizationSuccess(trace_id: string, is_optimization_found: boolean) {
return prisma.optimization_events.update({
where: {
trace_id: trace_id,
},
data: {
is_optimization_found: is_optimization_found,
},
})
}
/**
* Update the event type of an optimization event.
*/
export async function updateOptimizationEventType(eventId: string, eventType: string) {
return prisma.optimization_events.update({
where: {
id: eventId,
},
data: {
event_type: eventType,
},
})
}

View file

@ -0,0 +1,118 @@
// Prisma-based repository/member logic
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
// Get all repositories
export async function getAllRepositories() {
return await prisma.repositories.findMany()
}
// Get a single repository by ID
export async function getRepositoryById(repoId: string) {
return await prisma.repositories.findUnique({
where: { id: repoId },
})
}
// Get all repositories for a user (member)
export async function getRepositoriesForMember(userId: string) {
return await prisma.repositories.findMany({
where: {
repository_members: {
some: { user_id: userId },
},
},
})
}
export async function upsertRepository(data: {
github_repo_id: string
installation_id: number
name: string
full_name: string
is_private: boolean
}) {
return await prisma.repositories.upsert({
where: { github_repo_id: data.github_repo_id },
update: {
name: data.name,
full_name: data.full_name,
is_private: data.is_private,
installation_id: data.installation_id,
},
create: {
github_repo_id: data.github_repo_id,
installation_id: data.installation_id,
name: data.name,
full_name: data.full_name,
is_private: data.is_private,
is_active: true,
},
})
}
// Add a new repository
export async function createRepository(data: {
github_repo_id: string
installation_id: number
name: string
full_name: string
is_private: boolean
}) {
return await prisma.repositories.create({
data: {
...data,
},
})
}
// Get all members of a repository
export async function getRepositoryMembers(repoId: string) {
return await prisma.repository_members.findMany({
where: {
repository_id: repoId,
},
include: {
user: true,
},
})
}
// Create a new repository member
export async function createRepositoryMember(data: {
repository_id: string
user_id: string
role: string
added_by?: string
}) {
return await prisma.repository_members.create({
data: {
...data,
},
})
}
// Get a repository member by ID
export async function getRepositoryMemberById(memberId: string) {
return await prisma.repository_members.findUnique({
where: { id: memberId },
include: {
user: true,
},
})
}
// Update a repository member's role
export async function updateRepositoryMemberRole(memberId: string, role: string) {
return await prisma.repository_members.update({
where: { id: memberId },
data: { role },
})
}
// Delete a repository member
export async function deleteRepositoryMember(memberId: string) {
return await prisma.repository_members.delete({
where: { id: memberId },
})
}