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 { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
import { isValidIp } from "@app/lib/ip"; import { isValidIp } from "@app/lib/ip";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { OrgServiceActor } from "@app/lib/types";
import { isFQDN } from "@app/lib/validator/validate-url"; import { isFQDN } from "@app/lib/validator/validate-url";
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns"; import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; 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 { return {
createKmipClient, createKmipClient,
updateKmipClient, updateKmipClient,
@ -806,6 +827,7 @@ export const kmipServiceFactory = ({
generateOrgKmipServerCertificate, generateOrgKmipServerCertificate,
getOrgKmip, getOrgKmip,
getServerCertificateBySerialNumber, getServerCertificateBySerialNumber,
registerServer registerServer,
getProjectClientCount
}; };
}; };

View File

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

View File

@ -881,6 +881,47 @@ export const secretScanningV2ServiceFactory = ({
return config; 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 { return {
listSecretScanningDataSourceOptions, listSecretScanningDataSourceOptions,
listSecretScanningDataSourcesByProjectId, listSecretScanningDataSourcesByProjectId,
@ -900,6 +941,7 @@ export const secretScanningV2ServiceFactory = ({
updateSecretScanningFindingById, updateSecretScanningFindingById,
findSecretScanningConfigByProjectId, findSecretScanningConfigByProjectId,
upsertSecretScanningConfig, upsertSecretScanningConfig,
getProjectResourcesCount,
github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue), github: githubSecretScanningService(secretScanningV2DAL, secretScanningV2Queue),
bitbucket: bitbucketSecretScanningService(secretScanningV2DAL, secretScanningV2Queue, kmsService) 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 { 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 { TSshHostGroupMembershipDALFactory } from "@app/ee/services/ssh-host-group/ssh-host-group-membership-dal";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@ -414,6 +415,26 @@ export const sshHostGroupServiceFactory = ({
return { sshHostGroup, sshHost }; 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 { return {
createSshHostGroup, createSshHostGroup,
getSshHostGroup, getSshHostGroup,
@ -421,6 +442,7 @@ export const sshHostGroupServiceFactory = ({
updateSshHostGroup, updateSshHostGroup,
listSshHostGroupHosts, listSshHostGroupHosts,
addHostToSshHostGroup, 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 { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
import { PgSqlLock } from "@app/keystore/keystore"; import { PgSqlLock } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
@ -60,6 +61,7 @@ type TSshHostServiceFactoryDep = {
| "findOne" | "findOne"
| "findSshHostByIdWithLoginMappings" | "findSshHostByIdWithLoginMappings"
| "findUserAccessibleSshHosts" | "findUserAccessibleSshHosts"
| "find"
>; >;
sshHostLoginUserDAL: TSshHostLoginUserDALFactory; sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory; sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
@ -637,6 +639,26 @@ export const sshHostServiceFactory = ({
return publicKey; 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 { return {
listSshHosts, listSshHosts,
createSshHost, createSshHost,
@ -646,6 +668,7 @@ export const sshHostServiceFactory = ({
issueSshHostUserCert, issueSshHostUserCert,
issueSshHostHostCert, issueSshHostHostCert,
getSshHostUserCaPk, getSshHostUserCaPk,
getSshHostHostCaPk getSshHostHostCaPk,
getProjectHostCount
}; };
}; };

View File

@ -12,6 +12,11 @@ type TKnexDynamicPrimitiveOperator<T extends object> =
operator: "notIn"; operator: "notIn";
value: string[]; value: string[];
field: Extract<keyof T, string>; field: Extract<keyof T, string>;
}
| {
operator: "lte";
value: Date;
field: Extract<keyof T, string>;
}; };
type TKnexDynamicInOperator<T extends object> = { type TKnexDynamicInOperator<T extends object> = {
@ -82,6 +87,10 @@ export const buildDynamicKnexQuery = <T extends object>(
}); });
break; break;
} }
case "lte": {
void queryBuilder.where(filterAst.field, "<=", filterAst.value);
break;
}
default: default:
throw new UnauthorizedError({ message: `Invalid knex dynamic operator: ${filterAst.operator}` }); 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 { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { OrderByDirection } from "@app/lib/types"; 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 { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -1354,4 +1354,128 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
return { secrets }; 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" | "findByIdWithAssociatedCa"
| "findWithAssociatedCa" | "findWithAssociatedCa"
| "findByNameAndProjectIdWithAssociatedCa" | "findByNameAndProjectIdWithAssociatedCa"
| "find"
>; >;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">; externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update" | "find">;
internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory; internalCertificateAuthorityService: TInternalCertificateAuthorityServiceFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@ -382,11 +383,36 @@ export const certificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "Invalid certificate authority type" }); 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 { return {
createCertificateAuthority, createCertificateAuthority,
findCertificateAuthorityByNameAndProjectId, findCertificateAuthorityByNameAndProjectId,
listCertificateAuthoritiesByProjectId, listCertificateAuthoritiesByProjectId,
updateCertificateAuthority, updateCertificateAuthority,
deleteCertificateAuthority deleteCertificateAuthority,
getProjectCertificateAuthorityCount
}; };
}; };

View File

@ -9,6 +9,7 @@ import {
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-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 { return {
getCert, getCert,
getCertPrivateKey, getCertPrivateKey,
@ -607,6 +640,7 @@ export const certificateServiceFactory = ({
revokeCert, revokeCert,
getCertBody, getCertBody,
importCert, 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 { return {
createCmek, createCmek,
updateCmekById, updateCmekById,
@ -387,6 +407,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService }: TC
cmekSign, cmekSign,
cmekVerify, cmekVerify,
listSigningAlgorithms, 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 { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { OrgServiceActor } from "@app/lib/types";
import { isUuidV4 } from "@app/lib/validator"; import { isUuidV4 } from "@app/lib/validator";
import { TGroupDALFactory } from "../../ee/services/group/group-dal"; import { TGroupDALFactory } from "../../ee/services/group/group-dal";
@ -33,7 +34,10 @@ import {
} from "./group-project-types"; } from "./group-project-types";
type TGroupProjectServiceFactoryDep = { type TGroupProjectServiceFactoryDep = {
groupProjectDAL: Pick<TGroupProjectDALFactory, "findOne" | "transaction" | "create" | "delete" | "findByProjectId">; groupProjectDAL: Pick<
TGroupProjectDALFactory,
"findOne" | "transaction" | "create" | "delete" | "findByProjectId" | "find"
>;
groupProjectMembershipRoleDAL: Pick< groupProjectMembershipRoleDAL: Pick<
TGroupProjectMembershipRoleDALFactory, TGroupProjectMembershipRoleDALFactory,
"create" | "transaction" | "insertMany" | "delete" "create" | "transaction" | "insertMany" | "delete"
@ -508,12 +512,33 @@ export const groupProjectServiceFactory = ({
return { users: members, totalCount }; 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 { return {
addGroupToProject, addGroupToProject,
updateGroupInProject, updateGroupInProject,
removeGroupFromProject, removeGroupFromProject,
listGroupsInProject, listGroupsInProject,
getGroupInProject, 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 { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { OrgServiceActor } from "@app/lib/types";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
@ -403,12 +404,33 @@ export const identityProjectServiceFactory = ({
return identityMembership; 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 { return {
createProjectIdentity, createProjectIdentity,
updateProjectIdentity, updateProjectIdentity,
deleteProjectIdentity, deleteProjectIdentity,
listProjectIdentities, listProjectIdentities,
getProjectIdentityByIdentityId, 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 { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { OrgServiceActor } from "@app/lib/types";
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
@ -567,6 +568,35 @@ export const projectMembershipServiceFactory = ({
return deletedMembership; 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 { return {
getProjectMemberships, getProjectMemberships,
getProjectMembershipByUsername, getProjectMembershipByUsername,
@ -575,6 +605,7 @@ export const projectMembershipServiceFactory = ({
deleteProjectMembership, // TODO: Remove this deleteProjectMembership, // TODO: Remove this
addUsersToProject, addUsersToProject,
leaveProject, leaveProject,
getProjectMembershipById getProjectMembershipById,
getProjectMembershipCount
}; };
}; };

View File

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

View File

@ -26,6 +26,7 @@ import { diff, groupBy } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex"; import { setKnexStringValue } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
@ -86,7 +87,7 @@ type TSecretV2BridgeServiceFactoryDep = {
secretTagDAL: TSecretTagDALFactory; secretTagDAL: TSecretTagDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">; folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
folderDAL: Pick< folderDAL: Pick<
TSecretFolderDALFactory, TSecretFolderDALFactory,
| "findBySecretPath" | "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 { return {
createSecret, createSecret,
deleteSecret, deleteSecret,
@ -2933,6 +2967,7 @@ export const secretV2BridgeServiceFactory = ({
getSecretsByFolderMappings, getSecretsByFolderMappings,
getSecretById, getSecretById,
getAccessibleSecrets, 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 { return {
attachTags, attachTags,
detachTags, detachTags,
@ -3371,6 +3385,7 @@ export const secretServiceFactory = ({
getSecretByIdRaw, getSecretByIdRaw,
getAccessibleSecrets, getAccessibleSecrets,
getSecretVersionsV2ByIds, 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; return project;
}; };
export const getProjectHomePage = (type: ProjectType) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars
switch (type) { export const getProjectHomePage = (_type: ProjectType) => {
case ProjectType.CertificateManager: return "/projects/$projectId/overview";
return `/projects/$projectId/${type}/subscribers` as const;
case ProjectType.SecretScanning: // switch (type) {
return `/projects/$projectId/${type}/data-sources` as const; // case ProjectType.CertificateManager:
default: // return `/projects/$projectId/${type}/subscribers` as const;
return `/projects/$projectId/${type}/overview` 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) => { export const getProjectTitle = (type: ProjectType) => {

View File

@ -15,7 +15,9 @@ import {
TGetDashboardProjectSecretsByKeys, TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO, TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO, TGetDashboardProjectSecretsOverviewDTO,
TGetDashboardProjectSecretsQuickSearchDTO TGetDashboardProjectSecretsQuickSearchDTO,
TGetProjectOverview,
TProjectOverview
} from "@app/hooks/api/dashboard/types"; } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
@ -72,7 +74,13 @@ export const dashboardKeys = {
...dashboardKeys.all(), ...dashboardKeys.all(),
"accessible-secrets", "accessible-secrets",
{ projectId, secretPath, environment, filterByAction } { projectId, secretPath, environment, filterByAction }
] as const ] as const,
getProjectOverview: ({ projectId, projectSlug }: TGetProjectOverview) => [
...dashboardKeys.all(),
"project-overview",
projectId,
projectSlug
]
}; };
export const fetchProjectSecretsOverview = async ({ export const fetchProjectSecretsOverview = async ({
@ -464,3 +472,35 @@ export const useGetAccessibleSecrets = ({
fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction, recursive }) 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.DescribeSecret
| ProjectPermissionSecretActions.ReadValue; | 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]"> <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"> <div className="border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
Project Overview Project Controls
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Menu> <Menu>

View File

@ -55,7 +55,7 @@ export const ProjectLayout = () => {
const isKms = currentProductType === ProjectType.KMS; const isKms = currentProductType === ProjectType.KMS;
const isSsh = currentProductType === ProjectType.SSH; const isSsh = currentProductType === ProjectType.SSH;
const isSecretScanning = currentProductType === ProjectType.SecretScanning; const isSecretScanning = currentProductType === ProjectType.SecretScanning;
const isOverview = !currentProductType;
return ( return (
<> <>
<div <div
@ -83,6 +83,28 @@ export const ProjectLayout = () => {
> >
<nav className="items-between flex h-full flex-col justify-between"> <nav className="items-between flex h-full flex-col justify-between">
<Menu> <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 <ShouldWrap
wrapper={Tooltip} wrapper={Tooltip}
isWrapped={sidebarStyle === SidebarStyle.Collapsed} isWrapped={sidebarStyle === SidebarStyle.Collapsed}

View File

@ -1,12 +1,35 @@
import { Link, Outlet } from "@tanstack/react-router"; import { Link, Outlet } from "@tanstack/react-router";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Menu, MenuItem } from "@app/components/v2"; import { Badge, Menu, MenuItem } from "@app/components/v2";
import { useWorkspace } from "@app/context"; 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 = () => { export const SecretScanningLayout = () => {
const { currentWorkspace } = useWorkspace(); 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 ( return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex"> <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"> <div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
@ -38,7 +61,18 @@ export const SecretScanningLayout = () => {
projectId: currentWorkspace.id 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>
<Link <Link
to="/projects/$projectId/secret-scanning/settings" 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"> <div className="w-full max-w-7xl">
<PageHeader <PageHeader
title={t("settings.project.title")} 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}> <Tabs defaultValue={tabs[0].key}>
<TabList> <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 organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route' import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
import { Route as projectLayoutGeneralImport } from './pages/project/layout-general' 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 organizationSettingsPageOauthCallbackPageRouteImport } from './pages/organization/SettingsPage/OauthCallbackPage/route'
import { Route as sshLayoutImport } from './pages/ssh/layout' import { Route as sshLayoutImport } from './pages/ssh/layout'
import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout' import { Route as secretScanningLayoutImport } from './pages/secret-scanning/layout'
@ -810,6 +811,12 @@ const projectLayoutGeneralRoute = projectLayoutGeneralImport.update({
getParentRoute: () => projectLayoutRoute, getParentRoute: () => projectLayoutRoute,
} as any) } as any)
const projectOverviewPageRouteRoute = projectOverviewPageRouteImport.update({
id: '/overview',
path: '/overview',
getParentRoute: () => projectLayoutRoute,
} as any)
const organizationSettingsPageOauthCallbackPageRouteRoute = const organizationSettingsPageOauthCallbackPageRouteRoute =
organizationSettingsPageOauthCallbackPageRouteImport.update({ organizationSettingsPageOauthCallbackPageRouteImport.update({
id: '/oauth/callback', id: '/oauth/callback',
@ -2348,6 +2355,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationSettingsPageOauthCallbackPageRouteImport preLoaderRoute: typeof organizationSettingsPageOauthCallbackPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsImport 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': { '/_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' id: '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout'
path: '' path: ''
@ -4053,6 +4067,7 @@ const AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutSshRout
) )
interface projectLayoutRouteChildren { interface projectLayoutRouteChildren {
projectOverviewPageRouteRoute: typeof projectOverviewPageRouteRoute
projectLayoutGeneralRoute: typeof projectLayoutGeneralRouteWithChildren projectLayoutGeneralRoute: typeof projectLayoutGeneralRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
@ -4063,6 +4078,7 @@ interface projectLayoutRouteChildren {
} }
const projectLayoutRouteChildren: projectLayoutRouteChildren = { const projectLayoutRouteChildren: projectLayoutRouteChildren = {
projectOverviewPageRouteRoute: projectOverviewPageRouteRoute,
projectLayoutGeneralRoute: projectLayoutGeneralRouteWithChildren, projectLayoutGeneralRoute: projectLayoutGeneralRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute: AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRoute:
AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren, AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren,
@ -4371,6 +4387,7 @@ export interface FileRoutesByFullPath {
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute '/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute '/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute '/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
'/projects/$projectId/overview': typeof projectOverviewPageRouteRoute
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren '/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren '/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
'/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren '/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren
@ -4564,6 +4581,7 @@ export interface FileRoutesByTo {
'/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute '/admin/resources/user-identities': typeof adminUserIdentitiesResourcesPageRouteRoute
'/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute '/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute
'/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute '/organization/settings/oauth/callback': typeof organizationSettingsPageOauthCallbackPageRouteRoute
'/projects/$projectId/overview': typeof projectOverviewPageRouteRoute
'/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren '/projects/$projectId/cert-manager': typeof certManagerLayoutRouteWithChildren
'/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren '/projects/$projectId/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
'/projects/$projectId/kms': typeof kmsLayoutRouteWithChildren '/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/projects/$projectId/_project-layout': typeof projectLayoutRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval': typeof secretManagerRedirectsRedirectApprovalPageRoute '/_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/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/_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/cert-manager': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutCertManagerRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations': typeof AuthenticateInjectOrgDetailsOrgLayoutProjectsProjectIdProjectLayoutIntegrationsRouteWithChildren
@ -4975,6 +4994,7 @@ export interface FileRouteTypes {
| '/admin/resources/user-identities' | '/admin/resources/user-identities'
| '/secret-manager/$projectId/approval' | '/secret-manager/$projectId/approval'
| '/organization/settings/oauth/callback' | '/organization/settings/oauth/callback'
| '/projects/$projectId/overview'
| '/projects/$projectId/cert-manager' | '/projects/$projectId/cert-manager'
| '/projects/$projectId/integrations' | '/projects/$projectId/integrations'
| '/projects/$projectId/kms' | '/projects/$projectId/kms'
@ -5167,6 +5187,7 @@ export interface FileRouteTypes {
| '/admin/resources/user-identities' | '/admin/resources/user-identities'
| '/secret-manager/$projectId/approval' | '/secret-manager/$projectId/approval'
| '/organization/settings/oauth/callback' | '/organization/settings/oauth/callback'
| '/projects/$projectId/overview'
| '/projects/$projectId/cert-manager' | '/projects/$projectId/cert-manager'
| '/projects/$projectId/integrations' | '/projects/$projectId/integrations'
| '/projects/$projectId/kms' | '/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/projects/$projectId/_project-layout'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/approval' | '/_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/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/_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/cert-manager'
| '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations' | '/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations'
@ -5903,6 +5925,7 @@ export const routeTree = rootRoute
"filePath": "project/layout.tsx", "filePath": "project/layout.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId", "parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId",
"children": [ "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/_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/cert-manager",
"/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/integrations", "/_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", "filePath": "organization/SettingsPage/OauthCallbackPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/settings" "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": { "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout/_project-general-layout": {
"filePath": "project/layout-general.tsx", "filePath": "project/layout-general.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/$projectId/_project-layout", "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", [ route("/projects/$projectId", [
layout("project-layout", "project/layout.tsx", [ layout("project-layout", "project/layout.tsx", [
route("/overview", "project/OverviewPage/route.tsx"),
projectGeneralRoutes, projectGeneralRoutes,
secretManagerRoutes, secretManagerRoutes,
secretManagerIntegrationsRedirect, secretManagerIntegrationsRedirect,