Compare commits

...

2 Commits

Author SHA1 Message Date
b467619341 improvement: address greptile feedback 2025-07-09 17:33:51 -07:00
0f20758df2 feature: add project overview page 2025-07-09 17:23:15 -07:00
30 changed files with 1049 additions and 32 deletions

View File

@ -5,6 +5,7 @@ import crypto, { KeyObject } from "crypto";
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { isValidIp } from "@app/lib/ip";
import { ms } from "@app/lib/ms";
import { OrgServiceActor } from "@app/lib/types";
import { isFQDN } from "@app/lib/validator/validate-url";
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
@ -795,6 +796,26 @@ export const kmipServiceFactory = ({
};
};
const getProjectClientCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const clients = await kmipClientDAL.find(
{
projectId
},
{ count: true }
);
return Number(clients?.[0]?.count ?? 0);
};
return {
createKmipClient,
updateKmipClient,
@ -806,6 +827,7 @@ export const kmipServiceFactory = ({
generateOrgKmipServerCertificate,
getOrgKmip,
getServerCertificateBySerialNumber,
registerServer
registerServer,
getProjectClientCount
};
};

View File

@ -437,6 +437,7 @@ export const secretScanningV2DALFactory = (db: TDbClient) => {
return {
dataSources: {
...dataSourceOrm,
findRaw: dataSourceOrm.find,
find: findDataSource,
findById: findDataSourceById,
findOne: findOneDataSource,

View File

@ -881,6 +881,47 @@ export const secretScanningV2ServiceFactory = ({
return config;
};
const getProjectResourcesCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const dataSources = await secretScanningV2DAL.dataSources.findRaw(
{
projectId
},
{ count: true }
);
const resources = await secretScanningV2DAL.resources.find(
{
$in: {
dataSourceId: dataSources.map((dataSource) => dataSource.id)
}
},
{ count: true }
);
const findings = await secretScanningV2DAL.findings.find(
{
projectId,
status: SecretScanningFindingStatus.Unresolved
},
{ count: true }
);
return {
dataSourceCount: Number(dataSources?.[0]?.count ?? 0),
resourceCount: Number(resources?.[0]?.count ?? 0),
findingCount: Number(findings?.[0]?.count ?? 0)
};
};
return {
listSecretScanningDataSourceOptions,
listSecretScanningDataSourcesByProjectId,
@ -900,6 +941,7 @@ export const secretScanningV2ServiceFactory = ({
updateSecretScanningFindingById,
findSecretScanningConfigByProjectId,
upsertSecretScanningConfig,
getProjectResourcesCount,
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService)
};

View File

@ -8,6 +8,7 @@ import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login
import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-dal";
import { TSshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
@ -414,6 +415,26 @@ export const sshHostGroupServiceFactory = ({
return { sshHostGroup, sshHost };
};
const getProjectHostGroupCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const hostGroups = await sshHostGroupDAL.find(
{
projectId
},
{ count: true }
);
return Number(hostGroups?.[0]?.count ?? 0);
};
return {
createSshHostGroup,
getSshHostGroup,
@ -421,6 +442,7 @@ export const sshHostGroupServiceFactory = ({
updateSshHostGroup,
listSshHostGroupHosts,
addHostToSshHostGroup,
removeHostFromSshHostGroup
removeHostFromSshHostGroup,
getProjectHostGroupCount
};
};

View File

@ -13,6 +13,7 @@ import { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ss
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
import { PgSqlLock } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
@ -60,6 +61,7 @@ type TSshHostServiceFactoryDep = {
| "findOne"
| "findSshHostByIdWithLoginMappings"
| "findUserAccessibleSshHosts"
| "find"
>;
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
@ -637,6 +639,26 @@ export const sshHostServiceFactory = ({
return publicKey;
};
const getProjectHostCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const hosts = await sshHostDAL.find(
{
projectId
},
{ count: true }
);
return Number(hosts?.[0]?.count ?? 0);
};
return {
listSshHosts,
createSshHost,
@ -646,6 +668,7 @@ export const sshHostServiceFactory = ({
issueSshHostUserCert,
issueSshHostHostCert,
getSshHostUserCaPk,
getSshHostHostCaPk
getSshHostHostCaPk,
getProjectHostCount
};
};

View File

@ -12,6 +12,11 @@ type TKnexDynamicPrimitiveOperator<T extends object> =
operator: "notIn";
value: string[];
field: Extract<keyof T, string>;
}
| {
operator: "lte";
value: Date;
field: Extract<keyof T, string>;
};
type TKnexDynamicInOperator<T extends object> = {
@ -82,6 +87,10 @@ export const buildDynamicKnexQuery = <T extends object>(
});
break;
}
case "lte": {
void queryBuilder.where(filterAst.field, "<=", filterAst.value);
break;
}
default:
throw new UnauthorizedError({ message: `Invalid knex dynamic operator: ${filterAst.operator}` });
}

View File

@ -9,7 +9,7 @@ import { DASHBOARD } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { OrderByDirection } from "@app/lib/types";
import { secretsLimit } from "@app/server/config/rateLimiter";
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -1354,4 +1354,128 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
return { secrets };
}
});
server.route({
method: "GET",
url: "/project-overview",
config: {
rateLimit: readLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string(),
projectSlug: z.string()
}),
response: {
200: z.object({
accessControl: z.object({
userCount: z.number(),
machineIdentityCount: z.number(),
groupCount: z.number()
}),
secretsManagement: z.object({
secretCount: z.number(),
environmentCount: z.number(),
pendingApprovalCount: z.number()
}),
certificateManagement: z.object({
internalCaCount: z.number(),
externalCaCount: z.number(),
expiryCount: z.number()
}),
kms: z.object({
keyCount: z.number(),
kmipClientCount: z.number()
}),
ssh: z.object({
hostCount: z.number(),
hostGroupCount: z.number()
}),
secretScanning: z.object({
dataSourceCount: z.number(),
resourceCount: z.number(),
findingCount: z.number()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const {
query: { projectId, projectSlug },
permission
} = req;
const userCount = await server.services.projectMembership.getProjectMembershipCount(projectId, permission);
const machineIdentityCount = await server.services.identityProject.getProjectIdentityCount(projectId, permission);
const groupCount = await server.services.groupProject.getProjectGroupCount(projectId, permission);
const secretsManagement = await server.services.secret.getProjectSecretResourcesCount(projectId, permission);
const accessApprovals = await server.services.accessApprovalRequest.getCount({
projectSlug,
actor: permission.type,
actorId: permission.id,
actorOrgId: permission.orgId,
actorAuthMethod: permission.authMethod
});
const secretApprovals = await server.services.secretApprovalRequest.requestCount({
projectId,
actor: permission.type,
actorId: permission.id,
actorOrgId: permission.orgId,
actorAuthMethod: permission.authMethod
});
const certificateAuthorityCount = await server.services.certificateAuthority.getProjectCertificateAuthorityCount(
projectId,
permission
);
const expiryCount = await server.services.certificate.getProjectExpiringCertificates(projectId, permission);
const keyCount = await server.services.cmek.getProjectKeyCount(projectId, permission);
const kmipClientCount = await server.services.kmip.getProjectClientCount(projectId, permission);
const hostCount = await server.services.sshHost.getProjectHostCount(projectId, permission);
const hostGroupCount = await server.services.sshHostGroup.getProjectHostGroupCount(projectId, permission);
const secretScanning = await server.services.secretScanningV2.getProjectResourcesCount(projectId, permission);
return {
accessControl: {
userCount,
machineIdentityCount,
groupCount
},
secretsManagement: {
...secretsManagement,
pendingApprovalCount: accessApprovals.count.pendingCount + secretApprovals.open
},
certificateManagement: {
...certificateAuthorityCount,
expiryCount
},
kms: {
keyCount,
kmipClientCount
},
ssh: {
hostCount,
hostGroupCount
},
secretScanning
};
}
});
};

View File

@ -47,8 +47,9 @@ type TCertificateAuthorityServiceFactoryDep = {
| "findByIdWithAssociatedCa"
| "findWithAssociatedCa"
| "findByNameAndProjectIdWithAssociatedCa"
| "find"
>;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update" | "find">;
internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@ -382,11 +383,36 @@ export const certificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "Invalid certificate authority type" });
};
const getProjectCertificateAuthorityCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const cas = await certificateAuthorityDAL.find({ projectId });
const externalCas = await externalCertificateAuthorityDAL.find(
{ $in: { caId: cas.map((ca) => ca.id) } },
{ count: true }
);
const externalCaCount = Number(externalCas?.[0]?.count ?? 0);
return {
externalCaCount,
internalCaCount: cas.length - externalCaCount
};
};
return {
createCertificateAuthority,
findCertificateAuthorityByNameAndProjectId,
listCertificateAuthoritiesByProjectId,
updateCertificateAuthority,
deleteCertificateAuthority
deleteCertificateAuthority,
getProjectCertificateAuthorityCount
};
};

View File

@ -9,6 +9,7 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
@ -600,6 +601,38 @@ export const certificateServiceFactory = ({
};
};
const getProjectExpiringCertificates = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const fourteenDaysFromNow = new Date(new Date().setDate(new Date().getDate() + 14));
const expiringCertificates = await certificateDAL.find(
{
projectId,
$complex: {
operator: "and",
value: [
{
operator: "lte",
field: "notAfter",
value: fourteenDaysFromNow
}
]
}
},
{ count: true }
);
return Number(expiringCertificates?.[0]?.count ?? 0);
};
return {
getCert,
getCertPrivateKey,
@ -607,6 +640,7 @@ export const certificateServiceFactory = ({
revokeCert,
getCertBody,
importCert,
getCertBundle
getCertBundle,
getProjectExpiringCertificates
};
};

View File

@ -375,6 +375,26 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
};
};
const getProjectKeyCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const keys = await kmsDAL.find(
{
projectId
},
{ count: true }
);
return Number(keys?.[0]?.count ?? 0);
};
return {
createCmek,
updateCmekById,
@ -387,6 +407,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
cmekSign,
cmekVerify,
listSigningAlgorithms,
getPublicKey
getPublicKey,
getProjectKeyCount
};
};

