mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
parent
c6d08a3ca7
commit
b09b74e02b
36 changed files with 2950 additions and 111 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
8
django/aiservice/authapp/user.py
Normal file
8
django/aiservice/authapp/user.py
Normal 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
|
||||
33
django/aiservice/log_features/log_event.py
Normal file
33
django/aiservice/log_features/log_event.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
25
js/cf-api/endpoints/optimiaztion-success.ts
Normal file
25
js/cf-api/endpoints/optimiaztion-success.ts
Normal 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." })
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
9
js/cf-api/package-lock.json
generated
9
js/cf-api/package-lock.json
generated
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ let nextConfig = {
|
|||
typescript: {
|
||||
ignoreBuildErrors: false,
|
||||
},
|
||||
images: {
|
||||
domains: ["avatars.githubusercontent.com", "github.com"],
|
||||
},
|
||||
}
|
||||
|
||||
// module.exports = nextConfig
|
||||
|
|
|
|||
152
js/cf-webapp/package-lock.json
generated
152
js/cf-webapp/package-lock.json
generated
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
16
js/cf-webapp/public/images/codeflash_darkmode.svg
Normal file
16
js/cf-webapp/public/images/codeflash_darkmode.svg
Normal 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 |
291
js/cf-webapp/src/app/app/home/action.ts
Normal file
291
js/cf-webapp/src/app/app/home/action.ts
Normal 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`,
|
||||
}))
|
||||
}
|
||||
1019
js/cf-webapp/src/app/app/home/page.tsx
Normal file
1019
js/cf-webapp/src/app/app/home/page.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
527
js/cf-webapp/src/app/app/repositories/page.tsx
Normal file
527
js/cf-webapp/src/app/app/repositories/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
1
js/common/.gitignore
vendored
1
js/common/.gitignore
vendored
|
|
@ -1,7 +1,6 @@
|
|||
node_modules
|
||||
dist
|
||||
*.tgz
|
||||
*.sql
|
||||
|
||||
/.npmrc
|
||||
.npmrc
|
||||
|
|
|
|||
2
js/common/package-lock.json
generated
2
js/common/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@codeflash-ai/common",
|
||||
"version": "1.0.13",
|
||||
"version": "1.0.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
|
|
@ -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])
|
||||
}
|
||||
44
js/common/src/cf-app-installations-functions.ts
Normal file
44
js/common/src/cf-app-installations-functions.ts
Normal 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 },
|
||||
})
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
90
js/common/src/optimization-event.ts
Normal file
90
js/common/src/optimization-event.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
||||
118
js/common/src/repositories-functions.ts
Normal file
118
js/common/src/repositories-functions.ts
Normal 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 },
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue