-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: External url button #158
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,12 +4,14 @@ import type { JobStatus } from "@ctrlplane/validators/jobs"; | |
import React, { Fragment } from "react"; | ||
import Link from "next/link"; | ||
import { usePathname } from "next/navigation"; | ||
import { IconLoader2 } from "@tabler/icons-react"; | ||
import { IconExternalLink, IconLoader2 } from "@tabler/icons-react"; | ||
import { capitalCase } from "change-case"; | ||
import _ from "lodash"; | ||
|
||
import { cn } from "@ctrlplane/ui"; | ||
import { buttonVariants } from "@ctrlplane/ui/button"; | ||
import { Table, TableBody, TableCell, TableRow } from "@ctrlplane/ui/table"; | ||
import { ReservedMetadataKey } from "@ctrlplane/validators/targets"; | ||
|
||
import { JobTableStatusIcon } from "~/app/[workspaceSlug]/_components/JobTableStatusIcon"; | ||
import { api } from "~/trpc/react"; | ||
|
@@ -56,70 +58,88 @@ export const TargetReleaseTable: React.FC<TargetReleaseTableProps> = ({ | |
)} | ||
</TableCell> | ||
</TableRow> | ||
{jobs.map((job, idx) => ( | ||
<TableRow | ||
key={job.id} | ||
className={cn( | ||
idx !== jobs.length - 1 && "border-b-neutral-800/50", | ||
)} | ||
> | ||
<TableCell className="hover:bg-neutral-800/55"> | ||
<Link | ||
href={`${pathname}?target_id=${job.target?.id}`} | ||
className="block w-full hover:text-blue-300" | ||
> | ||
{job.target?.name} | ||
</Link> | ||
</TableCell> | ||
<TableCell> | ||
<div className="flex items-center gap-1"> | ||
<JobTableStatusIcon status={job.job.status} /> | ||
{capitalCase(job.job.status)} | ||
</div> | ||
</TableCell> | ||
<TableCell>{job.type}</TableCell> | ||
<TableCell> | ||
{job.job.externalId != null ? ( | ||
<code className="font-mono text-xs"> | ||
{job.job.externalId} | ||
</code> | ||
) : ( | ||
<span className="text-sm text-muted-foreground"> | ||
No external ID | ||
</span> | ||
{jobs.map((job, idx) => { | ||
const linksMetadata = job.job.metadata.find( | ||
(m) => m.key === String(ReservedMetadataKey.Links), | ||
)?.value; | ||
|
||
const links = | ||
linksMetadata != null | ||
? (JSON.parse(linksMetadata) as Record<string, string>) | ||
: null; | ||
|
||
return ( | ||
<TableRow | ||
key={job.id} | ||
className={cn( | ||
idx !== jobs.length - 1 && "border-b-neutral-800/50", | ||
)} | ||
</TableCell> | ||
<TableCell> | ||
{job.job.externalUrl != null ? ( | ||
> | ||
<TableCell className="hover:bg-neutral-800/55"> | ||
<Link | ||
href={job.job.externalUrl} | ||
rel="nofollow noreferrer" | ||
target="_blank" | ||
href={`${pathname}?target_id=${job.target?.id}`} | ||
className="block w-full hover:text-blue-300" | ||
> | ||
{job.job.externalUrl} | ||
{job.target?.name} | ||
</Link> | ||
) : ( | ||
<span className="text-sm text-muted-foreground"> | ||
No external URL | ||
</span> | ||
)} | ||
</TableCell> | ||
<TableCell> | ||
<div className="flex justify-end"> | ||
<TargetDropdownMenu | ||
release={release} | ||
deploymentName={deploymentName} | ||
target={job.target} | ||
environmentId={job.environmentId} | ||
job={{ | ||
id: job.job.id, | ||
status: job.job.status as JobStatus, | ||
}} | ||
/> | ||
</div> | ||
</TableCell> | ||
</TableRow> | ||
))} | ||
</TableCell> | ||
<TableCell> | ||
<div className="flex items-center gap-1"> | ||
<JobTableStatusIcon status={job.job.status} /> | ||
{capitalCase(job.job.status)} | ||
</div> | ||
</TableCell> | ||
<TableCell>{job.type}</TableCell> | ||
<TableCell> | ||
{job.job.externalId != null ? ( | ||
<code className="font-mono text-xs"> | ||
{job.job.externalId} | ||
</code> | ||
) : ( | ||
<span className="text-sm text-muted-foreground"> | ||
No external ID | ||
</span> | ||
)} | ||
</TableCell> | ||
<TableCell> | ||
{links != null && ( | ||
<div className="flex items-center gap-1"> | ||
{Object.entries(links).map(([label, url]) => ( | ||
<Link | ||
key={label} | ||
href={url} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate URLs before rendering When rendering external links, ensure that the URLs are valid and safe. This helps prevent potential security risks such as XSS attacks. Consider validating the <Link
key={label}
- href={url}
+ href={(() => {
+ try {
+ const safeUrl = new URL(url);
+ return safeUrl.toString();
+ } catch {
+ console.error('Invalid URL:', url);
+ return '#';
+ }
+ })()}
target="_blank"
rel="noopener noreferrer"
className={buttonVariants({
variant: "secondary",
size: "sm",
className: "gap-1",
})}
>
|
||
target="_blank" | ||
rel="noopener noreferrer" | ||
className={buttonVariants({ | ||
variant: "secondary", | ||
size: "sm", | ||
className: "gap-1", | ||
})} | ||
> | ||
<IconExternalLink className="h-4 w-4" /> | ||
{label} | ||
</Link> | ||
))} | ||
</div> | ||
)} | ||
</TableCell> | ||
<TableCell> | ||
<div className="flex justify-end"> | ||
<TargetDropdownMenu | ||
release={release} | ||
deploymentName={deploymentName} | ||
target={job.target} | ||
environmentId={job.environmentId} | ||
job={{ | ||
id: job.job.id, | ||
status: job.job.status as JobStatus, | ||
}} | ||
/> | ||
</div> | ||
</TableCell> | ||
</TableRow> | ||
); | ||
})} | ||
</Fragment> | ||
); | ||
}) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ import { | |
environmentPolicyReleaseWindow, | ||
job, | ||
jobAgent, | ||
jobMetadata, | ||
release, | ||
releaseJobTrigger, | ||
system, | ||
|
@@ -189,6 +190,7 @@ const releaseJobTriggerRouter = createTRPCRouter({ | |
.input(z.string().uuid()) | ||
.query(({ ctx, input }) => | ||
releaseJobTriggerQuery(ctx.db) | ||
.leftJoin(jobMetadata, eq(jobMetadata.jobId, job.id)) | ||
.leftJoin( | ||
environmentPolicy, | ||
eq(environment.policyId, environmentPolicy.id), | ||
|
@@ -204,7 +206,10 @@ const releaseJobTriggerRouter = createTRPCRouter({ | |
.groupBy((row) => row.release_job_trigger.id) | ||
.map((v) => ({ | ||
...v[0]!.release_job_trigger, | ||
job: v[0]!.job, | ||
job: { | ||
...v[0]!.job, | ||
metadata: v.map((t) => t.job_metadata).filter(isPresent), | ||
}, | ||
Comment on lines
+209
to
+212
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure safe access to array elements and handle potential undefined values The usage of non-null assertion Apply this diff to safely access the first element of the array: - ...v[0]!.release_job_trigger,
+ ...(v[0]?.release_job_trigger || {}), Similarly, update the subsequent usages: - job: {
- ...v[0]!.job,
- metadata: v.map((t) => t.job_metadata).filter(isPresent),
- },
+ job: v[0]?.job
+ ? {
+ ...v[0].job,
+ metadata: v.map((t) => t.job_metadata).filter(isPresent),
+ }
+ : undefined, This ensures that you only access properties if
|
||
jobAgent: v[0]!.job_agent, | ||
target: v[0]!.target, | ||
release: { ...v[0]!.release, deployment: v[0]!.deployment }, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ALTER TABLE "job" DROP COLUMN IF EXISTS "external_url"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling for JSON parsing
If
linksMetadata
contains invalid JSON,JSON.parse
will throw an error, potentially causing the component to crash. Consider adding error handling to manage parsing errors gracefully.Apply this diff to add error handling:
📝 Committable suggestion