View File

@ -13,6 +13,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { OrgServiceActor } from "@app/lib/types";
import { isUuidV4 } from "@app/lib/validator";
import { TGroupDALFactory } from "../../ee/services/group/group-dal";
@ -33,7 +34,10 @@ import {
} from "./group-project-types";
type TGroupProjectServiceFactoryDep = {
groupProjectDAL: Pick<TGroupProjectDALFactory, "findOne" | "transaction" | "create" | "delete" | "findByProjectId">;
groupProjectDAL: Pick<
TGroupProjectDALFactory,
"findOne" | "transaction" | "create" | "delete" | "findByProjectId" | "find"
>;
groupProjectMembershipRoleDAL: Pick<
TGroupProjectMembershipRoleDALFactory,
"create" | "transaction" | "insertMany" | "delete"
@ -508,12 +512,33 @@ export const groupProjectServiceFactory = ({
return { users: members, totalCount };
};
const getProjectGroupCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const projectGroups = await groupProjectDAL.find(
{
projectId
},
{ count: true }
);
return Number(projectGroups?.[0]?.count ?? 0);
};
return {
addGroupToProject,
updateGroupInProject,
removeGroupFromProject,
listGroupsInProject,
getGroupInProject,
listProjectGroupUsers
listProjectGroupUsers,
getProjectGroupCount
};
};

