From 69bf9dc20f48d4bc3abdeff22e2f6a42cb6be61f Mon Sep 17 00:00:00 2001 From: = Date: Wed, 11 Dec 2024 15:28:46 +0530 Subject: [PATCH] feat: completed migration --- ...0241205160300_project-split-to-products.ts | 242 +++- backend/src/db/schemas/models.ts | 1 + backend/src/lib/api-docs/constants.ts | 3 +- backend/src/server/routes/sanitizedSchemas.ts | 1 + .../src/server/routes/v1/project-router.ts | 7 +- .../server/routes/v2/organization-router.ts | 4 +- backend/src/services/org/org-service.ts | 27 +- backend/src/services/org/org-types.ts | 2 + backend/src/services/project/project-dal.ts | 31 +- .../src/services/project/project-service.ts | 5 +- backend/src/services/project/project-types.ts | 1 + .../v2/projects/NewProjectModal.tsx | 19 +- .../WorkspaceContext/WorkspaceContext.tsx | 1 + frontend/src/helpers/project.ts | 4 +- .../src/hooks/api/migration/mutations.tsx | 3 +- .../src/hooks/api/workspace/mutations.tsx | 6 +- frontend/src/hooks/api/workspace/queries.tsx | 130 +- .../src/hooks/api/workspace/query-keys.tsx | 3 +- frontend/src/hooks/api/workspace/types.ts | 8 + frontend/src/layouts/AppLayout/AppLayout.tsx | 32 +- .../AppLayout/components/NavBar/NavBar.tsx | 343 ------ .../ProjectSelect/ProjectSelect.tsx | 4 +- .../pages/org/[id]/cert-manager/overview.tsx | 8 + frontend/src/pages/org/[id]/cmek/overview.tsx | 8 + .../src/pages/org/[id]/overview/index.tsx | 1036 +--------------- .../org/[id]/secret-manager/overview.tsx | 1059 +++++++++++++++++ .../OrgMembersSection/AddOrgMemberModal.tsx | 4 +- .../SecretV2MigrationSection.tsx | 4 +- 28 files changed, 1512 insertions(+), 1484 deletions(-) delete mode 100644 frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx create mode 100644 frontend/src/pages/org/[id]/cert-manager/overview.tsx create mode 100644 frontend/src/pages/org/[id]/cmek/overview.tsx create mode 100644 frontend/src/pages/org/[id]/secret-manager/overview.tsx diff --git a/backend/src/db/migrations/20241205160300_project-split-to-products.ts b/backend/src/db/migrations/20241205160300_project-split-to-products.ts index a7e0c2268..bf572edfc 100644 --- a/backend/src/db/migrations/20241205160300_project-split-to-products.ts +++ b/backend/src/db/migrations/20241205160300_project-split-to-products.ts @@ -1,13 +1,248 @@ import { Knex } from "knex"; +import { v4 as uuidV4 } from "uuid"; +import slugify from "@sindresorhus/slugify"; -import { TableName } from "../schemas"; +import { ProjectType, TableName } from "../schemas"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +/* eslint-disable no-await-in-loop,no-param-reassign,@typescript-eslint/ban-ts-comment */ +const newProject = async (knex: Knex, projectId: string, projectType: ProjectType) => { + const newProjectId = uuidV4(); + const project = await knex(TableName.Project).where("id", projectId).first(); + await knex(TableName.Project).insert({ + ...project, + type: projectType, + // @ts-ignore id is required + id: newProjectId, + slug: slugify(`${project?.name}-${alphaNumericNanoId(4)}`) + }); + + const customRoleMapping: Record = {}; + const projectCustomRoles = await knex(TableName.ProjectRoles).where("projectId", projectId); + if (projectCustomRoles.length) { + await knex(TableName.ProjectRoles).insert( + projectCustomRoles.map((el) => { + const id = uuidV4(); + customRoleMapping[el.id] = id; + el.id = id; + el.projectId = newProjectId; + el.permissions = el.permissions ? JSON.stringify(el.permissions) : el.permissions; + return el; + }) + ); + } + const groupMembershipMapping: Record = {}; + const groupMemberships = await knex(TableName.GroupProjectMembership).where("projectId", projectId); + if (groupMemberships.length) { + await knex(TableName.GroupProjectMembership).insert( + groupMemberships.map((el) => { + const id = uuidV4(); + groupMembershipMapping[el.id] = id; + el.id = id; + el.projectId = newProjectId; + return el; + }) + ); + } + + const groupMembershipRoles = await knex(TableName.GroupProjectMembershipRole).whereIn( + "projectMembershipId", + groupMemberships.map((el) => el.id) + ); + if (groupMembershipRoles.length) { + await knex(TableName.GroupProjectMembershipRole).insert( + groupMembershipRoles.map((el) => { + const id = uuidV4(); + el.id = id; + el.projectMembershipId = groupMembershipMapping[el.id]; + el.customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return el; + }) + ); + } + + const identityProjectMembershipMapping: Record = {}; + const identities = await knex(TableName.IdentityProjectMembership).where("projectId", projectId); + if (identities.length) { + await knex(TableName.IdentityProjectMembership).insert( + identities.map((el) => { + const id = uuidV4(); + identityProjectMembershipMapping[el.id] = id; + el.id = id; + el.projectId = newProjectId; + return el; + }) + ); + } + + const identitiesRoles = await knex(TableName.IdentityProjectMembershipRole).whereIn( + "projectMembershipId", + identities.map((el) => el.id) + ); + if (identitiesRoles.length) { + await knex(TableName.IdentityProjectMembershipRole).insert( + identitiesRoles.map((el) => { + const id = uuidV4(); + el.id = id; + el.projectMembershipId = identityProjectMembershipMapping[el.projectMembershipId]; + el.customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return el; + }) + ); + } + + const projectMembershipMapping: Record = {}; + const projectUserMembers = await knex(TableName.ProjectMembership).where("projectId", projectId); + if (projectUserMembers.length) { + await knex(TableName.ProjectMembership).insert( + projectUserMembers.map((el) => { + const id = uuidV4(); + projectMembershipMapping[el.id] = id; + el.id = id; + el.projectId = newProjectId; + return el; + }) + ); + } + const membershipRoles = await knex(TableName.ProjectUserMembershipRole).whereIn( + "projectMembershipId", + projectUserMembers.map((el) => el.id) + ); + if (membershipRoles.length) { + await knex(TableName.ProjectUserMembershipRole).insert( + membershipRoles.map((el) => { + const id = uuidV4(); + el.id = id; + el.projectMembershipId = projectMembershipMapping[el.projectMembershipId]; + el.customRoleId = el.customRoleId ? customRoleMapping[el.customRoleId] : el.customRoleId; + return el; + }) + ); + } + + const kmsKeys = await knex(TableName.KmsKey).where("projectId", projectId).andWhere("isReserved", true); + if (kmsKeys.length) { + await knex(TableName.KmsKey).insert( + kmsKeys.map((el) => { + const id = uuidV4(); + el.id = id; + el.projectId = newProjectId; + el.slug = slugify(alphaNumericNanoId(8).toLowerCase()); + return el; + }) + ); + } + const projectBot = await knex(TableName.ProjectBot).where("projectId", projectId).first(); + if (projectBot) { + const newProjectBot = { ...projectBot, id: uuidV4(), projectId: newProjectId }; + await knex(TableName.ProjectBot).insert(newProjectBot); + } + + const projectKeys = await knex(TableName.ProjectKeys).where("projectId", projectId); + if (projectKeys.length) { + await knex(TableName.ProjectKeys).insert( + projectKeys.map((el) => { + const id = uuidV4(); + el.id = id; + el.projectId = newProjectId; + return el; + }) + ); + } + + const serviceTokens = await knex(TableName.ServiceToken).where("projectId", projectId); + if (serviceTokens.length) { + await knex(TableName.ServiceToken).insert( + serviceTokens.map((el) => { + el.id = uuidV4(); + el.projectId = projectId; + el.scopes = el.scopes ? JSON.stringify(el.scopes) : el.scopes; + return el; + }) + ); + } + return newProjectId; +}; +/* eslint-enable */ + +const BATCH_SIZE = 500; export async function up(knex: Knex): Promise { + const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds); + if (!hasSplitMappingTable) { + await knex.schema.createTable(TableName.ProjectSplitBackfillIds, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("sourceProjectId", 36).notNullable(); + t.foreign("sourceProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + t.string("destinationProjectType").notNullable(); + t.string("destinationProjectId", 36).notNullable(); + t.foreign("destinationProjectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + }); + } + const hasTypeColumn = await knex.schema.hasColumn(TableName.Project, "type"); if (!hasTypeColumn) { await knex.schema.alterTable(TableName.Project, (t) => { t.string("type"); }); + + let projectsToBeTyped; + do { + // eslint-disable-next-line no-await-in-loop + projectsToBeTyped = await knex(TableName.Project).whereNull("type").limit(BATCH_SIZE).select("id"); + if (projectsToBeTyped.length) { + // eslint-disable-next-line no-await-in-loop + await knex(TableName.Project) + .whereIn( + "id", + projectsToBeTyped.map((el) => el.id) + ) + .update({ type: ProjectType.SecretManager }); + } + } while (projectsToBeTyped.length > 0); + + const projectsWithCertificates = await knex(TableName.CertificateAuthority) + .distinct("projectId") + .select("projectId"); + /* eslint-disable no-await-in-loop,no-param-reassign */ + for (const { projectId } of projectsWithCertificates) { + const newProjectId = await newProject(knex, projectId, ProjectType.CertificateManager); + await knex(TableName.CertificateAuthority).where("projectId", projectId).update({ projectId: newProjectId }); + await knex(TableName.PkiAlert).where("projectId", projectId).update({ projectId: newProjectId }); + await knex(TableName.PkiCollection).where("projectId", projectId).update({ projectId: newProjectId }); + await knex(TableName.ProjectSplitBackfillIds).insert({ + sourceProjectId: projectId, + destinationProjectType: ProjectType.CertificateManager, + destinationProjectId: newProjectId + }); + } + + const projectsWithCmek = await knex(TableName.KmsKey) + .where("isReserved", false) + .whereNotNull("projectId") + .distinct("projectId") + .select("projectId"); + for (const { projectId } of projectsWithCmek) { + if (projectId) { + const newProjectId = await newProject(knex, projectId, ProjectType.Cmek); + await knex(TableName.KmsKey) + .where({ + isReserved: false, + projectId + }) + .update({ projectId: newProjectId }); + await knex(TableName.ProjectSplitBackfillIds).insert({ + sourceProjectId: projectId, + destinationProjectType: ProjectType.Cmek, + destinationProjectId: newProjectId + }); + } + } + + /* eslint-enable */ + + await knex.schema.alterTable(TableName.Project, (t) => { + t.string("type").notNullable().alter(); + }); } } @@ -18,4 +253,9 @@ export async function down(knex: Knex): Promise { t.dropColumn("type"); }); } + + const hasSplitMappingTable = await knex.schema.hasTable(TableName.ProjectSplitBackfillIds); + if (hasSplitMappingTable) { + await knex.schema.dropTableIfExists(TableName.ProjectSplitBackfillIds); + } } diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 76f585928..7c2794209 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -106,6 +106,7 @@ export enum TableName { SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2", SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2", SnapshotSecretV2 = "secret_snapshot_secrets_v2", + ProjectSplitBackfillIds = "project_split_backfill_ids", // junction tables with tags SecretV2JnTag = "secret_v2_tag_junction", JnSecretTag = "secret_tag_junction", diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3c64dc60b..fabcae408 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -428,7 +428,8 @@ export const ORGANIZATIONS = { search: "The text string that identity membership names will be filtered by." }, GET_PROJECTS: { - organizationId: "The ID of the organization to get projects from." + organizationId: "The ID of the organization to get projects from.", + type: "The type of project to filter by." }, LIST_GROUPS: { organizationId: "The ID of the organization to list groups for." diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 69a648d9e..67aee3a1f 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -220,6 +220,7 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ id: true, name: true, description: true, + type: true, slug: true, autoCapitalization: true, orgId: true, diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index f27462d02..f348a7cf4 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -5,6 +5,7 @@ import { ProjectMembershipsSchema, ProjectRolesSchema, ProjectSlackConfigsSchema, + ProjectType, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -135,7 +136,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { includeRoles: z .enum(["true", "false"]) .default("false") - .transform((value) => value === "true") + .transform((value) => value === "true"), + type: z.nativeEnum(ProjectType).optional() }), response: { 200: z.object({ @@ -154,7 +156,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actor: req.permission.type, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + type: req.query.type }); return { workspaces }; } diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 5d34bc702..332870a50 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -5,6 +5,7 @@ import { OrgMembershipsSchema, ProjectMembershipsSchema, ProjectsSchema, + ProjectType, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; @@ -76,7 +77,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId) + organizationId: z.string().trim().describe(ORGANIZATIONS.GET_PROJECTS.organizationId), + type: z.nativeEnum(ProjectType).optional().describe(ORGANIZATIONS.GET_PROJECTS.type) }), response: { 200: z.object({ diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 9741220f8..9a48f583a 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -15,7 +15,6 @@ import { TProjectUserMembershipRolesInsert, TUsers } from "@app/db/schemas"; -import { TProjects } from "@app/db/schemas/projects"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal"; @@ -196,26 +195,18 @@ export const orgServiceFactory = ({ return org; }; - const findAllWorkspaces = async ({ actor, actorId, orgId }: TFindAllWorkspacesDTO) => { - const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id)); - - let workspaces: (TProjects & { organization: string } & { - environments: { - id: string; - slug: string; - name: string; - }[]; - })[]; - + const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => { if (actor === ActorType.USER) { - workspaces = await projectDAL.findAllProjects(actorId); - } else if (actor === ActorType.IDENTITY) { - workspaces = await projectDAL.findAllProjectsByIdentity(actorId); - } else { - throw new BadRequestError({ message: "Invalid actor type" }); + const workspaces = await projectDAL.findAllProjects(actorId, orgId, type); + return workspaces; } - return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id)); + if (actor === ActorType.IDENTITY) { + const workspaces = await projectDAL.findAllProjectsByIdentity(actorId, type); + return workspaces; + } + + throw new BadRequestError({ message: "Invalid actor type" }); }; const addGhostUser = async (orgId: string, tx?: Knex) => { diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 05df9429e..f00dfef18 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -1,6 +1,7 @@ import { TOrgPermission } from "@app/lib/types"; import { ActorAuthMethod, ActorType, MfaMethod } from "../auth/auth-type"; +import { ProjectType } from "@app/db/schemas"; export type TUpdateOrgMembershipDTO = { userId: string; @@ -55,6 +56,7 @@ export type TFindAllWorkspacesDTO = { actorOrgId: string | undefined; actorAuthMethod: ActorAuthMethod; orgId: string; + type?: ProjectType; }; export type TUpdateOrgDTO = { diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index e5e447145..2f1877799 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -1,7 +1,14 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { ProjectsSchema, ProjectUpgradeStatus, ProjectVersion, TableName, TProjectsUpdate } from "@app/db/schemas"; +import { + ProjectsSchema, + ProjectType, + ProjectUpgradeStatus, + ProjectVersion, + TableName, + TProjectsUpdate +} from "@app/db/schemas"; import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; @@ -12,12 +19,18 @@ export type TProjectDALFactory = ReturnType; export const projectDALFactory = (db: TDbClient) => { const projectOrm = ormify(db, TableName.Project); - const findAllProjects = async (userId: string) => { + const findAllProjects = async (userId: string, orgId: string, projectType?: ProjectType | null) => { try { const workspaces = await db .replicaNode()(TableName.ProjectMembership) .where({ userId }) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + if (projectType) { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), @@ -31,14 +44,17 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Environment}.position`, order: "asc" } ]); - const groups: string[] = await db(TableName.UserGroupMembership) - .where({ userId }) - .select(selectAllTableCols(TableName.UserGroupMembership)) - .pluck("groupId"); + const groups = db(TableName.UserGroupMembership).where({ userId }).select("groupId"); const groupWorkspaces = await db(TableName.GroupProjectMembership) .whereIn("groupId", groups) .join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.Project}.orgId`, orgId) + .andWhere((qb) => { + if (projectType) { + void qb.where(`${TableName.Project}.type`, projectType); + } + }) .whereNotIn( `${TableName.Project}.id`, workspaces.map(({ id }) => id) @@ -108,12 +124,13 @@ export const projectDALFactory = (db: TDbClient) => { } }; - const findAllProjectsByIdentity = async (identityId: string) => { + const findAllProjectsByIdentity = async (identityId: string, projectType?: ProjectType) => { try { const workspaces = await db .replicaNode()(TableName.IdentityProjectMembership) .where({ identityId }) .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) + .where(`${TableName.Project}.type`, projectType) .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index bdec8bad5..2c1dabc5c 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -157,7 +157,6 @@ export const projectServiceFactory = ({ type = ProjectType.SecretManager }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); - const { permission, membership: orgMembership } = await permissionService.getOrgPermission( actor, actorId, @@ -432,8 +431,8 @@ export const projectServiceFactory = ({ return deletedProject; }; - const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => { - const workspaces = await projectDAL.findAllProjects(actorId); + const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId, type }: TListProjectsDTO) => { + const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type); if (includeRoles) { const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod); diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index b9486f984..39a3f2520 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -85,6 +85,7 @@ export type TDeleteProjectDTO = { export type TListProjectsDTO = { includeRoles: boolean; + type?: ProjectType | null; } & Omit; export type TUpgradeProjectDTO = { diff --git a/frontend/src/components/v2/projects/NewProjectModal.tsx b/frontend/src/components/v2/projects/NewProjectModal.tsx index 1662b07ff..7510f5d23 100644 --- a/frontend/src/components/v2/projects/NewProjectModal.tsx +++ b/frontend/src/components/v2/projects/NewProjectModal.tsx @@ -41,6 +41,7 @@ import { } from "@app/hooks/api"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; +import { ProjectType } from "@app/hooks/api/workspace/types"; const formSchema = z.object({ name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"), @@ -59,11 +60,12 @@ type TAddProjectFormData = z.infer; interface NewProjectModalProps { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; + projectType: ProjectType; } -type NewProjectFormProps = Pick; +type NewProjectFormProps = Pick; -const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { +const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => { const router = useRouter(); const { currentOrg } = useOrganization(); const { permission } = useOrgPermission(); @@ -124,7 +126,8 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { projectName: name, projectDescription: description, kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined, - template + template, + type: projectType }); if (addMembers) { @@ -145,7 +148,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { createNotification({ text: "Project created", type: "success" }); reset(); onOpenChange(false); - router.push(`/project/${newProjectId}/secrets/overview`); + router.push(`/project/${newProjectId}/${projectType}/overview`); } catch (err) { console.error(err); createNotification({ text: "Failed to create project", type: "error" }); @@ -316,14 +319,18 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => { ); }; -export const NewProjectModal: FC = ({ isOpen, onOpenChange }) => { +export const NewProjectModal: FC = ({ + isOpen, + onOpenChange, + projectType +}) => { return ( - + ); diff --git a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx index 29ecacaa5..38d86ce97 100644 --- a/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx +++ b/frontend/src/context/WorkspaceContext/WorkspaceContext.tsx @@ -41,6 +41,7 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => { // handle redirects for project-specific routes useEffect(() => { if (shouldTriggerNoProjectAccess) { + console.log(value, workspaceId); createNotification({ text: "You are not a member of this project.", type: "info" diff --git a/frontend/src/helpers/project.ts b/frontend/src/helpers/project.ts index b6338ce35..403840027 100644 --- a/frontend/src/helpers/project.ts +++ b/frontend/src/helpers/project.ts @@ -1,5 +1,6 @@ import { apiRequest } from "@app/config/request"; import { createWorkspace } from "@app/hooks/api/workspace/queries"; +import { ProjectType } from "@app/hooks/api/workspace/types"; const secretsToBeAdded = [ { @@ -41,7 +42,8 @@ const initProjectHelper = async ({ projectName }: { projectName: string }) => { const { data: { project } } = await createWorkspace({ - projectName + projectName, + type: ProjectType.SecretManager }); try { diff --git a/frontend/src/hooks/api/migration/mutations.tsx b/frontend/src/hooks/api/migration/mutations.tsx index 41d17b0bd..feee7778d 100644 --- a/frontend/src/hooks/api/migration/mutations.tsx +++ b/frontend/src/hooks/api/migration/mutations.tsx @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace"; +import { ProjectType } from "../workspace/types"; export const useImportEnvKey = () => { const queryClient = useQueryClient(); @@ -31,7 +32,7 @@ export const useImportEnvKey = () => { } }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; diff --git a/frontend/src/hooks/api/workspace/mutations.tsx b/frontend/src/hooks/api/workspace/mutations.tsx index ae8829591..c4b8211bb 100644 --- a/frontend/src/hooks/api/workspace/mutations.tsx +++ b/frontend/src/hooks/api/workspace/mutations.tsx @@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request"; import { userKeys } from "../users/query-keys"; import { workspaceKeys } from "./query-keys"; -import { TUpdateWorkspaceGroupRoleDTO } from "./types"; +import { ProjectType, TUpdateWorkspaceGroupRoleDTO } from "./types"; export const useAddGroupToWorkspace = () => { const queryClient = useQueryClient(); @@ -83,7 +83,7 @@ export const useLeaveProject = () => { return apiRequest.delete(`/api/v1/workspace/${workspaceId}/leave`); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace()); } }); }; @@ -95,7 +95,7 @@ export const useMigrateProjectToV3 = () => { return apiRequest.post(`/api/v1/workspace/${workspaceId}/migrate-v3`); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index ec887a401..96d4e3c9d 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -26,6 +26,7 @@ import { DeleteWorkspaceDTO, NameWorkspaceSecretsDTO, ProjectIdentityOrderBy, + ProjectType, TGetUpgradeProjectStatusDTO, TListProjectIdentitiesDTO, ToggleAutoCapitalizationDTO, @@ -82,7 +83,7 @@ export const useUpgradeProject = () => { }); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; @@ -102,10 +103,11 @@ export const useGetUpgradeProjectStatus = ({ }); }; -const fetchUserWorkspaces = async (includeRoles?: boolean) => { +const fetchUserWorkspaces = async (includeRoles?: boolean, type?: ProjectType) => { const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", { params: { - includeRoles + includeRoles, + type } }); return data.workspaces; @@ -139,8 +141,16 @@ export const useGetWorkspaceById = ( }); }; -export const useGetUserWorkspaces = (includeRoles?: boolean) => - useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles)); +export const useGetUserWorkspaces = ({ + includeRoles, + type +}: { + includeRoles?: boolean; + type?: ProjectType; +} = {}) => + useQuery(workspaceKeys.getAllUserWorkspace(type || ""), () => + fetchUserWorkspaces(includeRoles, type) + ); const fetchUserWorkspaceMemberships = async (orgId: string) => { const { data } = await apiRequest.get>( @@ -206,33 +216,26 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) => refetchInterval: 4000 }); -export const createWorkspace = ({ - projectName, - projectDescription, - kmsKeyId, - template -}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => { - return apiRequest.post("/api/v2/workspace", { - projectName, - projectDescription, - kmsKeyId, - template - }); +export const createWorkspace = ( + dto: CreateWorkspaceDTO +): Promise<{ data: { project: Workspace } }> => { + return apiRequest.post("/api/v2/workspace", dto); }; export const useCreateWorkspace = () => { const queryClient = useQueryClient(); return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({ - mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) => + mutationFn: async ({ projectName, projectDescription, kmsKeyId, template, type }) => createWorkspace({ projectName, projectDescription, kmsKeyId, - template + template, + type }), - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.data.project.type)); } }); }; @@ -240,15 +243,19 @@ export const useCreateWorkspace = () => { export const useUpdateProject = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, UpdateProjectDTO>({ - mutationFn: ({ projectID, newProjectName, newProjectDescription }) => { - return apiRequest.patch(`/api/v1/workspace/${projectID}`, { - name: newProjectName, - description: newProjectDescription - }); + return useMutation({ + mutationFn: async ({ projectID, newProjectName, newProjectDescription }) => { + const { data } = await apiRequest.patch<{ workspace: Workspace }>( + `/api/v1/workspace/${projectID}`, + { + name: newProjectName, + description: newProjectDescription + } + ); + return data.workspace; }, - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); } }); }; @@ -256,13 +263,18 @@ export const useUpdateProject = () => { export const useToggleAutoCapitalization = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({ - mutationFn: ({ workspaceID, state }) => - apiRequest.post(`/api/v1/workspace/${workspaceID}/auto-capitalization`, { - autoCapitalization: state - }), - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + return useMutation({ + mutationFn: async ({ workspaceID, state }) => { + const { data } = await apiRequest.post<{ workspace: Workspace }>( + `/api/v1/workspace/${workspaceID}/auto-capitalization`, + { + autoCapitalization: state + } + ); + return data.workspace; + }, + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); } }); }; @@ -270,14 +282,15 @@ export const useToggleAutoCapitalization = () => { export const useUpdateWorkspaceVersionLimit = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, UpdatePitVersionLimitDTO>({ - mutationFn: ({ projectSlug, pitVersionLimit }) => { - return apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, { + return useMutation({ + mutationFn: async ({ projectSlug, pitVersionLimit }) => { + const { data } = await apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, { pitVersionLimit }); + return data.workspace; }, - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); } }); }; @@ -285,14 +298,18 @@ export const useUpdateWorkspaceVersionLimit = () => { export const useUpdateWorkspaceAuditLogsRetention = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, UpdateAuditLogsRetentionDTO>({ - mutationFn: ({ projectSlug, auditLogsRetentionDays }) => { - return apiRequest.put(`/api/v1/workspace/${projectSlug}/audit-logs-retention`, { - auditLogsRetentionDays - }); + return useMutation({ + mutationFn: async ({ projectSlug, auditLogsRetentionDays }) => { + const { data } = await apiRequest.put( + `/api/v1/workspace/${projectSlug}/audit-logs-retention`, + { + auditLogsRetentionDays + } + ); + return data.workspace; }, - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); } }); }; @@ -300,12 +317,13 @@ export const useUpdateWorkspaceAuditLogsRetention = () => { export const useDeleteWorkspace = () => { const queryClient = useQueryClient(); - return useMutation<{}, {}, DeleteWorkspaceDTO>({ - mutationFn: ({ workspaceID }) => { - return apiRequest.delete(`/api/v1/workspace/${workspaceID}`); + return useMutation({ + mutationFn: async ({ workspaceID }) => { + const { data } = await apiRequest.delete(`/api/v1/workspace/${workspaceID}`); + return data.workspace; }, - onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + onSuccess: (dto) => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); queryClient.invalidateQueries(["org-admin-projects"]); } }); @@ -322,7 +340,7 @@ export const useCreateWsEnvironment = () => { }); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; @@ -339,7 +357,7 @@ export const useUpdateWsEnvironment = () => { }); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; @@ -352,7 +370,7 @@ export const useDeleteWsEnvironment = () => { return apiRequest.delete(`/api/v1/workspace/${workspaceId}/environments/${id}`); }, onSuccess: () => { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }); }; diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index f5a02ec2b..a1c5770e9 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -11,7 +11,8 @@ export const workspaceKeys = { getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"], getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"], getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"], - getAllUserWorkspace: ["workspaces"] as const, + getAllUserWorkspace: (type?: string) => + type ? ["workspaces", { type }] : (["workspace"] as const), getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }, "workspace-audit-logs"] as const, getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const, diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 91cf3a9d8..b0117e70f 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -8,6 +8,12 @@ export enum ProjectVersion { V3 = 3 } +export enum ProjectType { + SecretManager = "secret-manager", + CertificateManager = "cert-manager", + Cmek = "cmek" +} + export enum ProjectUserMembershipTemporaryMode { Relative = "relative" } @@ -16,6 +22,7 @@ export type Workspace = { __v: number; id: string; name: string; + type: ProjectType; description?: string; orgId: string; version: ProjectVersion; @@ -59,6 +66,7 @@ export type CreateWorkspaceDTO = { projectDescription?: string; kmsKeyId?: string; template?: string; + type: ProjectType; }; export type UpdateProjectDTO = { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 8c4f8e1c8..ece5dd8e2 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -163,7 +163,7 @@ export const AppLayout = ({ children }: LayoutProps) => { !router.asPath.includes("secret-scanning") && !router.asPath.includes("integration"))) ) { - router.push(`/org/${currentOrg?.id}/overview`); + router.push(`/org/${currentOrg?.id}/secret-manager/overview`); } // else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) { @@ -216,7 +216,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
{(router.asPath.includes("project") || router.asPath.includes("integrations")) && ( - +
@@ -379,7 +379,7 @@ export const AppLayout = ({ children }: LayoutProps) => { (!router.asPath.includes("personal") && currentWorkspace ? ( ) : ( - +
Back to organization @@ -493,13 +493,33 @@ export const AppLayout = ({ children }: LayoutProps) => { ) : ( - + - Overview + Secret Manager + + + + + + + Cert Manager + + + + + + + Cmek diff --git a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx deleted file mode 100644 index 979758ba5..000000000 --- a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx +++ /dev/null @@ -1,343 +0,0 @@ -/* eslint-disable jsx-a11y/anchor-is-valid */ -/* eslint-disable react/jsx-key */ -import { Fragment, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons"; -import { - faAngleDown, - faBook, - faCoins, - faEnvelope, - faGear, - faPlus, - faRightFromBracket -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Menu, Transition } from "@headlessui/react"; -import { TFunction } from "i18next"; - -import guidGenerator from "@app/components/utilities/randomId"; -import { useOrganization, useSubscription, useUser } from "@app/context"; -import { useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api"; - -const supportOptions = (t: TFunction) => [ - [ - , - t("nav.support.slack"), - "https://infisical.com/slack" - ], - [ - , - t("nav.support.docs"), - "https://infisical.com/docs/documentation/getting-started/introduction" - ], - [ - , - t("nav.support.issue"), - "https://github.com/Infisical/infisical-cli/issues" - ], - [ - , - t("nav.support.email"), - "mailto:support@infisical.com" - ] -]; - -export interface ICurrentOrg { - name: string; -} - -export interface IUser { - firstName: string; - lastName: string; - email: string; -} - -/** - * This is the navigation bar in the main app. - * It has two main components: support options and user menu (inlcudes billing, logout, org/user settings) - * @returns NavBar - */ -export const Navbar = () => { - const router = useRouter(); - const { subscription } = useSubscription(); - - const { currentOrg, orgs } = useOrganization(); - const { mutateAsync } = useGetOrgTrialUrl(); - const { user } = useUser(); - - const logout = useLogoutUser(); - - const { t } = useTranslation(); - - // remove this memo - const supportOptionsList = useMemo(() => supportOptions(t), [t]); - - const closeApp = async () => { - try { - console.log("Logging out..."); - await logout.mutateAsync(); - localStorage.removeItem("protectedKey"); - localStorage.removeItem("protectedKeyIV"); - localStorage.removeItem("protectedKeyTag"); - localStorage.removeItem("publicKey"); - localStorage.removeItem("encryptedPrivateKey"); - localStorage.removeItem("iv"); - localStorage.removeItem("tag"); - localStorage.removeItem("PRIVATE_KEY"); - localStorage.removeItem("orgData.id"); - localStorage.removeItem("projectData.id"); - router.push("/login"); - } catch (error) { - console.error(error); - } - }; - - return ( -
-
-
-
- logo -
- - Infisical - -
-
- - - Docs - - -
- - - -
- - - {supportOptionsList.map(([icon, text, url]) => ( - -
- {icon} -
{text}
-
-
- ))} -
-
-
- -
- - {user?.firstName} {user?.lastName} - - -
- - -
-
- {t("nav.user.signed-in-as")} -
-
null} - role="button" - tabIndex={0} - onClick={() => router.push("/personal-settings")} - className="mx-1 my-1 flex cursor-pointer flex-row items-center rounded-md px-1 hover:bg-white/5" - > -
- {user?.firstName?.charAt(0)} -
-
-
-

- {" "} - {user?.firstName} {user?.lastName} -

-

{user?.email}

-
- -
-
-
-
-
- {t("nav.user.current-organization")} -
-
null} - role="button" - tabIndex={0} - onClick={() => router.push(`/settings/org/${router.query.id}`)} - className="mt-2 flex cursor-pointer flex-row items-center rounded-md px-2 py-1 hover:bg-white/5" - > -
- {currentOrg?.name?.charAt(0)} -
-
-

{currentOrg?.name}

- -
-
- {subscription && subscription.slug !== null && ( - - )} - -
- {orgs && orgs?.length > 1 && ( -
-
- {t("nav.user.other-organizations")} -
-
- {orgs - ?.filter((org: { id: string }) => org.id !== currentOrg?.id) - .map((org: { id: string; name: string }) => ( -
null} - role="button" - tabIndex={0} - key={guidGenerator()} - onClick={() => { - localStorage.setItem("orgData.id", org.id); - router.reload(); - }} - className="flex w-full cursor-pointer flex-row items-center justify-start rounded-md p-1.5 hover:bg-white/5" - > -
- {org.name.charAt(0)} -
-
-

{org.name}

-
-
- ))} -
-
- )} -
- - {({ active }) => ( - - )} - -
-
-
-
-
-
- {subscription && subscription.slug === "starter" && !subscription.has_used_trial && ( -
- -
- )} -
- ); -}; diff --git a/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx index 777d65b71..e69ada548 100644 --- a/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx +++ b/frontend/src/layouts/AppLayout/components/ProjectSelect/ProjectSelect.tsx @@ -19,7 +19,7 @@ import { import { usePopUp } from "@app/hooks"; import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; -import { Workspace } from "@app/hooks/api/workspace/types"; +import { ProjectType, Workspace } from "@app/hooks/api/workspace/types"; type TWorkspaceWithFaveProp = Workspace & { isFavorite: boolean }; @@ -138,6 +138,7 @@ export const ProjectSelect = () => { const { options, value } = useMemo(() => { const projectOptions = workspaces + .filter((el) => el.type === currentWorkspace?.type) .map((w): Workspace & { isFavorite: boolean } => ({ ...w, isFavorite: Boolean(projectFavorites?.includes(w.id)) @@ -206,6 +207,7 @@ export const ProjectSelect = () => { handlePopUpToggle("addNewWs", isOpen)} + projectType={currentWorkspace?.type || ProjectType.SecretManager} />
); diff --git a/frontend/src/pages/org/[id]/cert-manager/overview.tsx b/frontend/src/pages/org/[id]/cert-manager/overview.tsx new file mode 100644 index 000000000..4b50a3c6e --- /dev/null +++ b/frontend/src/pages/org/[id]/cert-manager/overview.tsx @@ -0,0 +1,8 @@ +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { ProductOverview } from "../secret-manager/overview"; + +const CertManagerOverviewPage = () => ; + +Object.assign(CertManagerOverviewPage, { requireAuth: true }); + +export default CertManagerOverviewPage; diff --git a/frontend/src/pages/org/[id]/cmek/overview.tsx b/frontend/src/pages/org/[id]/cmek/overview.tsx new file mode 100644 index 000000000..b1c3b96ec --- /dev/null +++ b/frontend/src/pages/org/[id]/cmek/overview.tsx @@ -0,0 +1,8 @@ +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { ProductOverview } from "../secret-manager/overview"; + +const CmekManagerOverviewPage = () => ; + +Object.assign(CmekManagerOverviewPage, { requireAuth: true }); + +export default CmekManagerOverviewPage; diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 9e39fd389..25a2bbcf0 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -1,1042 +1,18 @@ -// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex - -import { ReactNode, useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import Head from "next/head"; -import Link from "next/link"; +import { useOrganization } from "@app/context"; import { useRouter } from "next/router"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; -import { faSlack } from "@fortawesome/free-brands-svg-icons"; -import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons"; -import { - faArrowDownAZ, - faArrowRight, - faArrowUpRightFromSquare, - faArrowUpZA, - faBorderAll, - faCheck, - faCheckCircle, - faClipboard, - faExclamationCircle, - faHandPeace, - faList, - faMagnifyingGlass, - faNetworkWired, - faPlug, - faPlus, - faSearch, - faStar as faSolidStar, - faUserPlus -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import * as Tabs from "@radix-ui/react-tabs"; - -import { createNotification } from "@app/components/notifications"; -import { OrgPermissionCan } from "@app/components/permissions"; -import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; -import { - Button, - IconButton, - Input, - Pagination, - Skeleton, - Tooltip, - UpgradePlanModal -} from "@app/components/v2"; -import { NewProjectModal } from "@app/components/v2/projects"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - useOrganization, - useSubscription, - useUser, - useWorkspace -} from "@app/context"; -import { usePagination, useResetPageHelper } from "@app/hooks"; -import { useRegisterUserAction } from "@app/hooks/api"; -import { OrderByDirection } from "@app/hooks/api/generic/types"; -// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; -import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; -import { Workspace } from "@app/hooks/api/types"; -import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; -import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; -import { usePopUp } from "@app/hooks/usePopUp"; - -const features = [ - { - id: 0, - name: "Kubernetes Operator", - link: "https://infisical.com/docs/documentation/getting-started/kubernetes", - description: - "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes." - }, - { - id: 1, - name: "Infisical Agent", - link: "https://infisical.com/docs/infisical-agent/overview", - description: "Inject secrets into your apps without modifying any application logic." - } -]; - -type ItemProps = { - text: string; - subText: string; - complete: boolean; - icon: IconProp; - time: string; - userAction?: string; - link?: string; -}; - -enum ProjectsViewMode { - GRID = "grid", - LIST = "list" -} - -enum ProjectOrderBy { - Name = "name" -} - -function copyToClipboard(id: string, setState: (value: boolean) => void) { - // Get the text field - const copyText = document.getElementById(id) as HTMLInputElement; - - // Select the text field - copyText.select(); - copyText.setSelectionRange(0, 99999); // For mobile devices - - // Copy the text inside the text field - navigator.clipboard.writeText(copyText.value); - - setState(true); - setTimeout(() => setState(false), 2000); - // Alert the copied text - // alert("Copied the text: " + copyText.value); -} - -const CodeItem = ({ - isCopied, - setIsCopied, - textExplanation, - code, - id -}: { - isCopied: boolean; - setIsCopied: (value: boolean) => void; - textExplanation: string; - code: string; - id: string; -}) => { - return ( - <> -

{textExplanation}

-
- - -
- - ); -}; - -const TabsObject = () => { - const [downloadCodeCopied, setDownloadCodeCopied] = useState(false); - const [downloadCode2Copied, setDownloadCode2Copied] = useState(false); - const [loginCodeCopied, setLoginCodeCopied] = useState(false); - const [initCodeCopied, setInitCodeCopied] = useState(false); - const [runCodeCopied, setRunCodeCopied] = useState(false); - - return ( - - - - MacOS - - - Windows - - {/* - Arch Linux - */} - - Other Platforms - - - - - - - -

- You can find example of start commands for different frameworks{" "} - - here - - .{" "} -

-
- - -
- - -
- - - -

- You can find example of start commands for different frameworks{" "} - - here - - .{" "} -

-
-
- ); -}; - -const LearningItem = ({ - text, - subText, - complete, - icon, - time, - userAction, - link -}: ItemProps): JSX.Element => { - const registerUserAction = useRegisterUserAction(); - if (link) { - return ( - -
-
null} - role="button" - tabIndex={0} - onClick={async () => { - if (userAction && userAction !== "first_time_secrets_pushed") { - await registerUserAction.mutateAsync(userAction); - } - }} - className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ - complete - ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" - : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" - } text-mineshaft-100 duration-200`} - > -
- - {complete && ( -
- -
- )} -
-
{text}
-
{subText}
-
-
-
- {complete ? "Complete!" : `About ${time}`} -
- {/* {complete &&
} */} -
-
-
- ); - } - return ( -
null} - role="button" - tabIndex={0} - onClick={async () => { - if (userAction) { - await registerUserAction.mutateAsync(userAction); - } - }} - className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500" - > -
- - {complete && ( -
- -
- )} -
-
{text}
-
{subText}
-
-
-
- {complete ? "Complete!" : `About ${time}`} -
- {complete &&
} -
- ); -}; - -const LearningItemSquare = ({ - text, - subText, - complete, - icon, - time, - userAction, - link -}: ItemProps): JSX.Element => { - const registerUserAction = useRegisterUserAction(); - return ( - -
-
null} - role="button" - tabIndex={0} - onClick={async () => { - if (userAction && userAction !== "first_time_secrets_pushed") { - await registerUserAction.mutateAsync(userAction); - } - }} - className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${ - complete - ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" - : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" - } text-mineshaft-100 duration-200`} - > -
-
- - {complete && ( -
- -
- )} -
- {complete ? "Complete!" : `About ${time}`} -
-
-
-
{text}
-
{subText}
-
-
-
-
-
- ); -}; +import { useEffect } from "react"; // #TODO: Update all the workspaceIds const OrganizationPage = () => { - const { t } = useTranslation(); - const router = useRouter(); - - const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace(); const { currentOrg } = useOrganization(); - const routerOrgId = String(router.query.id); - const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || []; - const { data: projectFavorites, isLoading: isProjectFavoritesLoading } = - useGetUserProjectFavorites(currentOrg?.id!); - const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); - - const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading; - - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ - "addNewWs", - "upgradePlan" - ] as const); - - const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); - const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); - const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); - const [usersInOrg, setUsersInOrg] = useState(false); - const [searchFilter, setSearchFilter] = useState(""); - const { user } = useUser(); - const { data: serverDetails } = useFetchServerStatus(); - const [projectsViewMode, setProjectsViewMode] = useState( - (localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID - ); - - const { subscription } = useSubscription(); - - const isAddingProjectsAllowed = subscription?.workspaceLimit - ? subscription.workspacesUsed < subscription.workspaceLimit - : true; - useEffect(() => { - onboardingCheck({ - orgId: routerOrgId, - setHasUserClickedIntro, - setHasUserClickedSlack, - setHasUserPushedSecrets, - setUsersInOrg - }); - }, []); - - const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0; - - const { - setPage, - perPage, - setPerPage, - page, - offset, - limit, - toggleOrderDirection, - orderDirection - } = usePagination(ProjectOrderBy.Name, { initPerPage: 24 }); - - const filteredWorkspaces = useMemo( - () => - orgWorkspaces - .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) - .sort((a, b) => - orderDirection === OrderByDirection.ASC - ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) - : b.name.toLowerCase().localeCompare(a.name.toLowerCase()) - ), - [searchFilter, page, perPage, orderDirection, offset, limit] - ); - - useResetPageHelper({ - setPage, - offset, - totalCount: filteredWorkspaces.length - }); - - const { workspacesWithFaveProp } = useMemo(() => { - const workspacesWithFav = filteredWorkspaces - .map((w): Workspace & { isFavorite: boolean } => ({ - ...w, - isFavorite: Boolean(projectFavorites?.includes(w.id)) - })) - .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)) - .slice(offset, limit * page); - - return { - workspacesWithFaveProp: workspacesWithFav - }; - }, [filteredWorkspaces, projectFavorites]); - - const addProjectToFavorites = async (projectId: string) => { - try { - if (currentOrg?.id) { - await updateUserProjectFavorites({ - orgId: currentOrg?.id, - projectFavorites: [...(projectFavorites || []), projectId] - }); - } - } catch (err) { - createNotification({ - text: "Failed to add project to favorites.", - type: "error" - }); + if (router.isReady && currentOrg?.id) { + router.push(`/org/${currentOrg?.id}/secret-manager/overview`); } - }; + }, [router.isReady, currentOrg?.id]); - const removeProjectFromFavorites = async (projectId: string) => { - try { - if (currentOrg?.id) { - await updateUserProjectFavorites({ - orgId: currentOrg?.id, - projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] - }); - } - } catch (err) { - createNotification({ - text: "Failed to remove project from favorites.", - type: "error" - }); - } - }; - - const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -
{ - router.push(`/project/${workspace.id}/secrets/overview`); - localStorage.setItem("projectData.id", workspace.id); - }} - key={workspace.id} - className="min-w-72 flex h-40 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4" - > -
-
{workspace.name}
- {isFavorite ? ( - { - e.stopPropagation(); - removeProjectFromFavorites(workspace.id); - }} - /> - ) : ( - { - e.stopPropagation(); - addProjectToFavorites(workspace.id); - }} - /> - )} -
- -
- {workspace.description} -
- -
-
- {workspace.environments?.length || 0} environments -
- -
-
- ); - - const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -
{ - router.push(`/project/${workspace.id}/secrets/overview`); - localStorage.setItem("projectData.id", workspace.id); - }} - key={workspace.id} - className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ - index === 0 && "rounded-t-md" - }`} - > -
-
{workspace.name}
-
-
-
- {workspace.environments?.length || 0} environments -
- {isFavorite ? ( - { - e.stopPropagation(); - removeProjectFromFavorites(workspace.id); - }} - /> - ) : ( - { - e.stopPropagation(); - addProjectToFavorites(workspace.id); - }} - /> - )} -
-
- ); - - let projectsComponents: ReactNode; - - if (filteredWorkspaces.length || isProjectViewLoading) { - switch (projectsViewMode) { - case ProjectsViewMode.GRID: - projectsComponents = ( -
- {isProjectViewLoading && - Array.apply(0, Array(3)).map((_x, i) => ( -
-
- -
-
- -
-
- -
-
- ))} - {!isProjectViewLoading && ( - <> - {workspacesWithFaveProp.map((workspace) => - renderProjectGridItem(workspace, workspace.isFavorite) - )} - - )} -
- ); - - break; - case ProjectsViewMode.LIST: - default: - projectsComponents = ( -
- {isProjectViewLoading && - Array.apply(0, Array(3)).map((_x, i) => ( -
- -
- ))} - {!isProjectViewLoading && - workspacesWithFaveProp.map((workspace, ind) => - renderProjectListItem(workspace, workspace.isFavorite, ind) - )} -
- ); - break; - } - } else if (orgWorkspaces.length) { - projectsComponents = ( -
- -
No projects match search...
-
- ); - } - - return ( -
- - {t("common.head-title", { title: t("settings.members.title") })} - - - {!serverDetails?.redisConfigured && ( -
-

Announcements

-
- - Attention: Updated versions of Infisical now require Redis for full functionality. Learn - how to configure it - - - here - - - . -
-
- )} -
-
-

Projects

-
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
- - - - - -
-
- { - localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID); - setProjectsViewMode(ProjectsViewMode.GRID); - }} - ariaLabel="grid" - size="xs" - className={`${ - projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent" - } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} - > - - - { - localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST); - setProjectsViewMode(ProjectsViewMode.LIST); - }} - ariaLabel="list" - size="xs" - className={`${ - projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent" - } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} - > - - -
- - {(isAllowed) => ( - - )} - -
- {projectsComponents} - {!isProjectViewLoading && Boolean(filteredWorkspaces.length) && ( - - )} - {isWorkspaceEmpty && ( -
- -
- You are not part of any projects in this organization yet. When you are, they will - appear here. -
-
- Create a new project, or ask other organization members to give you necessary - permissions. -
-
- )} -
-
-

Explore Infisical

-
- {features.map((feature) => ( -
-
{feature.name}
-
- {feature.description} -
-
-

- Setup time: 20 min -

- - Learn more{" "} - - -
-
- ))} -
-
- {!(new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000) && ( -
-

Onboarding Guide

-
- - {orgWorkspaces.length !== 0 && ( - <> - - - - )} -
- -
-
- {orgWorkspaces.length !== 0 && ( -
-
-
- - {false && ( -
- -
- )} -
-
Inject secrets locally
-
- Replace .env files with a more secure and efficient alternative. -
-
-
-
- About 2 min -
-
- - {false &&
} -
- )} - {orgWorkspaces.length !== 0 && ( - - )} -
- )} - handlePopUpToggle("addNewWs", isOpen)} - /> - handlePopUpToggle("upgradePlan", isOpen)} - text="You have exceeded the number of projects allowed on the free plan." - /> - {/* */} -
- ); + return
; }; Object.assign(OrganizationPage, { requireAuth: true }); diff --git a/frontend/src/pages/org/[id]/secret-manager/overview.tsx b/frontend/src/pages/org/[id]/secret-manager/overview.tsx new file mode 100644 index 000000000..54f6b7e55 --- /dev/null +++ b/frontend/src/pages/org/[id]/secret-manager/overview.tsx @@ -0,0 +1,1059 @@ +// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex + +import { ReactNode, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import Head from "next/head"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { faSlack } from "@fortawesome/free-brands-svg-icons"; +import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons"; +import { + faArrowDownAZ, + faArrowRight, + faArrowUpRightFromSquare, + faArrowUpZA, + faBorderAll, + faCheck, + faCheckCircle, + faClipboard, + faExclamationCircle, + faHandPeace, + faList, + faMagnifyingGlass, + faNetworkWired, + faPlug, + faPlus, + faSearch, + faStar as faSolidStar, + faUserPlus +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as Tabs from "@radix-ui/react-tabs"; + +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; +import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; +import { + Button, + IconButton, + Input, + Pagination, + Skeleton, + Tooltip, + UpgradePlanModal +} from "@app/components/v2"; +import { NewProjectModal } from "@app/components/v2/projects"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + useOrganization, + useSubscription, + useUser +} from "@app/context"; +import { usePagination, useResetPageHelper } from "@app/hooks"; +import { useGetUserWorkspaces, useRegisterUserAction } from "@app/hooks/api"; +import { OrderByDirection } from "@app/hooks/api/generic/types"; +// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; +import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; +import { Workspace } from "@app/hooks/api/types"; +import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation"; +import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries"; +import { usePopUp } from "@app/hooks/usePopUp"; +import { ProjectType } from "@app/hooks/api/workspace/types"; + +const features = [ + { + id: 0, + name: "Kubernetes Operator", + link: "https://infisical.com/docs/documentation/getting-started/kubernetes", + description: + "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes." + }, + { + id: 1, + name: "Infisical Agent", + link: "https://infisical.com/docs/infisical-agent/overview", + description: "Inject secrets into your apps without modifying any application logic." + } +]; + +type ItemProps = { + text: string; + subText: string; + complete: boolean; + icon: IconProp; + time: string; + userAction?: string; + link?: string; +}; + +enum ProjectsViewMode { + GRID = "grid", + LIST = "list" +} + +enum ProjectOrderBy { + Name = "name" +} + +function copyToClipboard(id: string, setState: (value: boolean) => void) { + // Get the text field + const copyText = document.getElementById(id) as HTMLInputElement; + + // Select the text field + copyText.select(); + copyText.setSelectionRange(0, 99999); // For mobile devices + + // Copy the text inside the text field + navigator.clipboard.writeText(copyText.value); + + setState(true); + setTimeout(() => setState(false), 2000); + // Alert the copied text + // alert("Copied the text: " + copyText.value); +} + +const CodeItem = ({ + isCopied, + setIsCopied, + textExplanation, + code, + id +}: { + isCopied: boolean; + setIsCopied: (value: boolean) => void; + textExplanation: string; + code: string; + id: string; +}) => { + return ( + <> +

{textExplanation}

+
+ + +
+ + ); +}; + +const TabsObject = () => { + const [downloadCodeCopied, setDownloadCodeCopied] = useState(false); + const [downloadCode2Copied, setDownloadCode2Copied] = useState(false); + const [loginCodeCopied, setLoginCodeCopied] = useState(false); + const [initCodeCopied, setInitCodeCopied] = useState(false); + const [runCodeCopied, setRunCodeCopied] = useState(false); + + return ( + + + + MacOS + + + Windows + + {/* + Arch Linux + */} + + Other Platforms + + + + + + + +

+ You can find example of start commands for different frameworks{" "} + + here + + .{" "} +

+
+ + +
+ + +
+ + + +

+ You can find example of start commands for different frameworks{" "} + + here + + .{" "} +

+
+
+ ); +}; + +const LearningItem = ({ + text, + subText, + complete, + icon, + time, + userAction, + link +}: ItemProps): JSX.Element => { + const registerUserAction = useRegisterUserAction(); + if (link) { + return ( + +
+
null} + role="button" + tabIndex={0} + onClick={async () => { + if (userAction && userAction !== "first_time_secrets_pushed") { + await registerUserAction.mutateAsync(userAction); + } + }} + className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ + complete + ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" + : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" + } text-mineshaft-100 duration-200`} + > +
+ + {complete && ( +
+ +
+ )} +
+
{text}
+
{subText}
+
+
+
+ {complete ? "Complete!" : `About ${time}`} +
+ {/* {complete &&
} */} +
+
+
+ ); + } + return ( +
null} + role="button" + tabIndex={0} + onClick={async () => { + if (userAction) { + await registerUserAction.mutateAsync(userAction); + } + }} + className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500" + > +
+ + {complete && ( +
+ +
+ )} +
+
{text}
+
{subText}
+
+
+
+ {complete ? "Complete!" : `About ${time}`} +
+ {complete &&
} +
+ ); +}; + +const LearningItemSquare = ({ + text, + subText, + complete, + icon, + time, + userAction, + link +}: ItemProps): JSX.Element => { + const registerUserAction = useRegisterUserAction(); + return ( + +
+
null} + role="button" + tabIndex={0} + onClick={async () => { + if (userAction && userAction !== "first_time_secrets_pushed") { + await registerUserAction.mutateAsync(userAction); + } + }} + className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${ + complete + ? "cursor-default border-mineshaft-900 bg-gradient-to-r from-[#0e1f01] to-mineshaft-700" + : "cursor-pointer border-mineshaft-600 bg-mineshaft-800 shadow-xl hover:bg-mineshaft-700" + } text-mineshaft-100 duration-200`} + > +
+
+ + {complete && ( +
+ +
+ )} +
+ {complete ? "Complete!" : `About ${time}`} +
+
+
+
{text}
+
{subText}
+
+
+
+
+
+ ); +}; + +const formatTitle = (type: ProjectType) => { + if (type === ProjectType.SecretManager) return "Secret Managers"; + if (type === ProjectType.CertificateManager) return "Cert Managers"; + return "Cmek"; +}; + +type Props = { + type: ProjectType; +}; + +// #TODO: Update all the workspaceIds +export const ProductOverview = ({ type }: Props) => { + const { t } = useTranslation(); + + const router = useRouter(); + + const { data: workspaces, isLoading: isWorkspaceLoading } = useGetUserWorkspaces({ type }); + const { currentOrg } = useOrganization(); + const routerOrgId = String(router.query.id); + const orgWorkspaces = workspaces || []; + const { data: projectFavorites, isLoading: isProjectFavoritesLoading } = + useGetUserProjectFavorites(currentOrg?.id!); + const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites(); + + const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading; + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "addNewWs", + "upgradePlan" + ] as const); + + const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false); + const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false); + const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false); + const [usersInOrg, setUsersInOrg] = useState(false); + const [searchFilter, setSearchFilter] = useState(""); + const { user } = useUser(); + const { data: serverDetails } = useFetchServerStatus(); + const [projectsViewMode, setProjectsViewMode] = useState( + (localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID + ); + + const { subscription } = useSubscription(); + + const isAddingProjectsAllowed = subscription?.workspaceLimit + ? subscription.workspacesUsed < subscription.workspaceLimit + : true; + + useEffect(() => { + onboardingCheck({ + orgId: routerOrgId, + setHasUserClickedIntro, + setHasUserClickedSlack, + setHasUserPushedSecrets, + setUsersInOrg + }); + }, []); + + const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0; + + const { + setPage, + perPage, + setPerPage, + page, + offset, + limit, + toggleOrderDirection, + orderDirection + } = usePagination(ProjectOrderBy.Name, { initPerPage: 24 }); + + const filteredWorkspaces = useMemo( + () => + orgWorkspaces + .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())) + .sort((a, b) => + orderDirection === OrderByDirection.ASC + ? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + : b.name.toLowerCase().localeCompare(a.name.toLowerCase()) + ), + [searchFilter, orderDirection, orgWorkspaces] + ); + + useResetPageHelper({ + setPage, + offset, + totalCount: filteredWorkspaces.length + }); + + const { workspacesWithFaveProp } = useMemo(() => { + const workspacesWithFav = filteredWorkspaces + .map((w): Workspace & { isFavorite: boolean } => ({ + ...w, + isFavorite: Boolean(projectFavorites?.includes(w.id)) + })) + .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)) + .slice(offset, limit * page); + + return { + workspacesWithFaveProp: workspacesWithFav + }; + }, [filteredWorkspaces, projectFavorites]); + + const addProjectToFavorites = async (projectId: string) => { + try { + if (currentOrg?.id) { + await updateUserProjectFavorites({ + orgId: currentOrg?.id, + projectFavorites: [...(projectFavorites || []), projectId] + }); + } + } catch (err) { + createNotification({ + text: "Failed to add project to favorites.", + type: "error" + }); + } + }; + + const removeProjectFromFavorites = async (projectId: string) => { + try { + if (currentOrg?.id) { + await updateUserProjectFavorites({ + orgId: currentOrg?.id, + projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)] + }); + } + } catch (err) { + createNotification({ + text: "Failed to remove project from favorites.", + type: "error" + }); + } + }; + + const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events +
{ + router.push(`/project/${workspace.id}/secrets/overview`); + localStorage.setItem("projectData.id", workspace.id); + }} + key={workspace.id} + className="min-w-72 flex h-40 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4" + > +
+
{workspace.name}
+ {isFavorite ? ( + { + e.stopPropagation(); + removeProjectFromFavorites(workspace.id); + }} + /> + ) : ( + { + e.stopPropagation(); + addProjectToFavorites(workspace.id); + }} + /> + )} +
+ +
+ {workspace.description} +
+ +
+ {type === ProjectType.SecretManager && ( +
+ {workspace.environments?.length || 0} environments +
+ )} + +
+
+ ); + + const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events +
{ + router.push(`/project/${workspace.id}/secrets/overview`); + localStorage.setItem("projectData.id", workspace.id); + }} + key={workspace.id} + className={`min-w-72 group grid h-14 cursor-pointer grid-cols-6 border-t border-l border-r border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ + index === 0 && "rounded-t-md" + }`} + > +
+
{workspace.name}
+
+
+
+ {workspace.environments?.length || 0} environments +
+ {isFavorite ? ( + { + e.stopPropagation(); + removeProjectFromFavorites(workspace.id); + }} + /> + ) : ( + { + e.stopPropagation(); + addProjectToFavorites(workspace.id); + }} + /> + )} +
+
+ ); + + let projectsComponents: ReactNode; + + if (filteredWorkspaces.length || isProjectViewLoading) { + switch (projectsViewMode) { + case ProjectsViewMode.GRID: + projectsComponents = ( +
+ {isProjectViewLoading && + Array.apply(0, Array(3)).map((_x, i) => ( +
+
+ +
+
+ +
+
+ +
+
+ ))} + {!isProjectViewLoading && ( + <> + {workspacesWithFaveProp.map((workspace) => + renderProjectGridItem(workspace, workspace.isFavorite) + )} + + )} +
+ ); + + break; + case ProjectsViewMode.LIST: + default: + projectsComponents = ( +
+ {isProjectViewLoading && + Array.apply(0, Array(3)).map((_x, i) => ( +
+ +
+ ))} + {!isProjectViewLoading && + workspacesWithFaveProp.map((workspace, ind) => + renderProjectListItem(workspace, workspace.isFavorite, ind) + )} +
+ ); + break; + } + } else if (orgWorkspaces.length && searchFilter) { + projectsComponents = ( +
+ +
No projects match search...
+
+ ); + } + + return ( +
+ + {t("common.head-title", { title: t("settings.members.title") })} + + + {!serverDetails?.redisConfigured && ( +
+

Announcements

+
+ + Attention: Updated versions of Infisical now require Redis for full functionality. Learn + how to configure it + + + here + + + . +
+
+ )} +
+
+

{formatTitle(type)}

+
+
+ setSearchFilter(e.target.value)} + leftIcon={} + /> +
+ + + + + +
+
+ { + localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID); + setProjectsViewMode(ProjectsViewMode.GRID); + }} + ariaLabel="grid" + size="xs" + className={`${ + projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent" + } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} + > + + + { + localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST); + setProjectsViewMode(ProjectsViewMode.LIST); + }} + ariaLabel="list" + size="xs" + className={`${ + projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent" + } min-w-[2.4rem] border-none hover:bg-mineshaft-600`} + > + + +
+ + {(isAllowed) => ( + + )} + +
+ {projectsComponents} + {!isProjectViewLoading && Boolean(filteredWorkspaces.length) && ( + + )} + {isWorkspaceEmpty && ( +
+ +
+ You are not part of any projects in this organization yet. When you are, they will + appear here. +
+
+ Create a new project, or ask other organization members to give you necessary + permissions. +
+
+ )} +
+
+

Explore Infisical

+
+ {features.map((feature) => ( +
+
{feature.name}
+
+ {feature.description} +
+
+

+ Setup time: 20 min +

+ + Learn more{" "} + + +
+
+ ))} +
+
+ {!(new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000) && ( +
+

Onboarding Guide

+
+ + {orgWorkspaces.length !== 0 && ( + <> + + + + )} +
+ +
+
+ {orgWorkspaces.length !== 0 && ( +
+
+
+ + {false && ( +
+ +
+ )} +
+
Inject secrets locally
+
+ Replace .env files with a more secure and efficient alternative. +
+
+
+
+ About 2 min +
+
+ + {false &&
} +
+ )} + {orgWorkspaces.length !== 0 && ( + + )} +
+ )} + handlePopUpToggle("addNewWs", isOpen)} + projectType={type} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You have exceeded the number of projects allowed on the free plan." + /> + {/* */} +
+ ); +}; + +const SecretManagerOverviewPage = () => ; + +Object.assign(SecretManagerOverviewPage, { requireAuth: true }); + +export default SecretManagerOverviewPage; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index 38faf53f1..dc2989f18 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -71,7 +71,9 @@ export const AddOrgMemberModal = ({ const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? ""); const { data: serverDetails } = useFetchServerStatus(); const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg(); - const { data: projects, isLoading: isProjectsLoading } = useGetUserWorkspaces(true); + const { data: projects, isLoading: isProjectsLoading } = useGetUserWorkspaces({ + includeRoles: true + }); const { control, diff --git a/frontend/src/views/SecretOverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx b/frontend/src/views/SecretOverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx index 08f3eeaef..afea5305b 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretV2MigrationSection/SecretV2MigrationSection.tsx @@ -12,7 +12,7 @@ import { useProjectPermission, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useGetWorkspaceById, useMigrateProjectToV3, workspaceKeys } from "@app/hooks/api"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { ProjectVersion } from "@app/hooks/api/workspace/types"; +import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types"; enum ProjectUpgradeStatus { InProgress = "IN_PROGRESS", @@ -53,7 +53,7 @@ export const SecretV2MigrationSection = () => { if (isProjectUpgraded && migrateProjectToV3.data) { createNotification({ type: "success", text: "Project upgrade completed successfully" }); migrateProjectToV3.reset(); - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(ProjectType.SecretManager)); } }, [isProjectUpgraded, Boolean(migrateProjectToV3.data)]);