Merge branch 'site-fixes' into fast-push
18 files+133−610/18 viewed
| 436 | 436 | g1t.page manage their apps under **On g1t.page** on the same page. | |
| 437 | 437 | - **A deployment's page**, `g1t.sh/<owner>/<project>/deployments/<id>`, | |
| 438 | 438 | shows its statuses in order, links to its log and run, and its payload. | |
| 439 | + | Only the newest status can show as still under way; a `queued` or | |
| 440 | + | `in_progress` one that a later status followed shows as over. | |
| 441 | + | - **Every link to where something runs**, a g1t.page address, an | |
| 442 | + | environment's URL, a pull request's preview or a project's homepage, | |
| 443 | + | opens in a new tab, wherever on the site it appears. | |
| 439 | 444 | - **The repository's code page** and **the project's overview** show a | |
| 440 | 445 | **Deployments** panel with how many there are, and each environment's | |
| 441 | 446 | latest deployment and when. It links to the Deployments page. |
| 195 | 195 | at most twice, or as often as the repository's **Revisions before asking | |
| 196 | 196 | you** allows. | |
| 197 | 197 | 4. **Ready to merge.** The required checks passed and it is approved. | |
| 198 | − | Merging is yours, unless the repository says otherwise (below). | |
| 198 | + | Merging is yours, unless the repository says otherwise (below). Until | |
| 199 | + | you merge it, Home and Code's overview list it as waiting for you to | |
| 200 | + | merge it, with a link to **Merge automatically when ready** in the | |
| 201 | + | repository's branch settings for a pull request g1t made. | |
| 199 | 202 | ||
| 200 | 203 | Agents are told to run the same tests and linters the workflows run before | |
| 201 | 204 | they finish, so most failures are caught in the sandbox. What "done" means |
| 1 | 1 | /** Small pieces every page about deployments shares. */ | |
| 2 | + | import type { AnchorHTMLAttributes } from "react"; | |
| 3 | + | ||
| 2 | 4 | import type { DeployStatus } from "@g1t/contracts"; | |
| 3 | 5 | ||
| 4 | 6 | export const DEPLOY_STATUS: Record<DeployStatus, { label: string; tone: string }> = { | |
| ⋯ | |||
| 25 | 27 | export function host(url: string): string { | |
| 26 | 28 | return url.replace(/^https?:\/\//, ""); | |
| 27 | 29 | } | |
| 30 | + | ||
| 31 | + | /** | |
| 32 | + | * A link to where something runs: a g1t.page address, an environment's | |
| 33 | + | * URL, a preview, a project's homepage. It always opens in a new tab, so | |
| 34 | + | * the page about it stays open behind it. `rel` adds to noopener and | |
| 35 | + | * noreferrer (nofollow, say), never replaces them. | |
| 36 | + | */ | |
| 37 | + | export function DeployLink({ rel, ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) { | |
| 38 | + | return <a {...props} target="_blank" rel={rel ? `noopener noreferrer ${rel}` : "noopener noreferrer"} />; | |
| 39 | + | } | |
| 3 | 3 | * latest deployment and when, linking to the Deployments page. Also the | |
| 4 | 4 | * state icon and badge every page about deployments shares. | |
| 5 | 5 | */ | |
| 6 | − | import { Check, CircleDashed, Clock, Loader2, Rocket, X } from "lucide-react"; | |
| 6 | + | import { Check, CircleDashed, CircleDot, Clock, Loader2, Rocket, X } from "lucide-react"; | |
| 7 | 7 | import { Link } from "react-router"; | |
| 8 | 8 | ||
| 9 | 9 | import type { DeploymentEnvironments, DeploymentSource, DeploymentState } from "@g1t/contracts"; | |
| ⋯ | |||
| 26 | 26 | }; | |
| 27 | 27 | ||
| 28 | 28 | /** A deployment's state as an icon: a check, a cross, a spinner, a clock. */ | |
| 29 | − | export function DeploymentStateIcon({ state, size = 16, className }: { state: DeploymentState; size?: number; className?: string }) { | |
| 29 | + | export function DeploymentStateIcon({ | |
| 30 | + | state, | |
| 31 | + | size = 16, | |
| 32 | + | className, | |
| 33 | + | over = false, | |
| 34 | + | }: { | |
| 35 | + | state: DeploymentState; | |
| 36 | + | size?: number; | |
| 37 | + | className?: string; | |
| 38 | + | /** A past status a later one followed: under way then, over now, so it does not spin. */ | |
| 39 | + | over?: boolean; | |
| 40 | + | }) { | |
| 30 | 41 | const label = STATE_WORD[state]; | |
| 31 | 42 | switch (state) { | |
| 32 | 43 | case "success": | |
| ⋯ | |||
| 43 | 54 | </span> | |
| 44 | 55 | ); | |
| 45 | 56 | case "in_progress": | |
| 57 | + | if (over) return <CircleDot size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />; | |
| 46 | 58 | return <Loader2 size={size} aria-label={label} className={cn("shrink-0 animate-spin text-warn", className)} />; | |
| 47 | 59 | case "queued": | |
| 48 | 60 | return <Clock size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />; | |
| 2 | 2 | import { useEffect, useRef, useState } from "react"; | |
| 3 | 3 | ||
| 4 | 4 | import { cn } from "../lib/cn"; | |
| 5 | + | import { DeployLink } from "./deploy"; | |
| 5 | 6 | ||
| 6 | 7 | /** | |
| 7 | 8 | * A thumbnail of production as it is now, linking to it. The screenshot is | |
| ⋯ | |||
| 58 | 59 | ); | |
| 59 | 60 | ||
| 60 | 61 | return href ? ( | |
| 61 | − | <a href={href} className={cn("group block", className)} aria-label={`Visit ${label}`}> | |
| 62 | + | <DeployLink href={href} className={cn("group block", className)} aria-label={`Visit ${label}`}> | |
| 62 | 63 | {frame} | |
| 63 | − | </a> | |
| 64 | + | </DeployLink> | |
| 64 | 65 | ) : ( | |
| 65 | 66 | <div className={className}>{frame}</div> | |
| 66 | 67 | ); | |
| 6 | 6 | ||
| 7 | 7 | import { cn } from "../lib/cn"; | |
| 8 | 8 | import { KIND_CHOICES, type KindChoice, type ShownLink, choiceOf, linksToShow } from "../lib/project-kind"; | |
| 9 | + | import { DeployLink } from "./deploy"; | |
| 9 | 10 | import { Input, SubmitButton } from "./ui"; | |
| 10 | 11 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; | |
| 11 | 12 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "./ui/dropdown-menu"; | |
| ⋯ | |||
| 30 | 31 | <ul className={cn("space-y-1.5 text-sm", className)}> | |
| 31 | 32 | {list.map((link) => ( | |
| 32 | 33 | <li key={link.key} className="min-w-0"> | |
| 33 | − | <a | |
| 34 | + | <DeployLink | |
| 34 | 35 | href={link.url} | |
| 35 | − | rel="noopener noreferrer nofollow" | |
| 36 | + | rel="nofollow" | |
| 36 | 37 | className="flex min-w-0 items-center gap-2 text-fg-soft hover:text-accent [&_svg]:shrink-0 [&_svg]:text-faint" | |
| 37 | 38 | > | |
| 38 | 39 | {LINK_ICON[link.type]} | |
| 39 | 40 | <span className="truncate">{link.label}</span> | |
| 40 | − | </a> | |
| 41 | + | </DeployLink> | |
| 41 | 42 | </li> | |
| 42 | 43 | ))} | |
| 43 | 44 | </ul> | |
| 10 | 10 | ||
| 11 | 11 | import type { DeployStatus, Project } from "@g1t/contracts"; | |
| 12 | 12 | ||
| 13 | − | import { host, StatusDot } from "./deploy"; | |
| 13 | + | import { DeployLink, host, StatusDot } from "./deploy"; | |
| 14 | 14 | import { ButtonLink, Input, SubmitButton, TimeAgo } from "./ui"; | |
| 15 | 15 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; | |
| 16 | 16 | import { Hint } from "./ui/hint"; | |
| ⋯ | |||
| 66 | 66 | ||
| 67 | 67 | function BigLink({ url }: { url: string }) { | |
| 68 | 68 | return ( | |
| 69 | − | <a href={url} rel="noopener noreferrer" className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-lg font-medium hover:text-accent"> | |
| 69 | + | <DeployLink href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-lg font-medium hover:text-accent"> | |
| 70 | 70 | <span className="truncate">{host(url).replace(/\/$/, "")}</span> | |
| 71 | 71 | <ArrowUpRight size={16} className="shrink-0 text-faint" /> | |
| 72 | − | </a> | |
| 72 | + | </DeployLink> | |
| 73 | 73 | ); | |
| 74 | 74 | } | |
| 75 | 75 | ||
| ⋯ | |||
| 190 | 190 | </div> | |
| 191 | 191 | {url && ( | |
| 192 | 192 | <div className="flex shrink-0 items-center gap-2"> | |
| 193 | − | <ButtonLink to={url} variant="accent" reloadDocument> | |
| 193 | + | <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer"> | |
| 194 | 194 | Visit | |
| 195 | 195 | <ArrowUpRight size={14} /> | |
| 196 | 196 | </ButtonLink> | |
| ⋯ | |||
| 291 | 291 | </div> | |
| 292 | 292 | {url && ( | |
| 293 | 293 | <div className="flex shrink-0 items-center gap-2"> | |
| 294 | − | <ButtonLink to={url} variant="accent" reloadDocument> | |
| 294 | + | <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer"> | |
| 295 | 295 | Read | |
| 296 | 296 | <ArrowUpRight size={14} /> | |
| 297 | 297 | </ButtonLink> | |
| ⋯ | |||
| 329 | 329 | if (!url) return null; | |
| 330 | 330 | return ( | |
| 331 | 331 | <Hint label="Its homepage"> | |
| 332 | − | <a href={url} rel="noopener noreferrer" className="inline-flex items-center gap-1 text-faint hover:text-fg"> | |
| 332 | + | <DeployLink href={url} className="inline-flex items-center gap-1 text-faint hover:text-fg"> | |
| 333 | 333 | <Globe size={12} /> | |
| 334 | 334 | {host(url).replace(/\/$/, "")} | |
| 335 | − | </a> | |
| 335 | + | </DeployLink> | |
| 336 | 336 | </Hint> | |
| 337 | 337 | ); | |
| 338 | 338 | } | |
| 3 | 3 | import { Link } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | import { Duration, StatusIcon } from "./actions"; | |
| 6 | + | import { DeployLink } from "./deploy"; | |
| 6 | 7 | import { GRAPH, type GraphJob, type EdgeState, type PlacedGroup, type PlacedNode, type Standing, layoutRun, sharedUrl, standingOf, unitStanding } from "../lib/run-graph"; | |
| 7 | 8 | ||
| 8 | 9 | /** A job calling a workflow shows as running while its jobs do. */ | |
| ⋯ | |||
| 46 | 47 | /** Where a deployment is, under its node's name. */ | |
| 47 | 48 | function Address({ url }: { url: string }) { | |
| 48 | 49 | return ( | |
| 49 | − | <a | |
| 50 | + | <DeployLink | |
| 50 | 51 | href={url} | |
| 51 | − | target="_blank" | |
| 52 | − | rel="noreferrer" | |
| 53 | 52 | className="-mt-2 flex items-center gap-1 truncate pr-3 pl-9 font-mono text-xs text-accent hover:underline" | |
| 54 | 53 | style={{ height: GRAPH.urlHeight }} | |
| 55 | 54 | > | |
| 56 | 55 | <span className="truncate">{url.replace(/^https?:\/\//, "")}</span> | |
| 57 | 56 | <ExternalLink size={11} className="shrink-0" /> | |
| 58 | − | </a> | |
| 57 | + | </DeployLink> | |
| 59 | 58 | ); | |
| 60 | 59 | } | |
| 61 | 60 | ||
| 12 | 12 | groupBuilds, | |
| 13 | 13 | hasPayload, | |
| 14 | 14 | isFiltered, | |
| 15 | + | isNewestStatus, | |
| 15 | 16 | isPageBuild, | |
| 16 | 17 | orderEnvironments, | |
| 17 | 18 | pageRange, | |
| ⋯ | |||
| 195 | 196 | assert.equal(hasPayload({ region: "wnam" }), true); | |
| 196 | 197 | assert.equal(hasPayload(null), false); | |
| 197 | 198 | }); | |
| 199 | + | ||
| 200 | + | test("only a deployment's newest status can still be under way", () => { | |
| 201 | + | const statuses = [ | |
| 202 | + | { created_at: "2026-10-08T10:00:00.000Z" }, // Deploying, building | |
| 203 | + | { created_at: "2026-10-08T10:02:00.000Z" }, // Deployed | |
| 204 | + | ]; | |
| 205 | + | assert.equal(isNewestStatus(statuses, 0), false); | |
| 206 | + | assert.equal(isNewestStatus(statuses, 1), true); | |
| 207 | + | // Listed out of order: the newest is still the latest written. | |
| 208 | + | assert.equal(isNewestStatus([...statuses].reverse(), 0), true); | |
| 209 | + | // Written in the same moment: the last listed is the newest. | |
| 210 | + | const tied = [{ created_at: "2026-10-08T10:00:00.000Z" }, { created_at: "2026-10-08T10:00:00.000Z" }]; | |
| 211 | + | assert.equal(isNewestStatus(tied, 0), false); | |
| 212 | + | assert.equal(isNewestStatus(tied, 1), true); | |
| 213 | + | assert.equal(isNewestStatus([], 0), false); | |
| 214 | + | }); | |
| 9 | 9 | DeploymentFilter, | |
| 10 | 10 | DeploymentSource, | |
| 11 | 11 | DeploymentState, | |
| 12 | + | DeploymentStatus, | |
| 12 | 13 | RepoDeployment, | |
| 13 | 14 | } from "@g1t/contracts"; | |
| 14 | 15 | ||
| ⋯ | |||
| 210 | 211 | export function hasPayload(payload: Record<string, unknown> | null | undefined): boolean { | |
| 211 | 212 | return payload != null && typeof payload === "object" && Object.keys(payload).length > 0; | |
| 212 | 213 | } | |
| 214 | + | ||
| 215 | + | /** | |
| 216 | + | * Whether a status on a deployment's timeline is its newest: the latest | |
| 217 | + | * one written (the last listed, when two share a time). Only the newest | |
| 218 | + | * can still be under way; one a later status followed is over, whatever | |
| 219 | + | * it said, so it is never shown as running. | |
| 220 | + | */ | |
| 221 | + | export function isNewestStatus(statuses: readonly Pick<DeploymentStatus, "created_at">[], index: number): boolean { | |
| 222 | + | const at = statuses[index]?.created_at; | |
| 223 | + | if (at == null) return false; | |
| 224 | + | return statuses.every((other, i) => i === index || (i < index ? other.created_at <= at : other.created_at < at)); | |
| 225 | + | } | |
| 393 | 393 | key, | |
| 394 | 394 | kind: "ready", | |
| 395 | 395 | title: pull.title, | |
| 396 | − | detail: lowConfidence ? confidenceAsk(lowConfidence) : "Its required checks passed and it was approved. It lands when you merge it.", | |
| 396 | + | // The ask first: a list cuts the sentence short, and "it lands" alone reads as stuck. | |
| 397 | + | detail: lowConfidence ? confidenceAsk(lowConfidence) : "Waiting for you to merge it. It was approved and its required checks passed.", | |
| 397 | 398 | to, | |
| 398 | 399 | action: "Merge", | |
| 399 | 400 | at: Date.parse(pull.updatedAt), | |
| ⋯ | |||
| 406 | 407 | fields: { action: "merge" }, | |
| 407 | 408 | done: "Merging", | |
| 408 | 409 | }), | |
| 410 | + | // This repository leaves landing an agent's change to a person; say where that is changed. | |
| 411 | + | link: isAgent(pull.agent) ? { label: "Merge automatically when ready", to: `/${repo.namespace}/${repo.name}/settings/branches` } : null, | |
| 409 | 412 | }); | |
| 410 | 413 | } else if (!lifecycle && pull.status === "open" && pull.checkStatus === "failed") { | |
| 411 | 414 | // Taken out of the merge queue: its change failed combined with what was ahead. | |
| 7 | 7 | import type { Route } from "./+types/explore"; | |
| 8 | 8 | import { EmptyState, Pill, TimeAgo, notACredential } from "../components/ui"; | |
| 9 | 9 | import { page } from "../lib/meta"; | |
| 10 | − | import { host } from "../components/deploy"; | |
| 10 | + | import { DeployLink, host } from "../components/deploy"; | |
| 11 | 11 | import { projects, search } from "../lib/services.server"; | |
| 12 | 12 | import { getViewer } from "../lib/session.server"; | |
| 13 | 13 | ||
| ⋯ | |||
| 66 | 66 | </span> | |
| 67 | 67 | <span className="mt-2 line-clamp-2 grow text-sm text-muted">{repo.description ?? "No description."}</span> | |
| 68 | 68 | {link && ( | |
| 69 | − | <a | |
| 69 | + | <DeployLink | |
| 70 | 70 | href={link} | |
| 71 | − | rel="noopener noreferrer nofollow" | |
| 71 | + | rel="nofollow" | |
| 72 | 72 | className="relative z-10 mt-2 flex min-w-0 items-center gap-1 self-start font-mono text-xs text-muted hover:text-accent" | |
| 73 | 73 | > | |
| 74 | 74 | <span className="truncate">{host(link).replace(/\/$/, "")}</span> | |
| 75 | 75 | <ArrowUpRight size={11} className="shrink-0" /> | |
| 76 | − | </a> | |
| 76 | + | </DeployLink> | |
| 77 | 77 | )} | |
| 78 | 78 | {repo.topics.length > 0 && ( | |
| 79 | 79 | <span className="mt-3 flex flex-wrap gap-1.5"> | |
| 9 | 9 | import { TimeAgo } from "../../components/ui"; | |
| 10 | 10 | import { Badge } from "../../components/ui/badge"; | |
| 11 | 11 | import { Hint } from "../../components/ui/hint"; | |
| 12 | − | import { host } from "../../components/deploy"; | |
| 12 | + | import { DeployLink, host } from "../../components/deploy"; | |
| 13 | 13 | import { DeploymentStateBadge, DeploymentStateIcon, STATE_TONE, sourceLabel } from "../../components/deployments-panel"; | |
| 14 | 14 | import { deployments } from "../../lib/services.server"; | |
| 15 | 15 | import { getViewer } from "../../lib/session.server"; | |
| 16 | 16 | import { requireRepo } from "../../lib/access.server"; | |
| 17 | − | import { STATE_WORD, buildError, environmentLabel, hasPayload, isPageBuild, shortSha } from "../../lib/deployments"; | |
| 17 | + | import { STATE_WORD, buildError, environmentLabel, hasPayload, isNewestStatus, isPageBuild, shortSha } from "../../lib/deployments"; | |
| 18 | 18 | ||
| 19 | 19 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 20 | 20 | return page(args, { title: `Deployment · ${params.owner}/${params.repo} · g1t` }); | |
| ⋯ | |||
| 89 | 89 | </p> | |
| 90 | 90 | </div> | |
| 91 | 91 | {url && ( | |
| 92 | − | <a | |
| 92 | + | <DeployLink | |
| 93 | 93 | href={url} | |
| 94 | 94 | className="inline-flex max-w-full items-center gap-1.5 rounded-md bg-accent px-3.5 py-2 text-sm font-medium text-bg hover:bg-accent-hover" | |
| 95 | 95 | > | |
| 96 | 96 | <span className="truncate">{host(url)}</span> | |
| 97 | 97 | <ExternalLink size={14} className="shrink-0" /> | |
| 98 | − | </a> | |
| 98 | + | </DeployLink> | |
| 99 | 99 | )} | |
| 100 | 100 | </header> | |
| 101 | 101 | ||
| ⋯ | |||
| 169 | 169 | ) : ( | |
| 170 | 170 | <ol className="mt-4"> | |
| 171 | 171 | {deployment.statuses.map((status, index) => ( | |
| 172 | − | <StatusItem key={status.id} status={status} last={index === deployment.statuses.length - 1} /> | |
| 172 | + | <StatusItem | |
| 173 | + | key={status.id} | |
| 174 | + | status={status} | |
| 175 | + | last={index === deployment.statuses.length - 1} | |
| 176 | + | over={!isNewestStatus(deployment.statuses, index)} | |
| 177 | + | /> | |
| 173 | 178 | ))} | |
| 174 | 179 | </ol> | |
| 175 | 180 | )} | |
| ⋯ | |||
| 201 | 206 | } | |
| 202 | 207 | ||
| 203 | 208 | /** One status on the timeline, oldest at the top, with a line down to the next. */ | |
| 204 | − | function StatusItem({ status, last }: { status: DeploymentStatus; last: boolean }) { | |
| 209 | + | function StatusItem({ status, last, over }: { status: DeploymentStatus; last: boolean; over: boolean }) { | |
| 205 | 210 | return ( | |
| 206 | 211 | <li className="relative flex gap-3 pb-5 last:pb-0"> | |
| 207 | 212 | {!last && <span aria-hidden="true" className="absolute top-6 bottom-1 left-[9px] w-px bg-line" />} | |
| 208 | 213 | <span className="relative mt-0.5 flex size-[19px] shrink-0 items-center justify-center bg-bg"> | |
| 209 | − | <DeploymentStateIcon state={status.state} size={18} /> | |
| 214 | + | <DeploymentStateIcon state={status.state} size={18} over={over} /> | |
| 210 | 215 | </span> | |
| 211 | 216 | <div className="min-w-0 grow"> | |
| 212 | 217 | <p className="flex flex-wrap items-baseline gap-x-2 text-sm"> | |
| 213 | − | <span className={`font-medium ${STATE_TONE[status.state]}`}>{STATE_WORD[status.state]}</span> | |
| 218 | + | <span className={`font-medium ${over && status.state === "in_progress" ? "text-muted" : STATE_TONE[status.state]}`}>{STATE_WORD[status.state]}</span> | |
| 214 | 219 | <span className="text-xs text-faint"> | |
| 215 | 220 | {status.creator} ·{" "} | |
| 216 | 221 | <Hint label={new Date(status.created_at).toUTCString()}> | |
| ⋯ | |||
| 224 | 229 | {(status.environment_url || status.log_url) && ( | |
| 225 | 230 | <p className="mt-1 flex flex-wrap gap-x-4 gap-y-0.5 text-xs"> | |
| 226 | 231 | {status.environment_url && ( | |
| 227 | − | <a href={status.environment_url} className="inline-flex min-w-0 items-center gap-1 font-mono text-accent hover:underline"> | |
| 232 | + | <DeployLink href={status.environment_url} className="inline-flex min-w-0 items-center gap-1 font-mono text-accent hover:underline"> | |
| 228 | 233 | <span className="truncate">{host(status.environment_url)}</span> | |
| 229 | 234 | <ExternalLink size={11} className="shrink-0" /> | |
| 230 | − | </a> | |
| 235 | + | </DeployLink> | |
| 231 | 236 | )} | |
| 232 | 237 | {status.log_url && ( | |
| 233 | 238 | <a href={status.log_url} className="inline-flex items-center gap-1 text-muted hover:text-fg"> | |
| ⋯ | |||
| 265 | 270 | {BUILD_WORDS[build.status]} · started <TimeAgo at={build.createdAt} /> by {build.createdBy} | |
| 266 | 271 | </p> | |
| 267 | 272 | {build.status === "ready" && ( | |
| 268 | − | <a href={build.url} className="mt-3 inline-flex items-center gap-1.5 font-mono text-sm text-accent hover:underline"> | |
| 273 | + | <DeployLink href={build.url} className="mt-3 inline-flex items-center gap-1.5 font-mono text-sm text-accent hover:underline"> | |
| 269 | 274 | {host(build.url)} | |
| 270 | 275 | <ExternalLink size={12} /> | |
| 271 | − | </a> | |
| 276 | + | </DeployLink> | |
| 272 | 277 | )} | |
| 273 | 278 | </> | |
| 274 | 279 | ); | |
| 16 | 16 | ||
| 17 | 17 | import type { DeploymentEnvironment, FeatureState, RepoDeployment } from "@g1t/contracts"; | |
| 18 | 18 | ||
| 19 | − | import { host } from "../../components/deploy"; | |
| 19 | + | import { DeployLink, host } from "../../components/deploy"; | |
| 20 | 20 | import { DeploymentStateBadge, DeploymentStateIcon, sourceLabel } from "../../components/deployments-panel"; | |
| 21 | 21 | import { FilterMenu } from "../../components/labels"; | |
| 22 | 22 | ||
| ⋯ | |||
| 156 | 156 | </p> | |
| 157 | 157 | </div> | |
| 158 | 158 | {productionUrl && ( | |
| 159 | − | <a | |
| 159 | + | <DeployLink | |
| 160 | 160 | href={productionUrl} | |
| 161 | 161 | className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-line px-3 py-1.5 font-mono text-xs text-muted hover:border-line-strong hover:text-fg" | |
| 162 | 162 | > | |
| 163 | 163 | <Globe size={13} className="shrink-0" /> | |
| 164 | 164 | <span className="truncate">{host(productionUrl)}</span> | |
| 165 | − | </a> | |
| 165 | + | </DeployLink> | |
| 166 | 166 | )} | |
| 167 | 167 | </header> | |
| 168 | 168 | ||
| ⋯ | |||
| 297 | 297 | {latest && <DeploymentStateBadge state={latest.state} />} | |
| 298 | 298 | </div> | |
| 299 | 299 | {url ? ( | |
| 300 | − | <a href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline"> | |
| 300 | + | <DeployLink href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline"> | |
| 301 | 301 | <span className="truncate">{host(url)}</span> | |
| 302 | 302 | <ExternalLink size={12} className="shrink-0" /> | |
| 303 | − | </a> | |
| 303 | + | </DeployLink> | |
| 304 | 304 | ) : ( | |
| 305 | 305 | <p className="mt-2 text-sm text-faint">No address</p> | |
| 306 | 306 | )} | |
| ⋯ | |||
| 652 | 652 | </Link> | |
| 653 | 653 | )} | |
| 654 | 654 | </span> | |
| 655 | − | <a href={app.url} className="hidden min-w-0 truncate font-mono text-xs text-muted hover:text-fg sm:block"> | |
| 655 | + | <DeployLink href={app.url} className="hidden min-w-0 truncate font-mono text-xs text-muted hover:text-fg sm:block"> | |
| 656 | 656 | {host(app.url)} | |
| 657 | − | </a> | |
| 657 | + | </DeployLink> | |
| 658 | 658 | <span className="ml-auto shrink-0">{can.run && <AppActions branch={app.branch} up compact />}</span> | |
| 659 | 659 | </li> | |
| 660 | 660 | ))} | |
| ⋯ | |||
| 776 | 776 | <p className="mt-0.5 text-xs text-faint">{off ? "Off for this repository." : hint}</p> | |
| 777 | 777 | {app && href ? ( | |
| 778 | 778 | <> | |
| 779 | − | <a href={href} className="mt-3 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline"> | |
| 779 | + | <DeployLink href={href} className="mt-3 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline"> | |
| 780 | 780 | <span className="truncate">{host(href)}</span> | |
| 781 | 781 | <ExternalLink size={12} className="shrink-0" /> | |
| 782 | − | </a> | |
| 782 | + | </DeployLink> | |
| 783 | 783 | <p className="mt-1 text-xs text-faint"> | |
| 784 | 784 | <span className="font-mono">{app.commit.slice(0, 8)}</span> · deployed <TimeAgo at={app.deployedAt} /> | |
| 785 | 785 | </p> | |
| 39 | 39 | ||
| 40 | 40 | import type { Route } from "./+types/overview"; | |
| 41 | 41 | import { InlineMarkdown } from "../../components/inline-markdown"; | |
| 42 | − | import { host, StatusDot } from "../../components/deploy"; | |
| 42 | + | import { DeployLink, host, StatusDot } from "../../components/deploy"; | |
| 43 | 43 | import { CheckBadge } from "../../components/checks"; | |
| 44 | 44 | import { Elapsed, formatCost, useLiveRefresh } from "../../components/agents"; | |
| 45 | 45 | import { ActivityFeed, DeployStrip, Meter, NeedsList, Panel, Quiet, Unavailable, percent } from "../../components/mission"; | |
| ⋯ | |||
| 822 | 822 | <div className="flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-line px-5 py-2.5 text-xs sm:px-6"> | |
| 823 | 823 | <KindMenu project={project} label={kindLabel(project)} canChange={canChange} /> | |
| 824 | 824 | {stripLinks.map((link) => ( | |
| 825 | − | <a key={link.key} href={link.url} rel="noopener noreferrer" className="inline-flex min-w-0 items-center gap-1.5 text-muted hover:text-fg"> | |
| 825 | + | <DeployLink key={link.key} href={link.url} className="inline-flex min-w-0 items-center gap-1.5 text-muted hover:text-fg"> | |
| 826 | 826 | {link.type === "docs" ? <BookOpen size={12} className="shrink-0 text-faint" /> : <Globe size={12} className="shrink-0 text-faint" />} | |
| 827 | 827 | <span className="truncate">{link.type === "docs" ? `Docs · ${bare(link.url)}` : link.label}</span> | |
| 828 | − | </a> | |
| 828 | + | </DeployLink> | |
| 829 | 829 | ))} | |
| 830 | 830 | </div> | |
| 831 | 831 | )} | |
| ⋯ | |||
| 865 | 865 | </p> | |
| 866 | 866 | {production ? ( | |
| 867 | 867 | <> | |
| 868 | − | <a | |
| 868 | + | <DeployLink | |
| 869 | 869 | href={productionUrl ?? production.url} | |
| 870 | 870 | className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent" | |
| 871 | 871 | > | |
| 872 | 872 | {host(productionUrl ?? production.url)} | |
| 873 | 873 | <ArrowUpRight size={16} className="shrink-0 text-faint" /> | |
| 874 | − | </a> | |
| 874 | + | </DeployLink> | |
| 875 | 875 | <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted"> | |
| 876 | 876 | {latestProduction && <StatusDot status={latestProduction.status} label={latestProduction.status === "ready" ? "Live" : undefined} />} | |
| 877 | 877 | <span className="inline-flex items-center gap-1 font-mono"> | |
| ⋯ | |||
| 908 | 908 | {member && ( | |
| 909 | 909 | <div className="flex shrink-0 items-center gap-2"> | |
| 910 | 910 | {production && ( | |
| 911 | − | <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument> | |
| 911 | + | <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer"> | |
| 912 | 912 | Visit | |
| 913 | 913 | <ArrowUpRight size={14} /> | |
| 914 | 914 | </ButtonLink> | |
| ⋯ | |||
| 1264 | 1264 | {previews.map((app) => ( | |
| 1265 | 1265 | <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm"> | |
| 1266 | 1266 | <span className="min-w-0 grow"> | |
| 1267 | − | <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent"> | |
| 1267 | + | <DeployLink href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent"> | |
| 1268 | 1268 | {host(app.url)} | |
| 1269 | − | </a> | |
| 1269 | + | </DeployLink> | |
| 1270 | 1270 | <span className="text-xs text-muted"> | |
| 1271 | 1271 | {app.branch} | |
| 1272 | 1272 | {app.number != null && ( | |
| 94 | 94 | import { actions, agents, deployments, identity, inbox, repos, work } from "../../lib/services.server"; | |
| 95 | 95 | import { commitChecksFor } from "../../lib/commit-checks.server"; | |
| 96 | 96 | import { CommitChecksBadge } from "../../components/commit-checks"; | |
| 97 | + | import { DeployLink } from "../../components/deploy"; | |
| 97 | 98 | import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server"; | |
| 98 | 99 | import { accessTo, refusal, repoFor } from "../../lib/access.server"; | |
| 99 | 100 | import { SubscriptionBox } from "../../components/notifications"; | |
| ⋯ | |||
| 1683 | 1684 | : "This branch is live"} | |
| 1684 | 1685 | </p> | |
| 1685 | 1686 | {preview ? ( | |
| 1686 | − | <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent"> | |
| 1687 | + | <DeployLink href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent"> | |
| 1687 | 1688 | {preview.url.replace(/^https?:[/][/]/, "")} | |
| 1688 | − | </a> | |
| 1689 | + | </DeployLink> | |
| 1689 | 1690 | ) : ( | |
| 1690 | 1691 | build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p> | |
| 1691 | 1692 | )} | |
| ⋯ | |||
| 1697 | 1698 | </Link> | |
| 1698 | 1699 | )} | |
| 1699 | 1700 | {preview && ( | |
| 1700 | − | <a | |
| 1701 | + | <DeployLink | |
| 1701 | 1702 | href={preview.url} | |
| 1702 | 1703 | className="inline-flex items-center gap-1.5 rounded-md bg-accent px-3 py-1.5 text-xs font-medium text-bg hover:opacity-90" | |
| 1703 | 1704 | > | |
| 1704 | 1705 | Visit preview | |
| 1705 | 1706 | <ArrowUpRight size={13} /> | |
| 1706 | − | </a> | |
| 1707 | + | </DeployLink> | |
| 1707 | 1708 | )} | |
| 1708 | 1709 | </div> | |
| 1709 | 1710 | </div> | |
| 176 | 176 | <GitPullRequest size={16} className="mt-0.5 shrink-0 text-warn" /> | |
| 177 | 177 | <span className="min-w-0 grow"> | |
| 178 | 178 | <span className="block truncate text-sm font-medium text-fg">{row.title}</span> | |
| 179 | − | <span className="block truncate text-xs text-muted"> | |
| 179 | + | <span className="line-clamp-2 text-xs text-muted"> | |
| 180 | 180 | {row.repo ? `${row.repo.name}${row.ref ? ` ${row.ref}` : ""} · ` : ""} | |
| 181 | 181 | {row.ask} | |
| 182 | 182 | </span> |
| 4 | 4 | import type { PackageSummary, Project, ProjectDeploys, User } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import type { Route } from "./+types/overview"; | |
| 7 | − | import { host, StatusDot } from "../../components/deploy"; | |
| 7 | + | import { DeployLink, host, StatusDot } from "../../components/deploy"; | |
| 8 | 8 | import { Avatar, ButtonLink, CopyLine, Pill, TimeAgo } from "../../components/ui"; | |
| 9 | 9 | import { UsageCard } from "../../components/usage-card"; | |
| 10 | 10 | import { PullIcon } from "../../components/work-icons"; | |
| ⋯ | |||
| 176 | 176 | {project.name} | |
| 177 | 177 | </Link> | |
| 178 | 178 | {link && !(library && pkg && !production) ? ( | |
| 179 | − | <a | |
| 179 | + | <DeployLink | |
| 180 | 180 | href={link} | |
| 181 | 181 | className="relative z-10 mt-0.5 flex items-center gap-1 truncate font-mono text-xs text-muted hover:text-accent" | |
| 182 | 182 | > | |
| 183 | 183 | {host(link)} | |
| 184 | 184 | <ArrowUpRight size={11} className="shrink-0" /> | |
| 185 | − | </a> | |
| 185 | + | </DeployLink> | |
| 186 | 186 | ) : library && pkg ? ( | |
| 187 | 187 | <Link | |
| 188 | 188 | to={packagePath(pkg)} | |