mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +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 { AzureDevOpsConnectionConfig } from "@sourcebot/schemas/v3/azuredevops.type";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
import { getTokenFromConfig, measure, fetchWithRetry } from "./utils.js";
|
import { measure, fetchWithRetry } from "./utils.js";
|
||||||
import micromatch from "micromatch";
|
import micromatch from "micromatch";
|
||||||
import { PrismaClient } from "@sourcebot/db";
|
import { PrismaClient } from "@sourcebot/db";
|
||||||
import { BackendException, BackendError } from "@sourcebot/error";
|
import { BackendException, BackendError } from "@sourcebot/error";
|
||||||
|
|
@ -8,6 +8,7 @@ import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import * as azdev from "azure-devops-node-api";
|
import * as azdev from "azure-devops-node-api";
|
||||||
import { GitRepository } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
import { GitRepository } from "azure-devops-node-api/interfaces/GitInterfaces.js";
|
||||||
|
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||||
|
|
||||||
const logger = createLogger('azuredevops');
|
const logger = createLogger('azuredevops');
|
||||||
const AZUREDEVOPS_CLOUD_HOSTNAME = "dev.azure.com";
|
const AZUREDEVOPS_CLOUD_HOSTNAME = "dev.azure.com";
|
||||||
|
|
@ -34,7 +35,7 @@ export const getAzureDevOpsReposFromConfig = async (
|
||||||
const baseUrl = config.url || `https://${AZUREDEVOPS_CLOUD_HOSTNAME}`;
|
const baseUrl = config.url || `https://${AZUREDEVOPS_CLOUD_HOSTNAME}`;
|
||||||
|
|
||||||
const token = config.token ?
|
const token = config.token ?
|
||||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
await getTokenFromConfig(config.token, orgId, db) :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type"
|
||||||
import type { ClientOptions, ClientPathsWithMethod } from "openapi-fetch";
|
import type { ClientOptions, ClientPathsWithMethod } from "openapi-fetch";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
import { PrismaClient } from "@sourcebot/db";
|
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 * as Sentry from "@sentry/node";
|
||||||
import {
|
import {
|
||||||
SchemaRepository as CloudRepository,
|
SchemaRepository as CloudRepository,
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import { SchemaRestRepository as ServerRepository } from "@coderabbitai/bitbucket/server/openapi";
|
import { SchemaRestRepository as ServerRepository } from "@coderabbitai/bitbucket/server/openapi";
|
||||||
import { processPromiseResults } from "./connectionUtils.js";
|
import { processPromiseResults } from "./connectionUtils.js";
|
||||||
import { throwIfAnyFailed } from "./connectionUtils.js";
|
import { throwIfAnyFailed } from "./connectionUtils.js";
|
||||||
|
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||||
|
|
||||||
const logger = createLogger('bitbucket');
|
const logger = createLogger('bitbucket');
|
||||||
const BITBUCKET_CLOUD_GIT = 'https://bitbucket.org';
|
const BITBUCKET_CLOUD_GIT = 'https://bitbucket.org';
|
||||||
|
|
@ -59,7 +60,7 @@ type ServerPaginatedResponse<T> = {
|
||||||
|
|
||||||
export const getBitbucketReposFromConfig = async (config: BitbucketConnectionConfig, orgId: number, db: PrismaClient) => {
|
export const getBitbucketReposFromConfig = async (config: BitbucketConnectionConfig, orgId: number, db: PrismaClient) => {
|
||||||
const token = config.token ?
|
const token = config.token ?
|
||||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
await getTokenFromConfig(config.token, orgId, db) :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
if (config.deploymentType === 'server' && !config.url) {
|
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 { loadConfig } from "@sourcebot/shared";
|
||||||
import { env } from "../env.js";
|
import { env } from "../env.js";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
import { getTokenFromConfig } from "../utils.js";
|
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||||
import { PrismaClient } from "@sourcebot/db";
|
import { PrismaClient } from "@sourcebot/db";
|
||||||
import { App } from "@octokit/app";
|
import { App } from "@octokit/app";
|
||||||
import { GitHubAppConfig } from "@sourcebot/schemas/v3/index.type";
|
import { GitHubAppConfig } from "@sourcebot/schemas/v3/index.type";
|
||||||
|
|
@ -54,7 +53,7 @@ export class GithubAppManager {
|
||||||
return;
|
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`);
|
logger.info(`Found ${githubApps.length} GitHub apps in config`);
|
||||||
|
|
||||||
for (const app of githubApps) {
|
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
|
// @todo: we should move SINGLE_TENANT_ORG_ID to shared package or just remove the need to pass this in
|
||||||
// when resolving tokens
|
// when resolving tokens
|
||||||
const SINGLE_TENANT_ORG_ID = 1;
|
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({
|
const octokitApp = new App({
|
||||||
appId: Number(app.id),
|
appId: Number(app.id),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Api, giteaApi, HttpResponse, Repository as GiteaRepository } from 'gitea-js';
|
import { Api, giteaApi, HttpResponse, Repository as GiteaRepository } from 'gitea-js';
|
||||||
import { GiteaConnectionConfig } from '@sourcebot/schemas/v3/gitea.type';
|
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 fetch from 'cross-fetch';
|
||||||
import { createLogger } from '@sourcebot/logger';
|
import { createLogger } from '@sourcebot/logger';
|
||||||
import micromatch from 'micromatch';
|
import micromatch from 'micromatch';
|
||||||
|
|
@ -8,6 +8,7 @@ import { PrismaClient } from '@sourcebot/db';
|
||||||
import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js';
|
import { processPromiseResults, throwIfAnyFailed } from './connectionUtils.js';
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import { env } from './env.js';
|
import { env } from './env.js';
|
||||||
|
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||||
|
|
||||||
const logger = createLogger('gitea');
|
const logger = createLogger('gitea');
|
||||||
const GITEA_CLOUD_HOSTNAME = "gitea.com";
|
const GITEA_CLOUD_HOSTNAME = "gitea.com";
|
||||||
|
|
@ -18,7 +19,7 @@ export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, org
|
||||||
GITEA_CLOUD_HOSTNAME;
|
GITEA_CLOUD_HOSTNAME;
|
||||||
|
|
||||||
const token = config.token ?
|
const token = config.token ?
|
||||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
await getTokenFromConfig(config.token, orgId, db) :
|
||||||
hostname === GITEA_CLOUD_HOSTNAME ?
|
hostname === GITEA_CLOUD_HOSTNAME ?
|
||||||
env.FALLBACK_GITEA_CLOUD_TOKEN :
|
env.FALLBACK_GITEA_CLOUD_TOKEN :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ import micromatch from "micromatch";
|
||||||
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
||||||
import { GithubAppManager } from "./ee/githubAppManager.js";
|
import { GithubAppManager } from "./ee/githubAppManager.js";
|
||||||
import { env } from "./env.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";
|
export const GITHUB_CLOUD_HOSTNAME = "github.com";
|
||||||
const logger = createLogger('github');
|
const logger = createLogger('github');
|
||||||
|
|
@ -97,7 +98,7 @@ export const getGitHubReposFromConfig = async (config: GithubConnectionConfig, o
|
||||||
GITHUB_CLOUD_HOSTNAME;
|
GITHUB_CLOUD_HOSTNAME;
|
||||||
|
|
||||||
const token = config.token ?
|
const token = config.token ?
|
||||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
await getTokenFromConfig(config.token, orgId, db) :
|
||||||
hostname === GITHUB_CLOUD_HOSTNAME ?
|
hostname === GITHUB_CLOUD_HOSTNAME ?
|
||||||
env.FALLBACK_GITHUB_CLOUD_TOKEN :
|
env.FALLBACK_GITHUB_CLOUD_TOKEN :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@ import { Gitlab, ProjectSchema } from "@gitbeaker/rest";
|
||||||
import micromatch from "micromatch";
|
import micromatch from "micromatch";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"
|
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 { PrismaClient } from "@sourcebot/db";
|
||||||
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import { env } from "./env.js";
|
import { env } from "./env.js";
|
||||||
|
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||||
|
|
||||||
const logger = createLogger('gitlab');
|
const logger = createLogger('gitlab');
|
||||||
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
|
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
|
||||||
|
|
@ -17,7 +18,7 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, o
|
||||||
GITLAB_CLOUD_HOSTNAME;
|
GITLAB_CLOUD_HOSTNAME;
|
||||||
|
|
||||||
const token = config.token ?
|
const token = config.token ?
|
||||||
await getTokenFromConfig(config.token, orgId, db, logger) :
|
await getTokenFromConfig(config.token, orgId, db) :
|
||||||
hostname === GITLAB_CLOUD_HOSTNAME ?
|
hostname === GITLAB_CLOUD_HOSTNAME ?
|
||||||
env.FALLBACK_GITLAB_CLOUD_TOKEN :
|
env.FALLBACK_GITLAB_CLOUD_TOKEN :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ import { Logger } from "winston";
|
||||||
import { RepoAuthCredentials, RepoWithConnections } from "./types.js";
|
import { RepoAuthCredentials, RepoWithConnections } from "./types.js";
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { PrismaClient, Repo } from "@sourcebot/db";
|
import { PrismaClient, Repo } from "@sourcebot/db";
|
||||||
import { getTokenFromConfig as getTokenFromConfigBase } from "@sourcebot/crypto";
|
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||||
import { BackendException, BackendError } from "@sourcebot/error";
|
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig, AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
|
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig, BitbucketConnectionConfig, AzureDevOpsConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
|
||||||
import { GithubAppManager } from "./ee/githubAppManager.js";
|
import { GithubAppManager } from "./ee/githubAppManager.js";
|
||||||
|
|
@ -24,22 +23,6 @@ export const marshalBool = (value?: boolean) => {
|
||||||
return !!value ? '1' : '0';
|
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) => {
|
export const resolvePathRelativeToConfig = (localPath: string, configPath: string) => {
|
||||||
let absolutePath = localPath;
|
let absolutePath = localPath;
|
||||||
if (!path.isAbsolute(absolutePath)) {
|
if (!path.isAbsolute(absolutePath)) {
|
||||||
|
|
@ -156,7 +139,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
||||||
if (connection.connectionType === 'github') {
|
if (connection.connectionType === 'github') {
|
||||||
const config = connection.config as unknown as GithubConnectionConfig;
|
const config = connection.config as unknown as GithubConnectionConfig;
|
||||||
if (config.token) {
|
if (config.token) {
|
||||||
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
const token = await getTokenFromConfig(config.token, connection.orgId, db);
|
||||||
return {
|
return {
|
||||||
hostUrl: config.url,
|
hostUrl: config.url,
|
||||||
token,
|
token,
|
||||||
|
|
@ -171,7 +154,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
||||||
} else if (connection.connectionType === 'gitlab') {
|
} else if (connection.connectionType === 'gitlab') {
|
||||||
const config = connection.config as unknown as GitlabConnectionConfig;
|
const config = connection.config as unknown as GitlabConnectionConfig;
|
||||||
if (config.token) {
|
if (config.token) {
|
||||||
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
const token = await getTokenFromConfig(config.token, connection.orgId, db);
|
||||||
return {
|
return {
|
||||||
hostUrl: config.url,
|
hostUrl: config.url,
|
||||||
token,
|
token,
|
||||||
|
|
@ -187,7 +170,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
||||||
} else if (connection.connectionType === 'gitea') {
|
} else if (connection.connectionType === 'gitea') {
|
||||||
const config = connection.config as unknown as GiteaConnectionConfig;
|
const config = connection.config as unknown as GiteaConnectionConfig;
|
||||||
if (config.token) {
|
if (config.token) {
|
||||||
const token = await getTokenFromConfig(config.token, connection.orgId, db, logger);
|
const token = await getTokenFromConfig(config.token, connection.orgId, db);
|
||||||
return {
|
return {
|
||||||
hostUrl: config.url,
|
hostUrl: config.url,
|
||||||
token,
|
token,
|
||||||
|
|
@ -202,7 +185,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
||||||
} else if (connection.connectionType === 'bitbucket') {
|
} else if (connection.connectionType === 'bitbucket') {
|
||||||
const config = connection.config as unknown as BitbucketConnectionConfig;
|
const config = connection.config as unknown as BitbucketConnectionConfig;
|
||||||
if (config.token) {
|
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';
|
const username = config.user ?? 'x-token-auth';
|
||||||
return {
|
return {
|
||||||
hostUrl: config.url,
|
hostUrl: config.url,
|
||||||
|
|
@ -219,7 +202,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, db: P
|
||||||
} else if (connection.connectionType === 'azuredevops') {
|
} else if (connection.connectionType === 'azuredevops') {
|
||||||
const config = connection.config as unknown as AzureDevOpsConnectionConfig;
|
const config = connection.config as unknown as AzureDevOpsConnectionConfig;
|
||||||
if (config.token) {
|
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
|
// 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
|
// 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 { render } from "@react-email/components";
|
||||||
import * as Sentry from '@sentry/nextjs';
|
import * as Sentry from '@sentry/nextjs';
|
||||||
import { encrypt, generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto";
|
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 { createLogger } from "@sourcebot/logger";
|
||||||
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
|
||||||
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.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 { 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 { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
|
||||||
import { ApiKeyPayload, TenancyMode } from "./lib/types";
|
import { ApiKeyPayload, TenancyMode } from "./lib/types";
|
||||||
import { withOptionalAuthV2 } from "./withAuthV2";
|
import { withAuthV2, withOptionalAuthV2 } from "./withAuthV2";
|
||||||
|
|
||||||
const logger = createLogger('web-actions');
|
const logger = createLogger('web-actions');
|
||||||
const auditService = getAuditService();
|
const auditService = getAuditService();
|
||||||
|
|
@ -593,6 +593,7 @@ export const getReposStats = async () => sew(() =>
|
||||||
prisma.repo.count({
|
prisma.repo.count({
|
||||||
where: {
|
where: {
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
indexedAt: null,
|
||||||
jobs: {
|
jobs: {
|
||||||
some: {
|
some: {
|
||||||
type: RepoIndexingJobType.INDEX,
|
type: RepoIndexingJobType.INDEX,
|
||||||
|
|
@ -604,7 +605,6 @@ export const getReposStats = async () => sew(() =>
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
indexedAt: null,
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
prisma.repo.count({
|
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(() =>
|
export const getRepoInfoByName = async (repoName: string) => sew(() =>
|
||||||
withOptionalAuthV2(async ({ org, prisma }) => {
|
withOptionalAuthV2(async ({ org, prisma }) => {
|
||||||
// @note: repo names are represented by their remote url
|
// @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 { SourcebotLogo } from "@/app/components/sourcebotLogo";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -39,6 +39,11 @@ export const NavigationMenu = async ({
|
||||||
throw new ServiceErrorException(repoStats);
|
throw new ServiceErrorException(repoStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connectionStats = isAuthenticated ? await getConnectionStats() : null;
|
||||||
|
if (isServiceError(connectionStats)) {
|
||||||
|
throw new ServiceErrorException(connectionStats);
|
||||||
|
}
|
||||||
|
|
||||||
const sampleRepos = await getRepos({
|
const sampleRepos = await getRepos({
|
||||||
where: {
|
where: {
|
||||||
jobs: {
|
jobs: {
|
||||||
|
|
@ -93,7 +98,12 @@ export const NavigationMenu = async ({
|
||||||
<NavigationItems
|
<NavigationItems
|
||||||
domain={domain}
|
domain={domain}
|
||||||
numberOfRepos={numberOfRepos}
|
numberOfRepos={numberOfRepos}
|
||||||
numberOfReposWithFirstTimeIndexingJobsInProgress={numberOfReposWithFirstTimeIndexingJobsInProgress}
|
isReposButtonNotificationDotVisible={numberOfReposWithFirstTimeIndexingJobsInProgress > 0}
|
||||||
|
isSettingsButtonNotificationDotVisible={
|
||||||
|
connectionStats ?
|
||||||
|
connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0 :
|
||||||
|
false
|
||||||
|
}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
/>
|
/>
|
||||||
</NavigationMenuBase>
|
</NavigationMenuBase>
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,23 @@
|
||||||
import { NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
import { NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { cn, getShortenedNumberDisplayString } from "@/lib/utils";
|
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 { usePathname } from "next/navigation";
|
||||||
|
import { NotificationDot } from "../notificationDot";
|
||||||
|
|
||||||
interface NavigationItemsProps {
|
interface NavigationItemsProps {
|
||||||
domain: string;
|
domain: string;
|
||||||
numberOfRepos: number;
|
numberOfRepos: number;
|
||||||
numberOfReposWithFirstTimeIndexingJobsInProgress: number;
|
isReposButtonNotificationDotVisible: boolean;
|
||||||
|
isSettingsButtonNotificationDotVisible: boolean;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavigationItems = ({
|
export const NavigationItems = ({
|
||||||
domain,
|
domain,
|
||||||
numberOfRepos,
|
numberOfRepos,
|
||||||
numberOfReposWithFirstTimeIndexingJobsInProgress,
|
isReposButtonNotificationDotVisible,
|
||||||
|
isSettingsButtonNotificationDotVisible,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
}: NavigationItemsProps) => {
|
}: NavigationItemsProps) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
@ -59,9 +62,7 @@ export const NavigationItems = ({
|
||||||
<span className="mr-2">Repositories</span>
|
<span className="mr-2">Repositories</span>
|
||||||
<Badge variant="secondary" className="px-1.5 relative">
|
<Badge variant="secondary" className="px-1.5 relative">
|
||||||
{getShortenedNumberDisplayString(numberOfRepos)}
|
{getShortenedNumberDisplayString(numberOfRepos)}
|
||||||
{numberOfReposWithFirstTimeIndexingJobsInProgress > 0 && (
|
{isReposButtonNotificationDotVisible && <NotificationDot className="absolute -right-0.5 -top-0.5" />}
|
||||||
<CircleIcon className="absolute -right-0.5 -top-0.5 h-2 w-2 text-green-600" fill="currentColor" />
|
|
||||||
)}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
{isActive(`/${domain}/repos`) && <ActiveIndicator />}
|
{isActive(`/${domain}/repos`) && <ActiveIndicator />}
|
||||||
|
|
@ -74,6 +75,7 @@ export const NavigationItems = ({
|
||||||
>
|
>
|
||||||
<SettingsIcon className="w-4 h-4 mr-1" />
|
<SettingsIcon className="w-4 h-4 mr-1" />
|
||||||
Settings
|
Settings
|
||||||
|
{isSettingsButtonNotificationDotVisible && <NotificationDot className="absolute -right-0.5 -top-0.5" />}
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
{isActive(`/${domain}/settings`) && <ActiveIndicator />}
|
{isActive(`/${domain}/settings`) && <ActiveIndicator />}
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
|
|
|
||||||
|
|
@ -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 { useToast } from "@/components/hooks/use-toast";
|
||||||
import { DisplayDate } from "../../components/DisplayDate"
|
import { DisplayDate } from "../../components/DisplayDate"
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||||
|
import { NotificationDot } from "../../components/notificationDot"
|
||||||
|
|
||||||
// @see: https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
|
// @see: https://v0.app/chat/repo-indexing-status-uhjdDim8OUS
|
||||||
|
|
||||||
|
|
@ -53,6 +54,7 @@ export type Repo = {
|
||||||
imageUrl: string | null
|
imageUrl: string | null
|
||||||
indexedCommitHash: string | null
|
indexedCommitHash: string | null
|
||||||
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
||||||
|
isFirstTimeIndex: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusBadgeVariants = cva("", {
|
const statusBadgeVariants = cva("", {
|
||||||
|
|
@ -111,14 +113,32 @@ export const columns: ColumnDef<Repo>[] = [
|
||||||
{repo.displayName?.charAt(0) ?? repo.name.charAt(0)}
|
{repo.displayName?.charAt(0) ?? repo.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Link href={getBrowsePath({
|
|
||||||
repoName: repo.name,
|
{/* Link to the details page (instead of browse) when the repo is indexing
|
||||||
path: '/',
|
as the code will not be available yet */}
|
||||||
pathType: 'tree',
|
<Link
|
||||||
domain: SINGLE_TENANT_ORG_DOMAIN,
|
href={repo.isFirstTimeIndex ? `/${SINGLE_TENANT_ORG_DOMAIN}/repos/${repo.id}` : getBrowsePath({
|
||||||
})} className="font-medium hover:underline">
|
repoName: repo.name,
|
||||||
|
path: '/',
|
||||||
|
pathType: 'tree',
|
||||||
|
domain: SINGLE_TENANT_ORG_DOMAIN,
|
||||||
|
})}
|
||||||
|
className="font-medium hover:underline"
|
||||||
|
>
|
||||||
{repo.displayName || repo.name}
|
{repo.displayName || repo.name}
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -150,7 +170,7 @@ export const columns: ColumnDef<Repo>[] = [
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 ? (
|
const HashComponent = commitUrl ? (
|
||||||
<Link
|
<Link
|
||||||
href={commitUrl}
|
href={commitUrl}
|
||||||
className="font-mono text-sm text-link hover:underline"
|
className="font-mono text-sm text-link hover:underline"
|
||||||
>
|
>
|
||||||
{smallHash}
|
{smallHash}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className="font-mono text-sm text-muted-foreground">
|
<span className="font-mono text-sm text-muted-foreground">
|
||||||
{smallHash}
|
{smallHash}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,31 @@ import { ServiceErrorException } from "@/lib/serviceError";
|
||||||
import { isServiceError } from "@/lib/utils";
|
import { isServiceError } from "@/lib/utils";
|
||||||
import { withOptionalAuthV2 } from "@/withAuthV2";
|
import { withOptionalAuthV2 } from "@/withAuthV2";
|
||||||
import { ReposTable } from "./components/reposTable";
|
import { ReposTable } from "./components/reposTable";
|
||||||
|
import { RepoIndexingJobStatus } from "@sourcebot/db";
|
||||||
|
|
||||||
export default async function ReposPage() {
|
export default async function ReposPage() {
|
||||||
|
|
||||||
const repos = await getReposWithLatestJob();
|
const _repos = await getReposWithLatestJob();
|
||||||
if (isServiceError(repos)) {
|
if (isServiceError(_repos)) {
|
||||||
throw new ServiceErrorException(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
@ -27,7 +44,8 @@ export default async function ReposPage() {
|
||||||
createdAt: repo.createdAt,
|
createdAt: repo.createdAt,
|
||||||
webUrl: repo.webUrl,
|
webUrl: repo.webUrl,
|
||||||
imageUrl: repo.imageUrl,
|
imageUrl: repo.imageUrl,
|
||||||
latestJobStatus: repo.jobs.length > 0 ? repo.jobs[0].status : null,
|
latestJobStatus: repo.latestJobStatus,
|
||||||
|
isFirstTimeIndex: repo.isFirstTimeIndex,
|
||||||
codeHostType: repo.external_codeHostType,
|
codeHostType: repo.external_codeHostType,
|
||||||
indexedCommitHash: repo.indexedCommitHash,
|
indexedCommitHash: repo.indexedCommitHash,
|
||||||
}))} />
|
}))} />
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
"use client"
|
"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 Link from "next/link"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { cn } from "@/lib/utils"
|
import React from "react"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
|
||||||
|
|
||||||
export type SidebarNavItem = {
|
export type SidebarNavItem = {
|
||||||
href: string
|
href: string
|
||||||
hrefRegex?: string
|
hrefRegex?: string
|
||||||
title: React.ReactNode
|
title: React.ReactNode
|
||||||
|
isNotificationDotVisible?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
|
|
@ -43,6 +45,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
|
{item.isNotificationDotVisible && <NotificationDot className="ml-1.5" />}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { DisplayDate } from "@/app/[domain]/components/DisplayDate"
|
import { DisplayDate } from "@/app/[domain]/components/DisplayDate"
|
||||||
|
import { NotificationDot } from "@/app/[domain]/components/notificationDot"
|
||||||
import { useToast } from "@/components/hooks/use-toast"
|
import { useToast } from "@/components/hooks/use-toast"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
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 { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants"
|
||||||
import { CodeHostType, getCodeHostIcon } from "@/lib/utils"
|
import { CodeHostType, getCodeHostIcon } from "@/lib/utils"
|
||||||
import {
|
import {
|
||||||
|
|
@ -35,6 +37,7 @@ export type Connection = {
|
||||||
syncedAt: Date | null
|
syncedAt: Date | null
|
||||||
codeHostType: CodeHostType
|
codeHostType: CodeHostType
|
||||||
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
latestJobStatus: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED" | null
|
||||||
|
isFirstTimeSync: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusBadgeVariants = cva("", {
|
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">
|
<Link href={`/${SINGLE_TENANT_ORG_DOMAIN}/settings/connections/${connection.id}`} className="font-medium hover:underline">
|
||||||
{connection.name}
|
{connection.name}
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,33 @@ import { CodeHostType, isServiceError } from "@/lib/utils";
|
||||||
import { withAuthV2 } from "@/withAuthV2";
|
import { withAuthV2 } from "@/withAuthV2";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ConnectionsTable } from "./components/connectionsTable";
|
import { ConnectionsTable } from "./components/connectionsTable";
|
||||||
|
import { ConnectionSyncJobStatus } from "@prisma/client";
|
||||||
|
|
||||||
const DOCS_URL = "https://docs.sourcebot.dev/docs/connections/overview";
|
const DOCS_URL = "https://docs.sourcebot.dev/docs/connections/overview";
|
||||||
|
|
||||||
export default async function ConnectionsPage() {
|
export default async function ConnectionsPage() {
|
||||||
const connections = await getConnectionsWithLatestJob();
|
const _connections = await getConnectionsWithLatestJob();
|
||||||
if (isServiceError(connections)) {
|
if (isServiceError(_connections)) {
|
||||||
throw new ServiceErrorException(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 (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -24,7 +42,8 @@ export default async function ConnectionsPage() {
|
||||||
name: connection.name,
|
name: connection.name,
|
||||||
codeHostType: connection.connectionType as CodeHostType,
|
codeHostType: connection.connectionType as CodeHostType,
|
||||||
syncedAt: connection.syncedAt,
|
syncedAt: connection.syncedAt,
|
||||||
latestJobStatus: connection.syncJobs.length > 0 ? connection.syncJobs[0].status : null,
|
latestJobStatus: connection.latestJobStatus,
|
||||||
|
isFirstTimeSync: connection.isFirstTimeSync,
|
||||||
}))} />
|
}))} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -34,16 +53,22 @@ const getConnectionsWithLatestJob = async () => sew(() =>
|
||||||
withAuthV2(async ({ prisma }) => {
|
withAuthV2(async ({ prisma }) => {
|
||||||
const connections = await prisma.connection.findMany({
|
const connections = await prisma.connection.findMany({
|
||||||
include: {
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
syncJobs: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
syncJobs: {
|
syncJobs: {
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc'
|
createdAt: 'desc'
|
||||||
},
|
},
|
||||||
take: 1
|
take: 1
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
name: 'asc'
|
name: 'asc'
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return connections;
|
return connections;
|
||||||
}));
|
}));
|
||||||
|
|
@ -6,7 +6,7 @@ import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { isServiceError } from "@/lib/utils";
|
import { isServiceError } from "@/lib/utils";
|
||||||
import { getMe, getOrgAccountRequests } from "@/actions";
|
import { getConnectionStats, getMe, getOrgAccountRequests } from "@/actions";
|
||||||
import { ServiceErrorException } from "@/lib/serviceError";
|
import { ServiceErrorException } from "@/lib/serviceError";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
import { getOrgFromDomain } from "@/data/org";
|
||||||
import { OrgRole } from "@prisma/client";
|
import { OrgRole } from "@prisma/client";
|
||||||
|
|
@ -63,6 +63,11 @@ export default async function SettingsLayout(
|
||||||
numJoinRequests = requests.length;
|
numJoinRequests = requests.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connectionStats = await getConnectionStats();
|
||||||
|
if (isServiceError(connectionStats)) {
|
||||||
|
throw new ServiceErrorException(connectionStats);
|
||||||
|
}
|
||||||
|
|
||||||
const sidebarNavItems: SidebarNavItem[] = [
|
const sidebarNavItems: SidebarNavItem[] = [
|
||||||
{
|
{
|
||||||
title: "General",
|
title: "General",
|
||||||
|
|
@ -98,6 +103,7 @@ export default async function SettingsLayout(
|
||||||
title: "Connections",
|
title: "Connections",
|
||||||
href: `/${domain}/settings/connections`,
|
href: `/${domain}/settings/connections`,
|
||||||
hrefRegex: `/${domain}/settings/connections(\/[^/]+)?$`,
|
hrefRegex: `/${domain}/settings/connections(\/[^/]+)?$`,
|
||||||
|
isNotificationDotVisible: connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0,
|
||||||
}
|
}
|
||||||
] : []),
|
] : []),
|
||||||
{
|
{
|
||||||
|
|
@ -141,3 +147,4 @@ export default async function SettingsLayout(
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue