Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Cloud, Plus } from "@unkey/icons";
import { Button, Card } from "@unkey/ui";
import { cn } from "@unkey/ui/src/lib/utils";

type Props = {
onCreateDeployment?: () => void;
className?: string;
};

export const ActiveDeploymentCardEmpty = ({ onCreateDeployment, className }: Props) => (
<Card
className={cn(
"rounded-[14px] flex justify-center items-center overflow-hidden border-gray-4 border-dashed bg-gray-1/50",
"min-h-[200px] relative group hover:border-gray-5 transition-colors duration-200",
className,
)}
>
<div className="flex flex-col items-center gap-4 px-8 py-12 text-center">
{/* Icon with subtle animation */}
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-accent-4 to-accent-3 rounded-full blur-xl opacity-20 group-hover:opacity-30 transition-opacity duration-300 animate-pulse" />
<div className="relative bg-gray-3 rounded-full p-3 group-hover:bg-gray-4 transition-all duration-200">
<Cloud
className="text-gray-9 size-6 group-hover:text-gray-11 transition-all duration-200 animate-pulse"
style={{ animationDuration: "2s" }}
/>
</div>
</div>

{/* Content */}
<div className="space-y-2">
<h3 className="text-gray-12 font-medium text-sm">No active deployments</h3>
<p className="text-gray-9 text-xs max-w-[280px] leading-relaxed">
Your deployments will appear here once you push code to your connected repository or
trigger a manual deployment.
</p>
</div>

{/* Action button */}
{onCreateDeployment && (
<Button onClick={onCreateDeployment} size="sm" className="mt-2 group/button">
<Plus className="size-4 mr-2 group-hover/button:rotate-90 transition-transform duration-200" />
Create deployment
</Button>
)}
</div>
</Card>
);
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type LogEntry = {
type LogFilter = "all" | "warnings" | "errors";

type UseDeploymentLogsProps = {
deploymentId: string;
deploymentId: string | null;
showBuildSteps: boolean;
};

Expand Down Expand Up @@ -61,9 +61,12 @@ export function useDeploymentLogs({
const { queryTime: timestamp } = useQueryTime();

const { data: buildData, isLoading: buildLoading } = trpc.deploy.deployment.buildSteps.useQuery(
{ deploymentId },
{
enabled: showBuildSteps && isExpanded,
// without this check TS yells at us
deploymentId: deploymentId ?? "",
},
{
enabled: showBuildSteps && isExpanded && Boolean(deploymentId),
refetchInterval: BUILD_STEPS_REFETCH_INTERVAL,
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { cn } from "@unkey/ui/src/lib/utils";
import { format } from "date-fns";
import { useProjectLayout } from "../../layout-provider";
import { Card } from "../card";
import { ActiveDeploymentCardEmpty } from "./active-deployment-card-empty";
import { FilterButton } from "./filter-button";
import { Avatar } from "./git-avatar";
import { useDeploymentLogs } from "./hooks/use-deployment-logs";
Expand Down Expand Up @@ -68,15 +69,17 @@ export const statusIndicator = (
};

type Props = {
deploymentId: string;
deploymentId: string | null;
};

export const ActiveDeploymentCard = ({ deploymentId }: Props) => {
const { collections } = useProjectLayout();
const { data } = useLiveQuery((q) =>
q
.from({ deployment: collections.deployments })
.where(({ deployment }) => eq(deployment.id, deploymentId)),
const { data, isLoading } = useLiveQuery(
(q) =>
q
.from({ deployment: collections.deployments })
.where(({ deployment }) => eq(deployment.id, deploymentId)),
[deploymentId],
);
const deployment = data.at(0);

Expand All @@ -101,9 +104,12 @@ export const ActiveDeploymentCard = ({ deploymentId }: Props) => {
showBuildSteps,
});

if (!deployment) {
if (isLoading) {
return <ActiveDeploymentCardSkeleton />;
}
if (!deployment) {
return <ActiveDeploymentCardEmpty />;
}

const statusConfig = statusIndicator(deployment.status);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CircleCheck, Link4, ShareUpRight } from "@unkey/icons";
import { Badge } from "@unkey/ui";
import Link from "next/link";
import { Card } from "./card";

type DomainRowProps = {
domain: string;
Expand Down Expand Up @@ -28,3 +29,47 @@ export function DomainRow({ domain }: DomainRowProps) {
</div>
);
}

export const DomainRowSkeleton = () => {
return (
<div className="border border-gray-4 border-t-0 first:border-t first:rounded-t-[14px] last:rounded-b-[14px] last:border-b w-full px-4 py-3 flex justify-between items-center">
<div className="flex items-center">
<Link4 className="text-grayA-6" size="sm-medium" />
<div className="h-3 w-32 bg-grayA-3 rounded animate-pulse ml-3 mr-2" />
<ShareUpRight className="text-grayA-6 shrink-0" size="md-regular" />
<div className="ml-3" />
</div>
<div className="p-[5px] size-[22px] bg-grayA-3 rounded animate-pulse flex items-center justify-center">
<div className="size-3 bg-grayA-4 rounded-full" />
</div>
</div>
);
};

export const DomainRowEmpty = () => (
<Card
className={
"rounded-[14px] flex justify-center items-center overflow-hidden border-gray-4 border-dashed bg-gray-1/50 min-h-[150px] relative group hover:border-gray-5 transition-colors duration-200"
}
>
<div className="flex flex-col items-center gap-3 px-6 py-8 text-center">
{/* Icon with subtle animation */}
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-r from-accent-4 to-accent-3 rounded-full blur-xl opacity-20 group-hover:opacity-30 transition-opacity duration-300 animate-pulse" />
<div className="relative bg-gray-3 rounded-full p-3 group-hover:bg-gray-4 transition-all duration-200">
<Link4
className="text-gray-9 size-6 group-hover:text-gray-11 transition-all duration-200 animate-pulse"
style={{ animationDuration: "2s" }}
/>
</div>
</div>
{/* Content */}
<div className="space-y-2">
<h3 className="text-gray-12 font-medium text-sm">No domains found</h3>
<p className="text-gray-9 text-xs max-w-[280px] leading-relaxed">
Your configured domains will appear here once they're set up and verified.
</p>
</div>
</div>
</Card>
);
21 changes: 18 additions & 3 deletions apps/dashboard/app/(app)/projects/[projectId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";
import { collectionManager } from "@/lib/collections";
import { eq, useLiveQuery } from "@tanstack/react-db";
import { DoubleChevronLeft } from "@unkey/icons";
import { Button, InfoTooltip } from "@unkey/ui";
import { useState } from "react";
Expand All @@ -25,10 +26,23 @@ type ProjectLayoutProps = {

const ProjectLayout = ({ projectId, children }: ProjectLayoutProps) => {
const [tableDistanceToTop, setTableDistanceToTop] = useState(0);
const [isDetailsOpen, setIsDetailsOpen] = useState(true);
const [isDetailsOpen, setIsDetailsOpen] = useState(false);

const collections = collectionManager.getProjectCollections(projectId);

const projects = useLiveQuery((q) =>
q.from({ project: collections.projects }).where(({ project }) => eq(project.id, projectId)),
);

const liveDeploymentId = projects.data.at(0)?.liveDeploymentId;

const getTooltipContent = () => {
if (!liveDeploymentId) {
return "No deployments available. Deploy your project to view details.";
}
return isDetailsOpen ? "Hide deployment details" : "Show deployment details";
};

return (
<ProjectLayoutContext.Provider
value={{
Expand All @@ -46,7 +60,7 @@ const ProjectLayout = ({ projectId, children }: ProjectLayoutProps) => {
detailsExpandableTrigger={
<InfoTooltip
asChild
content="Show details"
content={getTooltipContent()}
position={{
side: "bottom",
align: "end",
Expand All @@ -55,6 +69,7 @@ const ProjectLayout = ({ projectId, children }: ProjectLayoutProps) => {
<Button
variant="ghost"
className="size-7"
disabled={!liveDeploymentId}
onClick={() => setIsDetailsOpen(!isDetailsOpen)}
>
<DoubleChevronLeft size="lg-medium" className="text-gray-13" />
Expand All @@ -68,7 +83,7 @@ const ProjectLayout = ({ projectId, children }: ProjectLayoutProps) => {
<ProjectDetailsExpandable
projectId={projectId}
tableDistanceToTop={tableDistanceToTop}
isOpen={isDetailsOpen}
isOpen={isDetailsOpen && Boolean(liveDeploymentId)}
onClose={() => setIsDetailsOpen(false)}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,18 @@ export const ProjectNavigation = ({ projectId }: ProjectNavigationProps) => {
</Navbar.Breadcrumbs>
<div className="flex gap-4 items-center">
{activeProject.gitRepositoryUrl && (
<div className="text-gray-11 text-xs flex items-center gap-2.5">
<Refresh3 className="text-gray-12" size="sm-regular" />
<span>Auto-deploys from pushes to </span>
<RepoDisplay
url={activeProject.gitRepositoryUrl}
className="bg-grayA-4 px-1.5 font-medium text-xs text-gray-12 rounded-full min-h-[22px] max-w-[130px]"
/>
</div>
<>
<div className="text-gray-11 text-xs flex items-center gap-2.5">
<Refresh3 className="text-gray-12" size="sm-regular" />
<span>Auto-deploys from pushes to </span>
<RepoDisplay
url={activeProject.gitRepositoryUrl}
className="bg-grayA-4 px-1.5 font-medium text-xs text-gray-12 rounded-full min-h-[22px] max-w-[130px]"
/>
</div>
<Separator orientation="vertical" className="h-5 mx-2 bg-grayA-5" />
</>
)}
<Separator orientation="vertical" className="h-5 mx-2 bg-grayA-5" />
<div className="gap-2.5 items-center flex">
<NavbarActionButton title="Visit Project URL">Visit Project URL</NavbarActionButton>
<Button className="size-7" variant="outline">
Expand Down
46 changes: 19 additions & 27 deletions apps/dashboard/app/(app)/projects/[projectId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
import { collection } from "@/lib/collections";
import { eq, useLiveQuery } from "@tanstack/react-db";
import { Cloud, Earth, FolderCloud, Page2 } from "@unkey/icons";
import { Empty } from "@unkey/ui";
import { cn } from "@unkey/ui/src/lib/utils";
import type { ReactNode } from "react";
import { ActiveDeploymentCard } from "./details/active-deployment-card";
import { DomainRow } from "./details/domain-row";
import { DomainRow, DomainRowEmpty, DomainRowSkeleton } from "./details/domain-row";
import { EnvironmentVariablesSection } from "./details/env-variables-section";
import { useProjectLayout } from "./layout-provider";

Expand All @@ -18,24 +17,14 @@ export default function ProjectDetails() {
);

const project = projects.data.at(0);
const domains = useLiveQuery(
const { data: domains, isLoading: isDomainsLoading } = useLiveQuery(
(q) =>
q
.from({ domain: collections.domains })
.where(({ domain }) => eq(domain.deploymentId, project?.liveDeploymentId)),
[project?.liveDeploymentId],
);

if (!project) {
return (
<Empty>
<Empty.Icon />
<Empty.Title>No Project Found</Empty.Title>
<Empty.Description>Project not found</Empty.Description>
</Empty>
);
}

return (
<div
className={cn(
Expand All @@ -44,28 +33,31 @@ export default function ProjectDetails() {
)}
>
<div className="max-w-[960px] flex flex-col w-full mt-4 gap-5">
{project.liveDeploymentId ? (
<Section>
<SectionHeader
icon={<Cloud size="md-regular" className="text-gray-9" />}
title="Active Deployment"
/>
<ActiveDeploymentCard deploymentId={project.liveDeploymentId} />
</Section>
) : null}

<Section>
<SectionHeader
icon={<Cloud size="md-regular" className="text-gray-9" />}
title="Active Deployment"
/>
<ActiveDeploymentCard deploymentId={project?.liveDeploymentId ?? null} />
</Section>
<Section>
<SectionHeader
icon={<Earth size="md-regular" className="text-gray-9" />}
title="Domains"
/>
<div>
{domains.data.map((domain) => (
<DomainRow key={domain.id} domain={domain.domain} />
))}
{isDomainsLoading ? (
<>
<DomainRowSkeleton />
<DomainRowSkeleton />
</>
) : domains?.length > 0 ? (
domains.map((domain) => <DomainRow key={domain.id} domain={domain.domain} />)
) : (
<DomainRowEmpty />
)}
</div>
</Section>

<Section>
<SectionHeader
icon={<FolderCloud size="md-regular" className="text-gray-9" />}
Expand Down
10 changes: 10 additions & 0 deletions apps/dashboard/styles/tailwind/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ code .line::before {
}
}

@keyframes subtle-bounce {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-8px);
}
}

/* For Webkit-based browsers (Chrome, Safari and Opera) */
.scrollbar-hide::-webkit-scrollbar {
display: none;
Expand Down
1 change: 1 addition & 0 deletions internal/icons/src/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ export type IconProps = {
size?: IconSize;
filled?: boolean;
focusable?: boolean;
style?: React.CSSProperties;
};
Loading