mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
more improvements
This commit is contained in:
parent
7db49f48c5
commit
28da73e292
17 changed files with 210 additions and 78 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { AzureDevOpsConnectionConfig } from "@sourcebot/schemas/v3/azuredevops.type";
|
||||
import { createLogger } from "@sourcebot/logger";
|
||||
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
||||
import { measure, fetchWithRetry } from "./utils.js";
|
||||
import micromatch from "micromatch";
|
||||
import { PrismaClient } from "@sourcebot/db";
|
||||
import { BackendException, BackendError } from "@sourcebot/error";
|
||||
|
|
@ -8,6 +8,7 @@ import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
|||
import * as Sentry from "@sentry/node";
|
||||
import * as azdev from "azure-devops-node-api";
|
||||
import { GitRepository } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
||||
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||
|
||||
const logger = createLogger('azuredevops');
|
||||
const AZUREDEVOPS_CLOUD_HOSTNAME = "dev.azure.com";
|
||||
|
|
@ -34,7 +35,7 @@ export const getAzureDevOpsReposFromConfig = async (
|
|||
const baseUrl = config.url || `https://${AZUREDEVOPS_CLOUD_HOSTNAME}`;
|
||||
|
||||
const token = config.token ?
|
||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
||||
await getTokenFromConfig(config.token, orgId, db) :
|
||||
undefined;
|
||||
|
||||
if (!token) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type"
|
|||
import type { ClientOptions, ClientPathsWithMethod } from "openapi-fetch";
|
||||
import { createLogger } from "@sourcebot/logger";
|
||||
import { PrismaClient } from "@sourcebot/db";
|
||||
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
||||
import { measure, fetchWithRetry } from "./utils.js";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import {
|
||||
SchemaRepository as CloudRepository,
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { SchemaRestRepository as ServerRepository } from "@coderabbitai/bitbucket/server/openapi";
|
||||
import { processPromiseResults } from "./connectionUtils.js";
|
||||
import { throwIfAnyFailed } from "./connectionUtils.js";
|
||||
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||
|
||||
const logger = createLogger('bitbucket');
|
||||
const BITBUCKET_CLOUD_GIT = 'https://bitbucket.org';
|
||||
|
|
@ -59,7 +60,7 @@ type ServerPaginatedResponse<T> = {
|
|||
|
||||
export const getBitbucketReposFromConfig = async (config: BitbucketConnectionConfig, orgId: number, db: PrismaClient) => {
|
||||
const token = config.token ?
|
||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
||||
await getTokenFromConfig(config.token, orgId, db) :
|
||||
undefined;
|
||||
|
||||
if (config.deploymentType === 'server' && !config.url) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { GithubAppConfig, SourcebotConfig } from "@sourcebot/schemas/v3/index.type";
|
||||
import { loadConfig } from "@sourcebot/shared";
|
||||
import { env } from "../env.js";
|
||||
import { createLogger } from "@sourcebot/logger";
|
||||
import { getTokenFromConfig } from "../utils.js";
|
||||
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||
import { PrismaClient } from "@sourcebot/db";
|
||||
import { App } from "@octokit/app";
|
||||
import { GitHubAppConfig } from "@sourcebot/schemas/v3/index.type";
|
||||
|
|
@ -54,7 +53,7 @@ export class GithubAppManager {
|
|||
return;
|
||||
}
|
||||
|
||||
const githubApps = config.apps.filter(app => app.type === 'githubApp') as GithubAppConfig[];
|
||||
const githubApps = config.apps.filter(app => app.type === 'githubApp') as GitHubAppConfig[];
|
||||
logger.info(`Found ${githubApps.length} GitHub apps in config`);
|
||||
|
||||
for (const app of githubApps) {
|
||||
|
|
@ -63,7 +62,7 @@ export class GithubAppManager {
|
|||
// @todo: we should move SINGLE_TENANT_ORG_ID to shared package or just remove the need to pass this in
|
||||
// when resolving tokens
|
||||
const SINGLE_TENANT_ORG_ID = 1;
|
||||
const privateKey = await getTokenFromConfig(app.privateKey, SINGLE_TENANT_ORG_ID, this.db!);
|
||||
const privateKey = await getTokenFromConfig(app.privateKey, SINGLE_TENANT_ORG_ID, this.db);
|
||||
|
||||
const octokitApp = new App({
|
||||
appId: Number(app.id),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Api, giteaApi, HttpResponse, Repository as GiteaRepository } from 'gitea-js';
|
||||
import { GiteaConnectionConfig } from '@sourcebot/schemas/v3/gitea.type';
|
||||
import { getTokenFromConfig, measure } from './utils.js';
|
||||
import { measure } from './utils.js';
|
||||
import fetch from 'cross-fetch';
|
||||
import { createLogger } from '@sourcebot/logger';
|
||||
import micromatch from 'micromatch';
|
||||
|
|
@ -8,6 +8,7 @@ import { PrismaClient } from '@sourcebot/db';
|
|||
import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js';
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { env } from './env.js';
|
||||
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||
|
||||
const logger = createLogger('gitea');
|
||||
const GITEA_CLOUD_HOSTNAME = "gitea.com";
|
||||
|
|
@ -18,7 +19,7 @@ export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, org
|
|||
GITEA_CLOUD_HOSTNAME;
|
||||
|
||||
const token = config.token ?
|
||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
||||
await getTokenFromConfig(config.token, orgId, db) :
|
||||
hostname === GITEA_CLOUD_HOSTNAME ?
|
||||
env.FALLBACK_GITEA_CLOUD_TOKEN :
|
||||
undefined;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import micromatch from "micromatch";
|
|||
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
||||
import { GithubAppManager } from "./ee/githubAppManager.js";
|
||||
import { env } from "./env.js";
|
||||
import { fetchWithRetry, getTokenFromConfig, measure } from "./utils.js";
|
||||
import { fetchWithRetry, measure } from "./utils.js";
|
||||
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||
|
||||
export const GITHUB_CLOUD_HOSTNAME = "github.com";
|
||||
const logger = createLogger('github');
|
||||
|
|
@ -97,7 +98,7 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o
|
|||
GITHUB_CLOUD_HOSTNAME;
|
||||
|
||||
const token = config.token ?
|
||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
||||
await getTokenFromConfig(config.token, orgId, db) :
|
||||
hostname === GITHUB_CLOUD_HOSTNAME ?
|
||||
env.FALLBACK_GITHUB_CLOUD_TOKEN :
|
||||
undefined;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ import { Gitlab, ProjectSchema } from "@gitbeaker/rest";
|
|||
import micromatch from "micromatch";
|
||||
import { createLogger } from "@sourcebot/logger";
|
||||
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"
|
||||
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
||||
import { measure, fetchWithRetry } from "./utils.js";
|
||||
import { PrismaClient } from "@sourcebot/db";
|
||||
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { env } from "./env.js";
|
||||
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||
|
||||
const logger = createLogger('gitlab');
|
||||
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
|
||||
|
|
@ -17,7 +18,7 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
|
|||
GITLAB_CLOUD_HOSTNAME;
|
||||
|
||||
const token = config.token ?
|
||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
||||
await getTokenFromConfig(config.token, orgId, db) :
|
||||
hostname === GITLAB_CLOUD_HOSTNAME ?
|
||||
env.FALLBACK_GITLAB_CLOUD_TOKEN :
|
||||
undefined;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import { Logger } from "winston";
|
|||
import { RepoAuthCredentials, RepoWithConnections } from "./types.js";
|
||||
import path from 'path';
|
||||
import { PrismaClient, Repo } from "@sourcebot/db";
|
||||
import { getTokenFromConfig as getTokenFromConfigBase } from "@sourcebot/crypto";
|
||||
import { BackendException, BackendError } from "@sourcebot/error";
|
||||
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig, AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
|
||||
import { GithubAppManager } from "./ee/githubAppManager.js";
|
||||
|
|
@ -24,22 +23,6 @@ export const marshalBool = (value?: boolean) => {
|
|||
return !!value ? '1' : '0';
|
||||
}
|
||||
|
||||
export const getTokenFromConfig = async (token: any, orgId: number, db: PrismaClient, logger?: Logger) => {
|
||||
try {
|
||||
return await getTokenFromConfigBase(token, orgId, db);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
const e = new BackendException(BackendError.CONNECTION_SYNC_SECRET_DNE, {
|
||||
message: error.message,
|
||||
});
|
||||
Sentry.captureException(e);
|
||||
logger?.error(error.message);
|
||||
throw e;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const resolvePathRelativeToConfig = (localPath: string, configPath: string) => {
|
||||
let absolutePath = localPath;
|
||||
if (!path.isAbsolute(absolutePath)) {
|
||||
|
|
@ -156,7 +139,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
|||
if (connection.connectionType === 'github') {
|
||||
const config = connection.config as unknown as GithubConnectionConfig;
|
||||
if (config.token) {
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db);
|
||||
return {
|
||||
hostUrl: config.url,
|
||||
token,
|
||||
|
|
@ -171,7 +154,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
|||
} else if (connection.connectionType === 'gitlab') {
|
||||
const config = connection.config as unknown as GitlabConnectionConfig;
|
||||
if (config.token) {
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db);
|
||||
return {
|
||||
hostUrl: config.url,
|
||||
token,
|
||||
|
|
@ -187,7 +170,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
|||
} else if (connection.connectionType === 'gitea') {
|
||||
const config = connection.config as unknown as GiteaConnectionConfig;
|
||||
if (config.token) {
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db);
|
||||
return {
|
||||
hostUrl: config.url,
|
||||
token,
|
||||
|
|
@ -202,7 +185,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
|||
} else if (connection.connectionType === 'bitbucket') {
|
||||
const config = connection.config as unknown as BitbucketConnectionConfig;
|
||||
if (config.token) {
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db);
|
||||
const username = config.user ?? 'x-token-auth';
|
||||
return {
|
||||
hostUrl: config.url,
|
||||
|
|
@ -219,7 +202,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
|||
} else if (connection.connectionType === 'azuredevops') {
|
||||
const config = connection.config as unknown as AzureDevOpsConnectionConfig;
|
||||
if (config.token) {
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
||||
const token = await getTokenFromConfig(config.token, connection.orgId, db);
|
||||
|
||||
// For ADO server, multiple auth schemes may be supported. If the ADO deployment supports NTLM, the git clone will default
|
||||
// to this over basic auth. As a result, we cannot embed the token in the clone URL and must force basic auth by passing in the token
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { prisma } from "@/prisma";
|
|||
import { render } from "@react-email/components";
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { encrypt, generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto";
|
||||
import { ApiKey, Org, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType, StripeSubscriptionStatus } from "@sourcebot/db";
|
||||
import { ApiKey, ConnectionSyncJobStatus, Org, OrgRole, Prisma, RepoIndexingJobStatus, RepoIndexingJobType, StripeSubscriptionStatus } from "@sourcebot/db";
|
||||
import { createLogger } from "@sourcebot/logger";
|
||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
||||
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
|
||||
|
|
@ -30,7 +30,7 @@ import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail";
|
|||
import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SINGLE_TENANT_ORG_DOMAIN, SOURCEBOT_GUEST_USER_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants";
|
||||
import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
|
||||
import { ApiKeyPayload, TenancyMode } from "./lib/types";
|
||||
import { withOptionalAuthV2 } from "./withAuthV2";
|
||||
import { withAuthV2, withOptionalAuthV2 } from "./withAuthV2";
|
||||
|
||||
const logger = createLogger('web-actions');
|
||||
const auditService = getAuditService();
|
||||
|
|
@ -593,6 +593,7 @@ export const getReposStats = async () => sew(() =>
|
|||
prisma.repo.count({
|
||||
where: {
|
||||
orgId: org.id,
|
||||
indexedAt: null,
|
||||
jobs: {
|
||||
some: {
|
||||
type: RepoIndexingJobType.INDEX,
|
||||
|
|
@ -604,7 +605,6 @@ export const getReposStats = async () => sew(() =>
|
|||
}
|
||||
},
|
||||
},
|
||||
indexedAt: null,
|
||||
}
|
||||
}),
|
||||
prisma.repo.count({
|
||||
|
|
@ -625,6 +625,42 @@ export const getReposStats = async () => sew(() =>
|
|||
})
|
||||
)
|
||||
|
||||
export const getConnectionStats = async () => sew(() =>
|
||||
withAuthV2(async ({ org, prisma }) => {
|
||||
const [
|
||||
numberOfConnections,
|
||||
numberOfConnectionsWithFirstTimeSyncJobsInProgress,
|
||||
] = await Promise.all([
|
||||
prisma.connection.count({
|
||||
where: {
|
||||
orgId: org.id,
|
||||
}
|
||||
}),
|
||||
prisma.connection.count({
|
||||
where: {
|
||||
orgId: org.id,
|
||||
syncedAt: null,
|
||||
syncJobs: {
|
||||
some: {
|
||||
status: {
|
||||
in: [
|
||||
ConnectionSyncJobStatus.PENDING,
|
||||
ConnectionSyncJobStatus.IN_PROGRESS,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
numberOfConnections,
|
||||
numberOfConnectionsWithFirstTimeSyncJobsInProgress,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export const getRepoInfoByName = async (repoName: string) => sew(() =>
|
||||
withOptionalAuthV2(async ({ org, prisma }) => {
|
||||
// @note: repo names are represented by their remote url
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getRepos, getReposStats } from "@/actions";
|
||||
import { getConnectionStats, getRepos, getReposStats } from "@/actions";
|
||||
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
|
||||
import { auth } from "@/auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -39,6 +39,11 @@ export const NavigationMenu = async ({
|
|||
throw new ServiceErrorException(repoStats);
|
||||
}
|
||||
|
||||
const connectionStats = isAuthenticated ? await getConnectionStats() : null;
|
||||
if (isServiceError(connectionStats)) {
|
||||
throw new ServiceErrorException(connectionStats);
|
||||
}
|
||||
|
||||
const sampleRepos = await getRepos({
|
||||
where: {
|
||||
jobs: {
|
||||
|
|
@ -93,7 +98,12 @@ export const NavigationMenu = async ({
|
|||
<NavigationItems
|
||||
domain={domain}
|
||||
numberOfRepos={numberOfRepos}
|
||||
numberOfReposWithFirstTimeIndexingJobsInProgress={numberOfReposWithFirstTimeIndexingJobsInProgress}
|
||||
isReposButtonNotificationDotVisible={numberOfReposWithFirstTimeIndexingJobsInProgress > 0}
|
||||
isSettingsButtonNotificationDotVisible={
|
||||
connectionStats ?
|
||||
connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0 :
|
||||
false
|
||||
}
|
||||
isAuthenticated={isAuthenticated}
|
||||
/>
|
||||
</NavigationMenuBase>
|
||||
|
|
|
|||
|
|
@ -3,20 +3,23 @@
|
|||
import { NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn, getShortenedNumberDisplayString } from "@/lib/utils";
|
||||
import { SearchIcon, MessageCircleIcon, BookMarkedIcon, SettingsIcon, CircleIcon } from "lucide-react";
|
||||
import { SearchIcon, MessageCircleIcon, BookMarkedIcon, SettingsIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { NotificationDot } from "../notificationDot";
|
||||
|
||||
interface NavigationItemsProps {
|
||||
domain: string;
|
||||
numberOfRepos: number;
|
||||
numberOfReposWithFirstTimeIndexingJobsInProgress: number;
|
||||
isReposButtonNotificationDotVisible: boolean;
|
||||
isSettingsButtonNotificationDotVisible: boolean;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
|
||||
export const NavigationItems = ({
|
||||
domain,
|
||||
numberOfRepos,
|
||||
numberOfReposWithFirstTimeIndexingJobsInProgress,
|
||||
isReposButtonNotificationDotVisible,
|
||||
isSettingsButtonNotificationDotVisible,
|
||||
isAuthenticated,
|
||||
}: NavigationItemsProps) => {
|
||||
const pathname = usePathname();
|
||||
|
|
@ -59,9 +62,7 @@ export const NavigationItems = ({
|
|||
<span className="mr-2">Repositories</span>
|
||||
<Badge variant="secondary" className="px-1.5 relative">
|
||||
{getShortenedNumberDisplayString(numberOfRepos)}
|
||||
{numberOfReposWithFirstTimeIndexingJobsInProgress > 0 && (
|
||||
<CircleIcon className="absolute -right-0.5 -top-0.5 h-2 w-2 text-green-600" fill="currentColor" />
|
||||
)}
|
||||
{isReposButtonNotificationDotVisible && <NotificationDot className="absolute -right-0.5 -top-0.5" />}
|
||||
</Badge>
|
||||
</NavigationMenuLink>
|
||||
{isActive(`/${domain}/repos`) && <ActiveIndicator />}
|
||||
|
|
@ -74,6 +75,7 @@ export const NavigationItems = ({
|
|||
>
|
||||
<SettingsIcon className="w-4 h-4 mr-1" />
|
||||
Settings
|
||||
{isSettingsButtonNotificationDotVisible && <NotificationDot className="absolute -right-0.5 -top-0.5" />}
|
||||
</NavigationMenuLink>
|
||||
{isActive(`/${domain}/settings`) && <ActiveIndicator />}
|
||||
</NavigationMenuItem>
|
||||
|
|
@ -86,4 +88,4 @@ const ActiveIndicator = () => {
|
|||
return (
|
||||
<div className="absolute -bottom-2 left-0 right-0 h-0.5 bg-foreground" />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface NotificationDotProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const NotificationDot = ({ className }: NotificationDotProps) => {
|
||||
return <div className={cn("w-2 h-2 rounded-full bg-green-600", className)} />
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ import { useRouter } from "next/navigation"
|
|||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import { DisplayDate } from "../../components/DisplayDate"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { NotificationDot } from "../../components/notificationDot"
|
||||
|
||||
// @see: https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
|
||||
|
||||
|
|
@ -53,6 +54,7 @@ export type Repo = {
|
|||
imageUrl: string | null
|
||||
indexedCommitHash: string | null
|
||||
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
||||
isFirstTimeIndex: boolean
|
||||
}
|
||||
|
||||
const statusBadgeVariants = cva("", {
|
||||
|
|
@ -111,14 +113,32 @@ export const columns: ColumnDef<Repo>[] = [
|
|||
{repo.displayName?.charAt(0) ?? repo.name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<Link href={getBrowsePath({
|
||||
repoName: repo.name,
|
||||
path: '/',
|
||||
pathType: 'tree',
|
||||
domain: SINGLE_TENANT_ORG_DOMAIN,
|
||||
})} className="font-medium hover:underline">
|
||||
|
||||
{/* Link to the details page (instead of browse) when the repo is indexing
|
||||
as the code will not be available yet */}
|
||||
<Link
|
||||
href={repo.isFirstTimeIndex ? `/${SINGLE_TENANT_ORG_DOMAIN}/repos/${repo.id}` : getBrowsePath({
|
||||
repoName: repo.name,
|
||||
path: '/',
|
||||
pathType: 'tree',
|
||||
domain: SINGLE_TENANT_ORG_DOMAIN,
|
||||
})}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{repo.displayName || repo.name}
|
||||
</Link>
|
||||
{repo.isFirstTimeIndex && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<NotificationDot className="ml-1.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>This is the first time Sourcebot is indexing this repository. It may take a few minutes to complete.</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
@ -150,7 +170,7 @@ export const columns: ColumnDef<Repo>[] = [
|
|||
}
|
||||
|
||||
return (
|
||||
<DisplayDate date={indexedAt} className="ml-3"/>
|
||||
<DisplayDate date={indexedAt} className="ml-3" />
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -177,11 +197,11 @@ export const columns: ColumnDef<Repo>[] = [
|
|||
|
||||
const HashComponent = commitUrl ? (
|
||||
<Link
|
||||
href={commitUrl}
|
||||
className="font-mono text-sm text-link hover:underline"
|
||||
>
|
||||
{smallHash}
|
||||
</Link>
|
||||
href={commitUrl}
|
||||
className="font-mono text-sm text-link hover:underline"
|
||||
>
|
||||
{smallHash}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="font-mono text-sm text-muted-foreground">
|
||||
{smallHash}
|
||||
|
|
@ -337,7 +357,7 @@ export const ReposTable = ({ data }: { data: Repo[] }) => {
|
|||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
<TableHead
|
||||
key={header.id}
|
||||
style={{ width: `${header.getSize()}px` }}
|
||||
>
|
||||
|
|
@ -353,7 +373,7 @@ export const ReposTable = ({ data }: { data: Repo[] }) => {
|
|||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{ width: `${cell.column.getSize()}px` }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,31 @@ import { ServiceErrorException } from "@/lib/serviceError";
|
|||
import { isServiceError } from "@/lib/utils";
|
||||
import { withOptionalAuthV2 } from "@/withAuthV2";
|
||||
import { ReposTable } from "./components/reposTable";
|
||||
import { RepoIndexingJobStatus } from "@sourcebot/db";
|
||||
|
||||
export default async function ReposPage() {
|
||||
|
||||
const repos = await getReposWithLatestJob();
|
||||
if (isServiceError(repos)) {
|
||||
throw new ServiceErrorException(repos);
|
||||
const _repos = await getReposWithLatestJob();
|
||||
if (isServiceError(_repos)) {
|
||||
throw new ServiceErrorException(_repos);
|
||||
}
|
||||
|
||||
const repos = _repos
|
||||
.map((repo) => ({
|
||||
...repo,
|
||||
latestJobStatus: repo.jobs.length > 0 ? repo.jobs[0].status : null,
|
||||
isFirstTimeIndex: repo.indexedAt === null && repo.jobs.filter((job) => job.status === RepoIndexingJobStatus.PENDING || job.status === RepoIndexingJobStatus.IN_PROGRESS).length > 0,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.isFirstTimeIndex && !b.isFirstTimeIndex) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.isFirstTimeIndex && b.isFirstTimeIndex) {
|
||||
return 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
|
|
@ -27,7 +44,8 @@ export default async function ReposPage() {
|
|||
createdAt: repo.createdAt,
|
||||
webUrl: repo.webUrl,
|
||||
imageUrl: repo.imageUrl,
|
||||
latestJobStatus: repo.jobs.length > 0 ? repo.jobs[0].status : null,
|
||||
latestJobStatus: repo.latestJobStatus,
|
||||
isFirstTimeIndex: repo.isFirstTimeIndex,
|
||||
codeHostType: repo.external_codeHostType,
|
||||
indexedCommitHash: repo.indexedCommitHash,
|
||||
}))} />
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { NotificationDot } from "@/app/[domain]/components/notificationDot"
|
||||
import { cn } from "@/lib/utils"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import React from "react"
|
||||
|
||||
export type SidebarNavItem = {
|
||||
href: string
|
||||
hrefRegex?: string
|
||||
title: React.ReactNode
|
||||
isNotificationDotVisible?: boolean
|
||||
}
|
||||
|
||||
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
|
|
@ -43,6 +45,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
|||
)}
|
||||
>
|
||||
{item.title}
|
||||
{item.isNotificationDotVisible && <NotificationDot className="ml-1.5" />}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
"use client"
|
||||
|
||||
import { DisplayDate } from "@/app/[domain]/components/DisplayDate"
|
||||
import { NotificationDot } from "@/app/[domain]/components/notificationDot"
|
||||
import { useToast } from "@/components/hooks/use-toast"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
|
||||
import { CodeHostType, getCodeHostIcon } from "@/lib/utils"
|
||||
import {
|
||||
|
|
@ -35,6 +37,7 @@ export type Connection = {
|
|||
syncedAt: Date | null
|
||||
codeHostType: CodeHostType
|
||||
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
||||
isFirstTimeSync: boolean
|
||||
}
|
||||
|
||||
const statusBadgeVariants = cva("", {
|
||||
|
|
@ -91,6 +94,18 @@ export const columns: ColumnDef<Connection>[] = [
|
|||
<Link href={`/${SINGLE_TENANT_ORG_DOMAIN}/settings/connections/${connection.id}`} className="font-medium hover:underline">
|
||||
{connection.name}
|
||||
</Link>
|
||||
{connection.isFirstTimeSync && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<NotificationDot className="ml-1.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>This is the first time Sourcebot is syncing this connection. It may take a few minutes to complete.</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,15 +4,33 @@ import { CodeHostType, isServiceError } from "@/lib/utils";
|
|||
import { withAuthV2 } from "@/withAuthV2";
|
||||
import Link from "next/link";
|
||||
import { ConnectionsTable } from "./components/connectionsTable";
|
||||
import { ConnectionSyncJobStatus } from "@prisma/client";
|
||||
|
||||
const DOCS_URL = "https://docs.sourcebot.dev/docs/connections/overview";
|
||||
|
||||
export default async function ConnectionsPage() {
|
||||
const connections = await getConnectionsWithLatestJob();
|
||||
if (isServiceError(connections)) {
|
||||
throw new ServiceErrorException(connections);
|
||||
const _connections = await getConnectionsWithLatestJob();
|
||||
if (isServiceError(_connections)) {
|
||||
throw new ServiceErrorException(_connections);
|
||||
}
|
||||
|
||||
// Sort connections so that first time syncs are at the top.
|
||||
const connections = _connections
|
||||
.map((connection) => ({
|
||||
...connection,
|
||||
isFirstTimeSync: connection.syncedAt === null && connection.syncJobs.filter((job) => job.status === ConnectionSyncJobStatus.PENDING || job.status === ConnectionSyncJobStatus.IN_PROGRESS).length > 0,
|
||||
latestJobStatus: connection.syncJobs.length > 0 ? connection.syncJobs[0].status : null,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.isFirstTimeSync && !b.isFirstTimeSync) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.isFirstTimeSync && b.isFirstTimeSync) {
|
||||
return 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
|
|
@ -24,7 +42,8 @@ export default async function ConnectionsPage() {
|
|||
name: connection.name,
|
||||
codeHostType: connection.connectionType as CodeHostType,
|
||||
syncedAt: connection.syncedAt,
|
||||
latestJobStatus: connection.syncJobs.length > 0 ? connection.syncJobs[0].status : null,
|
||||
latestJobStatus: connection.latestJobStatus,
|
||||
isFirstTimeSync: connection.isFirstTimeSync,
|
||||
}))} />
|
||||
</div>
|
||||
)
|
||||
|
|
@ -34,16 +53,22 @@ const getConnectionsWithLatestJob = async () => sew(() =>
|
|||
withAuthV2(async ({ prisma }) => {
|
||||
const connections = await prisma.connection.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
syncJobs: true,
|
||||
}
|
||||
},
|
||||
syncJobs: {
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
take: 1
|
||||
}
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return connections;
|
||||
}));
|
||||
|
|
@ -6,7 +6,7 @@ import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
|
|||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/auth";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { getMe, getOrgAccountRequests } from "@/actions";
|
||||
import { getConnectionStats, getMe, getOrgAccountRequests } from "@/actions";
|
||||
import { ServiceErrorException } from "@/lib/serviceError";
|
||||
import { getOrgFromDomain } from "@/data/org";
|
||||
import { OrgRole } from "@prisma/client";
|
||||
|
|
@ -63,6 +63,11 @@ export default async function SettingsLayout(
|
|||
numJoinRequests = requests.length;
|
||||
}
|
||||
|
||||
const connectionStats = await getConnectionStats();
|
||||
if (isServiceError(connectionStats)) {
|
||||
throw new ServiceErrorException(connectionStats);
|
||||
}
|
||||
|
||||
const sidebarNavItems: SidebarNavItem[] = [
|
||||
{
|
||||
title: "General",
|
||||
|
|
@ -98,6 +103,7 @@ export default async function SettingsLayout(
|
|||
title: "Connections",
|
||||
href: `/${domain}/settings/connections`,
|
||||
hrefRegex: `/${domain}/settings/connections(\/[^/]+)?$`,
|
||||
isNotificationDotVisible: connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0,
|
||||
}
|
||||
] : []),
|
||||
{
|
||||
|
|
@ -140,4 +146,5 @@ export default async function SettingsLayout(
|
|||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue