From abd8d6aa8af73b0421e649e0a0733ca9ade807b2 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 31 May 2024 23:18:02 +0800 Subject: [PATCH] feat: added support for version limit update --- .../src/server/routes/v1/project-router.ts | 38 ++++++++ .../src/services/project/project-service.ts | 33 ++++++- backend/src/services/project/project-types.ts | 5 ++ frontend/src/hooks/api/workspace/queries.tsx | 16 ++++ frontend/src/hooks/api/workspace/types.ts | 4 +- .../PointInTimeVersionLimitSection.tsx | 88 +++++++++++++++++++ .../PointInTimeVersionLimitSection/index.tsx | 1 + .../ProjectGeneralTab/ProjectGeneralTab.tsx | 2 + 8 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx create mode 100644 frontend/src/views/Settings/ProjectSettingsPage/components/PointInTimeVersionLimitSection/index.tsx diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 1cf655a97..0984b66f6 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -334,6 +334,44 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "PUT", + url: "/:workspaceSlug/version-limit", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + workspaceSlug: z.string().trim() + }), + body: z.object({ + pitVersionLimit: z.number().min(1).max(100) + }), + response: { + 200: z.object({ + message: z.string(), + workspace: ProjectsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const workspace = await server.services.project.updateVersionLimit({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + pitVersionLimit: req.body.pitVersionLimit, + workspaceSlug: req.params.workspaceSlug + }); + + return { + message: "Successfully changed workspace version limit", + workspace + }; + } + }); + server.route({ method: "GET", url: "/:workspaceId/integrations", diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 1e588dd08..ac6cfbc00 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -39,6 +39,7 @@ import { TToggleProjectAutoCapitalizationDTO, TUpdateProjectDTO, TUpdateProjectNameDTO, + TUpdateProjectVersionLimitDTO, TUpgradeProjectDTO } from "./project-types"; @@ -407,6 +408,35 @@ export const projectServiceFactory = ({ return updatedProject; }; + const updateVersionLimit = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + pitVersionLimit, + workspaceSlug + }: TUpdateProjectVersionLimitDTO) => { + const project = await projectDAL.findProjectBySlug(workspaceSlug, actorOrgId); + if (!project) { + throw new BadRequestError({ + message: "Project not found" + }); + } + + const { hasRole } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + + if (!hasRole(ProjectMembershipRole.Admin)) + throw new BadRequestError({ message: "Only admins are allowed to take this action" }); + + return projectDAL.updateById(project.id, { pitVersionLimit }); + }; + const updateName = async ({ projectId, actor, @@ -502,6 +532,7 @@ export const projectServiceFactory = ({ getAProject, toggleAutoCapitalization, updateName, - upgradeProject + upgradeProject, + updateVersionLimit }; }; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index dcd424e18..fbcfd2d9a 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -43,6 +43,11 @@ export type TToggleProjectAutoCapitalizationDTO = { autoCapitalization: boolean; } & TProjectPermission; +export type TUpdateProjectVersionLimitDTO = { + pitVersionLimit: number; + workspaceSlug: string; +} & Omit; + export type TUpdateProjectNameDTO = { name: string; } & TProjectPermission; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 71dbb8e01..6462490bb 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -20,6 +20,7 @@ import { TUpdateWorkspaceIdentityRoleDTO, TUpdateWorkspaceUserRoleDTO, UpdateEnvironmentDTO, + UpdatePitVersionLimitDTO, Workspace } from "./types"; @@ -249,6 +250,21 @@ export const useToggleAutoCapitalization = () => { }); }; +export const useUpdateWorkspaceVersionLimit = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, UpdatePitVersionLimitDTO>({ + mutationFn: ({ projectSlug, pitVersionLimit }) => { + return apiRequest.put(`/api/v1/workspace/${projectSlug}/version-limit`, { + pitVersionLimit + }); + }, + onSuccess: () => { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); + } + }); +}; + export const useDeleteWorkspace = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 8be9beed0..8c28d0938 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -16,6 +16,7 @@ export type Workspace = { upgradeStatus: string | null; autoCapitalization: boolean; environments: WorkspaceEnv[]; + pitVersionLimit: number; slug: string; }; @@ -48,6 +49,7 @@ export type CreateWorkspaceDTO = { }; export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string }; +export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean }; export type DeleteWorkspaceDTO = { workspaceID: string }; @@ -128,4 +130,4 @@ export type TUpdateWorkspaceGroupRoleDTO = { temporaryAccessStartTime: string; } )[]; -}; \ No newline at end of file +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx new file mode 100644 index 000000000..8c820b727 --- /dev/null +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/PointInTimeVersionLimitSection/PointInTimeVersionLimitSection.tsx @@ -0,0 +1,88 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { useProjectPermission, useWorkspace } from "@app/context"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { useUpdateWorkspaceVersionLimit } from "@app/hooks/api/workspace/queries"; + +const formSchema = z.object({ + pitVersionLimit: z.coerce.number().min(1).max(100) +}); + +type TForm = z.infer; + +export const PointInTimeVersionLimitSection = () => { + const { mutateAsync: updatePitVersion } = useUpdateWorkspaceVersionLimit(); + + const { currentWorkspace } = useWorkspace(); + const { membership } = useProjectPermission(); + + const { + control, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + values: { + pitVersionLimit: currentWorkspace?.pitVersionLimit || 10 + } + }); + + if (!currentWorkspace) return null; + + const handleVersionLimitSubmit = async ({ pitVersionLimit }: TForm) => { + try { + await updatePitVersion({ + pitVersionLimit, + projectSlug: currentWorkspace.slug + }); + + createNotification({ + text: "Successfully updated version limit", + type: "success" + }); + } catch (err) { + createNotification({ + text: "Failed updating project's version limit", + type: "error" + }); + } + }; + + const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); + return ( +
+
+

Point in Time Recovery

+
+

+ This defines the maximum number of folder snapshots, secret versions, and folder versions + that are retained by the system +

+
+
+ ( + + + + )} + /> +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/PointInTimeVersionLimitSection/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/PointInTimeVersionLimitSection/index.tsx new file mode 100644 index 000000000..242b8c79a --- /dev/null +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/PointInTimeVersionLimitSection/index.tsx @@ -0,0 +1 @@ +export { PointInTimeVersionLimitSection } from "./PointInTimeVersionLimitSection"; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx index 7d7c30fb0..511dff93e 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx @@ -3,6 +3,7 @@ import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSect import { DeleteProjectSection } from "../DeleteProjectSection"; import { E2EESection } from "../E2EESection"; import { EnvironmentSection } from "../EnvironmentSection"; +import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection"; import { ProjectNameChangeSection } from "../ProjectNameChangeSection"; import { SecretTagsSection } from "../SecretTagsSection"; @@ -14,6 +15,7 @@ export const ProjectGeneralTab = () => { +