| 12 | 12 | | Radio, |
| 13 | 13 | | Rocket, |
| 14 | 14 | | RotateCw, |
| 15 | + | RotateCcw, |
| 16 | + | Settings2, |
| 15 | 17 | | } from "lucide-react"; |
| 16 | 18 | | import { type ReactNode, Suspense } from "react"; |
| 17 | 19 | | import { Await, Form, Link } from "react-router"; |
| ⋯ |
| 28 | 30 | | import { ProductionShot } from "../../components/production-shot"; |
| 29 | 31 | | import { GithubLinkStrip } from "../../components/github"; |
| 30 | 32 | | import { githubApp } from "../../lib/github.server"; |
| 31 | | − | import { ButtonLink, CopyLine, SubmitButton, TimeAgo } from "../../components/ui"; |
| 33 | + | import { ButtonLink, CopyLine, SoonPill, SubmitButton, TimeAgo } from "../../components/ui"; |
| 32 | 34 | | import { Card } from "../../components/ui/card"; |
| 35 | + | import { Button } from "../../components/ui/button"; |
| 33 | 36 | | import { type LivePull, LiveNowList, StartHere } from "../../components/coordination"; |
| 34 | 37 | | import { awakeAndIdle, liveByIssue } from "../../lib/coordination"; |
| 35 | 38 | | import { type Need, ageBuckets, rankNeeds, runHealth } from "../../lib/mission"; |
| ⋯ |
| 644 | 647 | | ); |
| 645 | 648 | | } |
| 646 | 649 | | |
| 647 | | − | /** Production on g1t.page: its picture, address and state, and for members, Visit and Redeploy. */ |
| 650 | + | /** Production on g1t.page: its picture, address and state. Actions live in the card header. */ |
| 648 | 651 | | function ProductionHead({ loaderData, base, name, upload, addImage }: { |
| 649 | 652 | | loaderData: Loaded; |
| 650 | 653 | | base: string; |
| ⋯ |
| 652 | 655 | | upload: { url: string; preferred: boolean } | null; |
| 653 | 656 | | addImage: string | null; |
| 654 | 657 | | }) { |
| 655 | | − | const { member, settings, production, latestProductionBuild, productionLatest, can } = loaderData; |
| 658 | + | const { member, settings, production, latestProductionBuild, productionLatest } = loaderData; |
| 656 | 659 | | // The project's own domain, once it is active, is where production is visited. |
| 657 | 660 | | const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null; |
| 658 | 661 | | return ( |
| ⋯ |
| 723 | 726 | | </> |
| 724 | 727 | | )} |
| 725 | 728 | | </div> |
| 726 | | − | {member && ( |
| 727 | | − | <div className="flex shrink-0 items-center gap-2"> |
| 728 | | − | {productionUrl && ( |
| 729 | | − | <ButtonLink to={productionUrl} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer"> |
| 730 | | − | Visit |
| 731 | | − | <ArrowUpRight size={14} /> |
| 732 | | − | </ButtonLink> |
| 733 | | − | )} |
| 734 | | − | {settings?.enabled |
| 735 | | − | ? can.run && ( |
| 736 | | − | <Form method="post"> |
| 737 | | − | <Hint label="Build production again from the default branch"> |
| 738 | | − | <SubmitButton variant="outline" name="intent" value="redeploy" pending="Redeploying…"> |
| 739 | | − | <RotateCw size={14} /> |
| 740 | | − | Redeploy |
| 741 | | − | </SubmitButton> |
| 742 | | − | </Hint> |
| 743 | | − | </Form> |
| 744 | | − | ) |
| 745 | | − | : can.manage_integrations && ( |
| 746 | | − | <ButtonLink to={`${base}/settings/deployments`} variant="accent"> |
| 747 | | − | <Rocket size={14} /> |
| 748 | | − | Turn on deployments |
| 749 | | − | </ButtonLink> |
| 750 | | − | )} |
| 751 | | − | </div> |
| 752 | | − | )} |
| 753 | | − | {!member && productionUrl && ( |
| 754 | | − | <ButtonLink to={productionUrl} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer" className="shrink-0"> |
| 755 | | − | Visit |
| 756 | | − | <ArrowUpRight size={14} /> |
| 729 | + | </div> |
| 730 | + | </div> |
| 731 | + | ); |
| 732 | + | } |
| 733 | + | |
| 734 | + | /** Production controls share a header, whether g1t or a workflow hosts it. */ |
| 735 | + | function ProductionActions({ loaderData, base, elsewhere }: { loaderData: Loaded; base: string; elsewhere: boolean }) { |
| 736 | + | const { member, settings, production, project, can } = loaderData; |
| 737 | + | const url = elsewhere |
| 738 | + | ? loaderData.deployment?.url ?? project?.productionUrl |
| 739 | + | : production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null; |
| 740 | + | return ( |
| 741 | + | <div className="flex flex-wrap items-center gap-2"> |
| 742 | + | {member && ( |
| 743 | + | <Hint label="Deployment history"> |
| 744 | + | <ButtonLink to={`${base}/deployments`} variant="outline" size="icon-sm" aria-label="Deployment history"> |
| 745 | + | <Rocket size={15} /> |
| 746 | + | </ButtonLink> |
| 747 | + | </Hint> |
| 748 | + | )} |
| 749 | + | {member && (can.run || can.manage_integrations) && ( |
| 750 | + | <Hint disabled label="Instant rollback is coming soon. It is not available yet."> |
| 751 | + | <Button disabled variant="outline" size="sm"> |
| 752 | + | <RotateCcw size={14} /> |
| 753 | + | Instant rollback <SoonPill /> |
| 754 | + | </Button> |
| 755 | + | </Hint> |
| 756 | + | )} |
| 757 | + | {!elsewhere && member && settings?.enabled && can.run && ( |
| 758 | + | <Form method="post"> |
| 759 | + | <Hint label="Build production again from the default branch"> |
| 760 | + | <SubmitButton variant="outline" size="sm" name="intent" value="redeploy" pending="Redeploying…"> |
| 761 | + | <RotateCw size={14} /> Redeploy |
| 762 | + | </SubmitButton> |
| 763 | + | </Hint> |
| 764 | + | </Form> |
| 765 | + | )} |
| 766 | + | {!elsewhere && member && !settings?.enabled && can.manage_integrations && ( |
| 767 | + | <ButtonLink to={`${base}/settings/deployments`} variant="accent" size="sm"> |
| 768 | + | <Rocket size={14} /> Turn on deployments |
| 769 | + | </ButtonLink> |
| 770 | + | )} |
| 771 | + | {member && can.manage_integrations && ( |
| 772 | + | <Hint label="Deployment settings"> |
| 773 | + | <ButtonLink to={`${base}/settings/deployments`} variant="outline" size="icon-sm" aria-label="Deployment settings"> |
| 774 | + | <Settings2 size={15} /> |
| 757 | 775 | | </ButtonLink> |
| 758 | | − | )} |
| 759 | | − | </div> |
| 776 | + | </Hint> |
| 777 | + | )} |
| 778 | + | {url && ( |
| 779 | + | <ButtonLink to={url} variant="accent" size="sm" reloadDocument target="_blank" rel="noopener noreferrer"> |
| 780 | + | Visit <ArrowUpRight size={14} /> |
| 781 | + | </ButtonLink> |
| 782 | + | )} |
| 760 | 783 | | </div> |
| 761 | 784 | | ); |
| 762 | 785 | | } |
| ⋯ |
| 801 | 824 | | {/* What the project is, whether it is running and healthy, and a line to its code. */} |
| 802 | 825 | | <Card asChild className="overflow-hidden rounded-2xl"> |
| 803 | 826 | | <section aria-label="Production"> |
| 804 | | − | {project && ( |
| 805 | | − | <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"> |
| 806 | | − | <KindMenu project={project} label={kindLabel(project)} canChange={canChange} /> |
| 807 | | − | |
| 827 | + | {(project || plan === "production") && ( |
| 828 | + | <div className="flex flex-wrap items-center justify-between gap-3 border-b border-line px-5 py-3 text-xs sm:px-6"> |
| 829 | + | <div className="flex flex-wrap items-center gap-3"> |
| 830 | + | {(plan === "production" || plan === "elsewhere") && <h2 className="text-sm font-semibold">Production deployment</h2>} |
| 831 | + | {project && <KindMenu project={project} label={kindLabel(project)} canChange={canChange} />} |
| 832 | + | </div> |
| 833 | + | {(plan === "production" || plan === "elsewhere") && <ProductionActions loaderData={loaderData} base={base} elsewhere={plan === "elsewhere"} />} |
| 808 | 834 | | </div> |
| 809 | 835 | | )} |
| 810 | 836 | | {plan === "elsewhere" && project ? ( |