View File

@ -10,6 +10,7 @@ import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { OrgServiceActor } from "@app/lib/types";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TProjectDALFactory } from "../project/project-dal";
@ -403,12 +404,33 @@ export const identityProjectServiceFactory = ({
return identityMembership;
};
const getProjectIdentityCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const identityMemberships = await identityProjectDAL.find(
{
projectId
},
{ count: true }
);
return Number(identityMemberships?.[0]?.count ?? 0);
};
return {
createProjectIdentity,
updateProjectIdentity,
deleteProjectIdentity,
listProjectIdentities,
getProjectIdentityByIdentityId,
getProjectIdentityByMembershipId
getProjectIdentityByMembershipId,
getProjectIdentityCount
};
};

View File

@ -14,6 +14,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { OrgServiceActor } from "@app/lib/types";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type";
@ -567,6 +568,35 @@ export const projectMembershipServiceFactory = ({
return deletedMembership;
};
const getProjectMembershipCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const projectMemberships = await projectMembershipDAL.find({
projectId
});
const users = await userDAL.find(
{
$in: {
id: projectMemberships.map((membership) => membership.userId)
},
isGhost: false
},
{
count: true
}
);
return Number(users?.[0]?.count ?? 0);
};
return {
getProjectMemberships,
getProjectMembershipByUsername,
@ -575,6 +605,7 @@ export const projectMembershipServiceFactory = ({
deleteProjectMembership, // TODO: Remove this
addUsersToProject,
leaveProject,
getProjectMembershipById
getProjectMembershipById,
getProjectMembershipCount
};
};

View File

@ -811,6 +811,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
return {
...secretOrm,
rawFind: secretOrm.find,
update,
bulkUpdate,
deleteMany,

View File

@ -26,6 +26,7 @@ import { diff, groupBy } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
@ -86,7 +87,7 @@ type TSecretV2BridgeServiceFactoryDep = {
secretTagDAL: TSecretTagDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
folderDAL: Pick<
TSecretFolderDALFactory,
| "findBySecretPath"
@ -2914,6 +2915,39 @@ export const secretV2BridgeServiceFactory = ({
});
};
const getProjectSecretResourcesCount = async (projectId: string, actor: OrgServiceActor) => {
// Anyone in the project should be able to get count.
await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId
});
const environments = await projectEnvDAL.find({
projectId
});
const folders = await folderDAL.find({
isReserved: false,
$in: {
envId: environments.map((env) => env.id)
}
});
const secrets = await secretDAL.rawFind(
{
$in: {
folderId: folders.map((folder) => folder.id)
}
},
{ countDistinct: "key" }
);
return { environmentCount: environments.length, secretCount: Number(secrets?.[0]?.count ?? 0) };
};
return {
createSecret,
deleteSecret,
@ -2933,6 +2967,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretsByFolderMappings,
getSecretById,
getAccessibleSecrets,
getSecretVersionsByIds
getSecretVersionsByIds,
getProjectSecretResourcesCount
};
};

View File

@ -3339,6 +3339,20 @@ export const secretServiceFactory = ({
}));
};
const getProjectSecretResourcesCount = async (projectId: string, actor: OrgServiceActor) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge)
throw new BadRequestError({
message: "Project version does not support pagination",
name: "PaginationNotSupportedError"
});
const count = await secretV2BridgeService.getProjectSecretResourcesCount(projectId, actor);
return count;
};
return {
attachTags,
detachTags,
@ -3371,6 +3385,7 @@ export const secretServiceFactory = ({
getSecretByIdRaw,
getAccessibleSecrets,
getSecretVersionsV2ByIds,
getChangeVersions
getChangeVersions,
getProjectSecretResourcesCount
};
};

File diff suppressed because one or more lines are too long

View File

@ -59,15 +59,18 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
return project;
};
export const getProjectHomePage = (type: ProjectType) => {
switch (type) {
case ProjectType.CertificateManager:
return `/projects/$projectId/${type}/subscribers` as const;
case ProjectType.SecretScanning:
return `/projects/$projectId/${type}/data-sources` as const;
default:
return `/projects/$projectId/${type}/overview` as const;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const getProjectHomePage = (_type: ProjectType) => {
return "/projects/$projectId/overview";
// switch (type) {
// case ProjectType.CertificateManager:
// return `/projects/$projectId/${type}/subscribers` as const;
// case ProjectType.SecretScanning:
// return `/projects/$projectId/${type}/data-sources` as const;
// default:
// return `/projects/$projectId/${type}/overview` as const;
// }
};
export const getProjectTitle = (type: ProjectType) => {

View File

@ -15,7 +15,9 @@ import {
TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO,
TGetDashboardProjectSecretsQuickSearchDTO
TGetDashboardProjectSecretsQuickSearchDTO,
TGetProjectOverview,
TProjectOverview
} from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
@ -72,7 +74,13 @@ export const dashboardKeys = {
...dashboardKeys.all(),
"accessible-secrets",
{ projectId, secretPath, environment, filterByAction }
] as const
] as const,
getProjectOverview: ({ projectId, projectSlug }: TGetProjectOverview) => [
...dashboardKeys.all(),
"project-overview",
projectId,
projectSlug
]
};
export const fetchProjectSecretsOverview = async ({
@ -464,3 +472,35 @@ export const useGetAccessibleSecrets = ({
fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive })
});
};
export const useGetProjectOverview = (
{ projectId, projectSlug }: TGetProjectOverview,
options?: Omit<
UseQueryOptions<
TProjectOverview,
unknown,
TProjectOverview,
ReturnType<typeof dashboardKeys.getProjectOverview>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({
...options,
refetchOnMount: "always",
queryKey: dashboardKeys.getProjectOverview({
projectId,
projectSlug
}),
queryFn: async () => {
const { data } = await apiRequest.get<TProjectOverview>(
"/api/v1/dashboard/project-overview",
{
params: { projectId, projectSlug }
}
);
return data;
}
});
};

View File

@ -156,3 +156,39 @@ export type TGetAccessibleSecretsDTO = {
| ProjectPermissionSecretActions.DescribeSecret
| ProjectPermissionSecretActions.ReadValue;
};
export type TGetProjectOverview = {
projectId: string;
projectSlug: string;
};
export type TProjectOverview = {
accessControl: {
userCount: number;
machineIdentityCount: number;
groupCount: number;
};
secretsManagement: {
secretCount: number;
environmentCount: number;
pendingApprovalCount: number;
};
certificateManagement: {
internalCaCount: number;
externalCaCount: number;
expiryCount: number;
};
kms: {
keyCount: number;
kmipClientCount: number;
};
ssh: {
hostCount: number;
hostGroupCount: number;
};
secretScanning: {
dataSourceCount: number;
resourceCount: number;
findingCount: number;
};
};

View File

@ -20,7 +20,7 @@ export const ProjectGeneralLayout = () => {
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
Project Overview
Project Controls
</div>
<div className="flex-1">
<Menu>

View File

@ -55,7 +55,7 @@ export const ProjectLayout = () => {
const isKms = currentProductType === ProjectType.KMS;
const isSsh = currentProductType === ProjectType.SSH;
const isSecretScanning = currentProductType === ProjectType.SecretScanning;
const isOverview = !currentProductType;
return (
<>
<div
@ -83,6 +83,28 @@ export const ProjectLayout = () => {
>
<nav className="items-between flex h-full flex-col justify-between">
<Menu>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}
content="Project Overview"
position="right"
>
<Link
to="/projects/$projectId/overview"
params={{ projectId: currentWorkspace.id }}
>
<MenuItem
className="relative flex items-center gap-2 overflow-hidden rounded-none"
isSelected={isOverview}
leftIcon={<Lottie className="inline-block h-6 w-6 shrink-0" icon="home" />}
>
{isOverview && (
<div className="absolute left-0 top-0 h-full w-0.5 bg-primary" />
)}
Overview
</MenuItem>
</Link>
</ShouldWrap>
<ShouldWrap
wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed}

View File

@ -1,12 +1,35 @@
import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { Badge, Menu, MenuItem } from "@app/components/v2";
import {
ProjectPermissionSub,
useProjectPermission,
useSubscription,
useWorkspace
} from "@app/context";
import { ProjectPermissionSecretScanningFindingActions } from "@app/context/ProjectPermissionContext/types";
import { useGetSecretScanningUnresolvedFindingCount } from "@app/hooks/api/secretScanningV2";
export const SecretScanningLayout = () => {
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const { subscription } = useSubscription();
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(
currentWorkspace.id,
{
enabled:
subscription.secretScanning &&
permission.can(
ProjectPermissionSecretScanningFindingActions.Read,
ProjectPermissionSub.SecretScanningFindings
),
refetchInterval: 30000
}
);
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
@ -38,7 +61,18 @@ export const SecretScanningLayout = () => {
projectId: currentWorkspace.id
}}
>
{({ isActive }) => <MenuItem isSelected={isActive}>Findings</MenuItem>}
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="flex w-full items-center justify-between">
<span>Findings</span>
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="mr-2">
{unresolvedFindings}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/$projectId/secret-scanning/settings"

View File

@ -0,0 +1,318 @@
import { Fragment } from "react";
import { Helmet } from "react-helmet";
import {
faArrowRightArrowLeft,
faArrowUpRightFromSquare,
faBook,
faClock,
faCode,
faCopy,
faExpand,
faRotate,
faSearch,
faServer,
faShield,
faTerminal,
faUser,
faUserGroup,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ContentLoader, IconButton, PageHeader } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetProjectOverview } from "@app/hooks/api/dashboard/queries";
import { ProductCard } from "@app/pages/project/OverviewPage/components/ProductCard";
const DocLinks = [
{
label: "API Docs",
description: "API endpoints, authentication, and examples",
icon: faBook,
link: "https://infisical.com/docs/api-reference/overview/introduction"
},
{
label: "Access Control",
description: "Manage user permissions and resource-level security",
icon: faShield,
link: "https://infisical.com/docs/documentation/platform/access-controls/overview"
},
{
label: "CLI",
description: "Install, configure, and use command-line tools",
icon: faTerminal,
link: "https://infisical.com/docs/cli/overview"
},
{
label: "SDKs",
description: "Libraries and SDKs for popular programming languages",
icon: faCode,
link: "https://infisical.com/docs/sdks/overview"
},
{
label: "Machine Identities",
description: "Configure service accounts for automated workflows",
icon: faServer,
link: "https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities"
},
{
label: "Secret Syncs",
description: "Automatically sync secrets to external platforms",
icon: faArrowRightArrowLeft,
link: "https://infisical.com/docs/integrations/secret-syncs/overview"
},
{
label: "Secret Scanning",
description: "Detect and prevent secret exposure in code",
icon: faExpand,
link: "https://infisical.com/docs/documentation/platform/secret-scanning/overview"
},
{
label: "Secret Rotation",
description: "Automate periodic secret updates and rotation",
icon: faRotate,
link: "https://infisical.com/docs/documentation/platform/secret-rotation/overview"
}
];
export const OverviewPage = () => {
const { currentWorkspace } = useWorkspace();
const {
data: overview = {
accessControl: { userCount: 0, machineIdentityCount: 0, groupCount: 0 },
secretsManagement: { secretCount: 0, environmentCount: 0, pendingApprovalCount: 0 },
certificateManagement: {
internalCaCount: 0,
externalCaCount: 0,
expiryCount: 0
},
ssh: { hostCount: 0, hostGroupCount: 0 },
kms: { keyCount: 0, kmipClientCount: 0 },
secretScanning: {
dataSourceCount: 0,
resourceCount: 0,
findingCount: 0
}
},
isPending
} = useGetProjectOverview({
projectId: currentWorkspace.id,
projectSlug: currentWorkspace.slug
});
if (isPending) return <ContentLoader />;
const { secretsManagement, certificateManagement, kms, ssh, secretScanning } = overview;
return (
<div className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 p-4 pt-8">
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet>
<title>Project Overview | {currentWorkspace.name}</title>
</Helmet>
<div className="flex w-full max-w-7xl flex-col justify-evenly">
<PageHeader
title={
<div>
<span>{currentWorkspace.name}</span>
<div className="flex w-full flex-wrap items-center gap-2 text-xs font-normal">
<div className="flex items-center gap-1 normal-case text-mineshaft-300">
<span>Project Slug: {currentWorkspace.slug}</span>
<IconButton
onClick={() => {
navigator.clipboard.writeText(currentWorkspace.slug);
createNotification({
text: "Project slug copied to clipboard",
type: "info"
});
}}
variant="plain"
size="xs"
ariaLabel="Copy project slug"
>
<FontAwesomeIcon className="text-mineshaft-400" icon={faCopy} />
</IconButton>
</div>
<span className="text-mineshaft-400">|</span>
<div className="flex items-center gap-1 normal-case text-mineshaft-300">
<span>Project ID: {currentWorkspace.id}</span>
<IconButton
onClick={() => {
navigator.clipboard.writeText(currentWorkspace.id);
createNotification({
text: "Project ID copied to clipboard",
type: "info"
});
}}
variant="plain"
size="xs"
ariaLabel="Copy project ID"
>
<FontAwesomeIcon className="text-mineshaft-400" icon={faCopy} />
</IconButton>
</div>
</div>
</div>
}
>
<div className="mb-3 mt-auto flex flex-col gap-4 lg:flex-row lg:items-center">
{[
{ icon: faUser, count: overview.accessControl.userCount, label: "Members" },
{
icon: faServer,
count: overview.accessControl.machineIdentityCount,
label: "Machine Identities"
},
{ icon: faUserGroup, count: overview.accessControl.groupCount, label: "Groups" }
].map(({ icon, count, label }, index) => (
<Fragment key={`${index + 1}-label`}>
<div className="flex items-center gap-1.5 whitespace-nowrap text-sm text-mineshaft-300">
<FontAwesomeIcon icon={icon} />
<span>{count}</span>
<span>{label}</span>
</div>
<span className="hidden text-sm text-mineshaft-400 last:hidden lg:block">|</span>
</Fragment>
))}
</div>
</PageHeader>
<div className="w-full border-t border-mineshaft-600" />
<div className="flex flex-col">
<div className="mt-16">
<h3 className="mb-4 text-2xl">Products</h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<ProductCard
title="Secrets Management"
lottie="vault"
to="/projects/$projectId/secret-manager/overview"
items={[
{ label: `${secretsManagement.secretCount} Secrets`, key: "secretCount" },
{
label: `${secretsManagement.environmentCount} Environments`,
key: "environmentCount"
}
]}
badgeProps={
secretsManagement.pendingApprovalCount
? {
variant: "primary",
icon: faClock,
label: `${secretsManagement.pendingApprovalCount} Approval${secretsManagement.pendingApprovalCount > 1 ? "s" : ""}`,
tooltipContent: `${secretsManagement.pendingApprovalCount} Pending Approval${secretsManagement.pendingApprovalCount > 1 ? "s" : ""}`
}
: undefined
}
/>
<ProductCard
title="Certificate Management"
lottie="note"
to="/projects/$projectId/cert-manager/subscribers"
items={[
{
label: `${certificateManagement.internalCaCount} Internal CAs`,
key: "internalCaCount"
},
{
label: `${certificateManagement.externalCaCount} External CAs`,
key: "externalCaCount"
}
]}
badgeProps={
certificateManagement.expiryCount
? {
variant: "primary",
icon: faWarning,
label: `${certificateManagement.expiryCount} Expiring`,
tooltipContent: `${certificateManagement.expiryCount} Certificate${certificateManagement.expiryCount > 1 ? "s" : ""} Are About To Expire`
}
: undefined
}
/>
<ProductCard
title="KMS"
lottie="unlock"
to="/projects/$projectId/kms/overview"
items={[
{ label: `${kms.keyCount} Keys`, key: "keyCount" },
{
label: `${kms.kmipClientCount} KMIP Clients`,
key: "kmipClientCount"
}
]}
/>
<ProductCard
title="SSH"
lottie="terminal"
to="/projects/$projectId/ssh/overview"
items={[
{ label: `${ssh.hostCount} Hosts`, key: "hostCount" },
{
label: `${ssh.hostGroupCount} Host Groups`,
key: "hostGroupCount"
}
]}
/>
<ProductCard
title="Secret Scanning"
lottie="secret-scan"
to="/projects/$projectId/secret-scanning/data-sources"
items={[
{
label: `${secretScanning.dataSourceCount} Data Sources`,
key: "dataSourceCount"
},
{
label: `${secretScanning.resourceCount} Resources`,
key: "resourceCount"
}
]}
badgeProps={
secretScanning.findingCount
? {
variant: "primary",
icon: faSearch,
label: `${secretScanning.findingCount} Finding${secretScanning.findingCount > 1 ? "s" : ""}`,
tooltipContent: `${secretScanning.findingCount} Unresolved Finding${secretScanning.findingCount > 1 ? "s" : ""}`
}
: undefined
}
/>
</div>
</div>
<div className="mt-20">
<h3 className="mb-4 text-xl">Documentation</h3>
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
{DocLinks.map(({ label, description, icon, link }) => (
<a
key={label}
href={link}
target="_blank"
rel="noopener noreferrer"
className="rounded border border-mineshaft-600 bg-mineshaft-800 p-4 transition-transform duration-100 hover:scale-[103%] hover:bg-mineshaft-700"
>
<div className="w-full items-start">
<div className="flex w-full items-center">
<FontAwesomeIcon className="mr-2 text-bunker-200" icon={icon} />
<span>{label}</span>
<FontAwesomeIcon
className="ml-auto text-bunker-400"
size="sm"
icon={faArrowUpRightFromSquare}
/>
</div>
<div>
<p className="text-sm text-bunker-300">{description}</p>
</div>
</div>
</a>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,63 @@
import { Fragment } from "react";
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, LinkProps } from "@tanstack/react-router";
import { Badge, Lottie, Tooltip } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
type Props = {
to: LinkProps["to"];
lottie: string;
title: string;
badgeProps?: {
variant: BadgeProps["variant"];
label: string;
tooltipContent: string;
icon: IconDefinition;
};
items: {
key: string;
label: string;
}[];
};
export const ProductCard = ({ to, items, title, badgeProps, lottie }: Props) => {
return (
<Link
to={to}
className="overflow-clip rounded border border-l-[4px] border-mineshaft-600 border-l-primary/75 bg-mineshaft-800 p-4 transition-transform duration-100 hover:scale-[103%] hover:border-l-primary hover:bg-mineshaft-700"
>
<div className="flex w-full items-center gap-3">
<div className="rounded border border-mineshaft-500 bg-mineshaft-600 p-1.5 shadow-inner">
<Lottie className="h-[1.75rem] w-[1.75rem] shrink-0" icon={lottie} />
</div>
<div className="-mt-0.5 flex w-full flex-col">
<div className="flex w-full items-center">
<span className="text-xl">{title}</span>
{badgeProps && (
<Tooltip className="max-w-sm" content={badgeProps.tooltipContent}>
<div className="ml-auto">
<Badge className="mt-0.5 flex items-center gap-1.5" variant={badgeProps.variant}>
<FontAwesomeIcon className="text-yellow" icon={badgeProps.icon} />
<span>{badgeProps.label}</span>
</Badge>
</div>
</Tooltip>
)}
</div>
<div className="-mt-0.5 flex items-center gap-2">
{items.map((item) => (
<Fragment key={item.key}>
<div className="flex items-center gap-1.5 whitespace-nowrap text-sm text-mineshaft-300">
{item.label}
</div>
<span className="text-sm text-mineshaft-400 last:hidden">|</span>
</Fragment>
))}
</div>
</div>
</div>
</Link>
);
};

View File

@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview"
)({
component: OverviewPage,
beforeLoad: ({ context }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Overview"
}
]
};
}
});

View File

@ -18,7 +18,7 @@ export const SettingsPage = () => {
<div className="w-full max-w-7xl">
<PageHeader
title={t("settings.project.title")}
description="Configure your project details and project-specific settings"
description="Configure your project details and project-specific settings"
/>
<Tabs defaultValue={tabs[0].key}>
<TabList>

View File

@ -69,6 +69,7 @@ import { Route as organizationSecretSharingPageRouteImport } from './pages/organ
import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
import { Route as projectLayoutGeneralImport } from './pages/project/layout-general'
import { Route as projectOverviewPageRouteImport } from './pages/project/OverviewPage/route'
import { Route as organizationSettingsPageOauthCallbackPageRouteImport } from './pages/organization/SettingsPage/OauthCallbackPage/route'
import { Route as sshLayoutImport } from './pages/ssh/layout'
import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout'
@ -810,6 +811,12 @@ const projectLayoutGeneralRoute = projectLayoutGeneralImport.update({
getParentRoute: () => projectLayoutRoute,
} as any)
const projectOverviewPageRouteRoute = projectOverviewPageRouteImport.update({
id: '/overview',
path: '/overview',
getParentRoute: () => projectLayoutRoute,
} as any)
const organizationSettingsPageOauthCallbackPageRouteRoute =
organizationSettingsPageOauthCallbackPageRouteImport.update({
id: '/oauth/callback',
@ -2348,6 +2355,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationSettingsPageOauthCallbackPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview'
path: '/overview'
fullPath: '/projects/$projectId/overview'
preLoaderRoute: typeof projectOverviewPageRouteImport
parentRoute: typeof projectLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout': {
id: '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout'
path: ''
@ -4053,6 +4067,7 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutSshRout
)
interface projectLayoutRouteChildren {
projectOverviewPageRouteRoute: typeof projectOverviewPageRouteRoute
projectLayoutGeneralRoute: typeof projectLayoutGeneralRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
@ -4063,6 +4078,7 @@ interface projectLayoutRouteChildren {
}
const projectLayoutRouteChildren: projectLayoutRouteChildren = {
projectOverviewPageRouteRoute: projectOverviewPageRouteRoute,
projectLayoutGeneralRoute: projectLayoutGeneralRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute:
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren,
@ -4371,6 +4387,7 @@ export interface FileRoutesByFullPath {
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
'/projects/$projectId/overview': typeof projectOverviewPageRouteRoute
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
'/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren
@ -4564,6 +4581,7 @@ export interface FileRoutesByTo {
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
'/projects/$projectId/overview': typeof projectOverviewPageRouteRoute
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
'/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren
@ -4766,6 +4784,7 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout': typeof projectLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
'/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview': typeof projectOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout': typeof projectLayoutGeneralRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
@ -4975,6 +4994,7 @@ export interface FileRouteTypes {
| '/admin/resources/user-identities'
| '/secret-manager/$projectId/approval'
| '/organization/settings/oauth/callback'
| '/projects/$projectId/overview'
| '/projects/$projectId/cert-manager'
| '/projects/$projectId/integrations'
| '/projects/$projectId/kms'
@ -5167,6 +5187,7 @@ export interface FileRouteTypes {
| '/admin/resources/user-identities'
| '/secret-manager/$projectId/approval'
| '/organization/settings/oauth/callback'
| '/projects/$projectId/overview'
| '/projects/$projectId/cert-manager'
| '/projects/$projectId/integrations'
| '/projects/$projectId/kms'
@ -5367,6 +5388,7 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings/oauth/callback'
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout'
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager'
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations'
@ -5903,6 +5925,7 @@ export const routeTree = rootRoute
"filePath": "project/layout.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId",
"children": [
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/cert-manager",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations",
@ -5920,6 +5943,10 @@ export const routeTree = rootRoute
"filePath": "organization/SettingsPage/OauthCallbackPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/settings"
},
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/overview": {
"filePath": "project/OverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout"
},
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout": {
"filePath": "project/layout-general.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout",

View File

@ -392,6 +392,7 @@ export const routes = rootRoute("root.tsx", [
]),
route("/projects/$projectId", [
layout("project-layout", "project/layout.tsx", [
route("/overview", "project/OverviewPage/route.tsx"),
projectGeneralRoutes,
secretManagerRoutes,
secretManagerIntegrationsRedirect,