diff --git a/apps/webservice/src/app/[workspaceSlug]/(appv2)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/jobs/release-table/EnvironmentRowDropdown.tsx b/apps/webservice/src/app/[workspaceSlug]/(appv2)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/jobs/release-table/EnvironmentRowDropdown.tsx new file mode 100644 index 000000000..9f7245b9a --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(appv2)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/jobs/release-table/EnvironmentRowDropdown.tsx @@ -0,0 +1,35 @@ +import React, { useState } from "react"; +import { IconAdjustmentsExclamation } from "@tabler/icons-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@ctrlplane/ui/dropdown-menu"; + +import { OverrideJobStatusDialog } from "~/app/[workspaceSlug]/(appv2)/_components/job/JobDropdownMenu"; + +export const EnvironmentRowDropdown: React.FC<{ + jobIds: string[]; + children: React.ReactNode; +}> = ({ jobIds, children }) => { + const [open, setOpen] = useState(false); + + return ( + + {children} + + setOpen(false)}> + e.preventDefault()} + className="space-x-2" + > + +

Override Job Status

+
+
+
+
+ ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(appv2)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/jobs/release-table/ResourceReleaseTable.tsx b/apps/webservice/src/app/[workspaceSlug]/(appv2)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/jobs/release-table/ResourceReleaseTable.tsx index a5b167149..f6f6fc800 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(appv2)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/jobs/release-table/ResourceReleaseTable.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(appv2)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/jobs/release-table/ResourceReleaseTable.tsx @@ -40,6 +40,7 @@ import { useFilter } from "~/app/[workspaceSlug]/(appv2)/_hooks/useFilter"; import { Sidebars } from "~/app/[workspaceSlug]/sidebars"; import { api } from "~/trpc/react"; import { EnvironmentApprovalRow } from "./EnvironmentApprovalRow"; +import { EnvironmentRowDropdown } from "./EnvironmentRowDropdown"; type Trigger = RouterOutputs["job"]["config"]["byReleaseId"][number]; @@ -75,6 +76,7 @@ const CollapsibleTableRow: React.FC = ({ ); const allTriggers = Object.values(triggersByResource).flat(); + const allJobIds = allTriggers.map((t) => t.job.id); const latestStatusesByResource = Object.entries(triggersByResource).map( ([_, triggers]) => { const sortedByCreatedAt = triggers.sort( @@ -170,6 +172,12 @@ const CollapsibleTableRow: React.FC = ({ release={release} /> ))} + + + + diff --git a/apps/webservice/src/app/[workspaceSlug]/(appv2)/_components/job/JobDropdownMenu.tsx b/apps/webservice/src/app/[workspaceSlug]/(appv2)/_components/job/JobDropdownMenu.tsx index c4f3fa612..b6d821490 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(appv2)/_components/job/JobDropdownMenu.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(appv2)/_components/job/JobDropdownMenu.tsx @@ -69,13 +69,13 @@ const overrideJobStatusFormSchema = z.object({ status: z.nativeEnum(JobStatus), }); -const OverrideJobStatusDialog: React.FC<{ - job: { id: string; status: JobStatus }; +export const OverrideJobStatusDialog: React.FC<{ + jobIds: string[]; onClose: () => void; children: React.ReactNode; -}> = ({ job, onClose, children }) => { +}> = ({ jobIds, onClose, children }) => { const [open, setOpen] = useState(false); - const updateJob = api.job.update.useMutation(); + const updateJobs = api.job.updateMany.useMutation(); const utils = api.useUtils(); const form = useForm({ @@ -84,10 +84,10 @@ const OverrideJobStatusDialog: React.FC<{ }); const onSubmit = form.handleSubmit((data) => - updateJob - .mutateAsync({ id: job.id, data }) + updateJobs + .mutateAsync({ ids: jobIds, data }) .then(() => utils.job.config.byReleaseId.invalidate()) - .then(() => utils.job.config.byId.invalidate(job.id)) + .then(() => jobIds.map((id) => utils.job.config.byId.invalidate(id))) .then(() => utils.release.list.invalidate()) .then(() => setOpen(false)) .then(() => onClose()), @@ -139,9 +139,9 @@ const OverrideJobStatusDialog: React.FC<{ )} /> - {updateJob.error != null && ( + {updateJobs.error != null && (
- {updateJob.error.message} + {updateJobs.error.message}
)} @@ -153,7 +153,7 @@ const OverrideJobStatusDialog: React.FC<{ @@ -359,7 +359,10 @@ export const JobDropdownMenu: React.FC<{ )} - setOpen(false)}> + setOpen(false)} + > e.preventDefault()} className="space-x-2" diff --git a/packages/api/src/router/job.ts b/packages/api/src/router/job.ts index 667ac64d5..369413d7d 100644 --- a/packages/api/src/router/job.ts +++ b/packages/api/src/router/job.ts @@ -782,6 +782,25 @@ export const jobRouter = createTRPCRouter({ .input(z.object({ id: z.string().uuid(), data: schema.updateJob })) .mutation(({ ctx, input }) => updateJob(ctx.db, input.id, input.data)), + updateMany: protectedProcedure + .input( + z.object({ ids: z.array(z.string().uuid()), data: schema.updateJob }), + ) + .meta({ + authorizationCheck: ({ canUser, input }) => { + const jobIds: string[] = input.ids; + const authzPromises = jobIds.map((id) => + canUser.perform(Permission.JobUpdate).on({ type: "job", id }), + ); + return Promise.all(authzPromises).then((results) => + results.every(Boolean), + ); + }, + }) + .mutation(({ ctx, input }) => + Promise.all(input.ids.map((id) => updateJob(ctx.db, id, input.data))), + ), + config: releaseJobTriggerRouter, agent: jobAgentRouter, trigger: jobTriggerRouter,