-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
π Release v0.29.1 #4261
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
Merged
Merged
π Release v0.29.1 #4261
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
0cae833
chore: adjust version bump timing in synchronization workflow
Siumauricio e7c5814
feat: add workflow dispatch trigger to version synchronization workflow
Siumauricio 958372c
chore: update paths in version synchronization workflow for MCP and Cβ¦
Siumauricio 425fef6
fix: remove 'v' prefix from version in synchronization workflow
Siumauricio f7b576c
Fix typo in custom entrypoint description
sancho1952007 4277a50
Merge pull request #4241 from sancho1952007/patch-1
Siumauricio 6f0ed89
feat: add dashboard home page with overview and recent deployments
Siumauricio e785939
[autofix.ci] apply automated fixes
autofix-ci[bot] 2ba1df1
feat: refine home page and fix libsql in bulk actions
Siumauricio 5c787ad
feat: implement homeStats query for dashboard overview
Siumauricio f6e2c03
[autofix.ci] apply automated fixes
autofix-ci[bot] d9945c0
style: update ShowHome component layout for improved responsiveness
Siumauricio b392e58
Merge pull request #4244 from Dokploy/feat/dashboard-home
Siumauricio 54417ca
fix: limit application columns in findPreviewDeploymentById to avoid β¦
colocated 13248c8
Merge pull request #4257 from colocated/fix/4256-preview-deployment-tβ¦
Siumauricio 98a5864
chore: bump version to v0.29.1 in package.json
Siumauricio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| import { formatDistanceToNow } from "date-fns"; | ||
| import { ArrowRight, Rocket, Server } from "lucide-react"; | ||
| import Link from "next/link"; | ||
| import { useMemo } from "react"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Card } from "@/components/ui/card"; | ||
| import { api } from "@/utils/api"; | ||
|
|
||
| type DeploymentStatus = "idle" | "running" | "done" | "error"; | ||
|
|
||
| const statusDotClass: Record<string, string> = { | ||
| done: "bg-emerald-500", | ||
| running: "bg-amber-500", | ||
| error: "bg-red-500", | ||
| idle: "bg-muted-foreground/40", | ||
| }; | ||
|
|
||
| function getServiceInfo(d: any) { | ||
| const app = d.application; | ||
| const comp = d.compose; | ||
| const serverName: string = | ||
| d.server?.name ?? app?.server?.name ?? comp?.server?.name ?? "Dokploy"; | ||
| if (app?.environment?.project && app.environment) { | ||
| return { | ||
| name: app.name as string, | ||
| environment: app.environment.name as string, | ||
| projectName: app.environment.project.name as string, | ||
| serverName, | ||
| href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`, | ||
| }; | ||
| } | ||
| if (comp?.environment?.project && comp.environment) { | ||
| return { | ||
| name: comp.name as string, | ||
| environment: comp.environment.name as string, | ||
| projectName: comp.environment.project.name as string, | ||
| serverName, | ||
| href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`, | ||
| }; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| function StatCard({ | ||
| label, | ||
| value, | ||
| delta, | ||
| }: { | ||
| label: string; | ||
| value: string; | ||
| delta?: string; | ||
| }) { | ||
| return ( | ||
| <div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col justify-between"> | ||
| <span className="text-xs uppercase tracking-wider text-muted-foreground"> | ||
| {label} | ||
| </span> | ||
| <div className="flex flex-col gap-1"> | ||
| <span className="text-3xl font-semibold tracking-tight">{value}</span> | ||
| {delta && ( | ||
| <span className="text-xs text-muted-foreground">{delta}</span> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function StatusListCard({ | ||
| label, | ||
| items, | ||
| }: { | ||
| label: string; | ||
| items: { dotClass: string; label: string; count: number }[]; | ||
| }) { | ||
| return ( | ||
| <div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col gap-3"> | ||
| <span className="text-xs uppercase tracking-wider text-muted-foreground"> | ||
| {label} | ||
| </span> | ||
| <ul className="flex flex-col gap-1.5"> | ||
| {items.map((item) => ( | ||
| <li key={item.label} className="flex items-center gap-2.5 text-sm"> | ||
| <span | ||
| className={`size-2 rounded-full shrink-0 ${item.dotClass}`} | ||
| aria-hidden | ||
| /> | ||
| <span className="font-semibold tabular-nums w-8">{item.count}</span> | ||
| <span className="text-muted-foreground">{item.label}</span> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export const ShowHome = () => { | ||
| const { data: auth } = api.user.get.useQuery(); | ||
| const { data: homeStats } = api.project.homeStats.useQuery(); | ||
| const { data: permissions } = api.user.getPermissions.useQuery(); | ||
| const canReadDeployments = !!permissions?.deployment.read; | ||
| const { data: deployments } = api.deployment.allCentralized.useQuery( | ||
| undefined, | ||
| { | ||
| enabled: canReadDeployments, | ||
| refetchInterval: 10000, | ||
| }, | ||
| ); | ||
|
|
||
| const firstName = auth?.user?.firstName?.trim(); | ||
|
|
||
| const totals = homeStats ?? { | ||
| projects: 0, | ||
| environments: 0, | ||
| applications: 0, | ||
| compose: 0, | ||
| databases: 0, | ||
| services: 0, | ||
| }; | ||
| const statusBreakdown = homeStats?.status ?? { | ||
| running: 0, | ||
| error: 0, | ||
| idle: 0, | ||
| }; | ||
|
|
||
| const recentDeployments = useMemo(() => { | ||
| if (!deployments) return []; | ||
| return [...deployments] | ||
| .sort( | ||
| (a, b) => | ||
| new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), | ||
| ) | ||
| .slice(0, 10); | ||
| }, [deployments]); | ||
|
|
||
| const deployStats = useMemo(() => { | ||
| const now = Date.now(); | ||
| const weekMs = 7 * 24 * 60 * 60 * 1000; | ||
| const lastStart = now - weekMs; | ||
| const prevStart = now - 2 * weekMs; | ||
|
|
||
| const last: NonNullable<typeof deployments> = []; | ||
| const prev: NonNullable<typeof deployments> = []; | ||
| for (const d of deployments ?? []) { | ||
| const t = new Date(d.createdAt).getTime(); | ||
| if (t >= lastStart) last.push(d); | ||
| else if (t >= prevStart) prev.push(d); | ||
| } | ||
|
|
||
| const lastCount = last.length; | ||
| const prevCount = prev.length; | ||
| let delta: string | undefined; | ||
| if (prevCount > 0) { | ||
| const pct = Math.round(((lastCount - prevCount) / prevCount) * 100); | ||
| delta = `${pct >= 0 ? "+" : ""}${pct}% vs prev 7d`; | ||
| } else if (lastCount > 0) { | ||
| delta = "no prior data"; | ||
| } else { | ||
| delta = "no activity yet"; | ||
| } | ||
|
|
||
| return { value: String(lastCount), delta }; | ||
| }, [deployments]); | ||
|
|
||
| return ( | ||
| <div className="w-full"> | ||
| <Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[85vh]"> | ||
| <div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-6 h-full"> | ||
| <div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between"> | ||
| <h1 className="text-3xl font-semibold tracking-tight"> | ||
| {firstName ? `Welcome back, ${firstName}` : "Welcome back"} | ||
| </h1> | ||
| <Button asChild variant="secondary" className="w-fit"> | ||
| <Link href="/dashboard/projects"> | ||
| Go to projects | ||
| <ArrowRight className="size-4" /> | ||
| </Link> | ||
| </Button> | ||
| </div> | ||
|
|
||
| <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> | ||
| <StatCard | ||
| label="Projects" | ||
| value={String(totals.projects)} | ||
| delta={`${totals.environments} ${totals.environments === 1 ? "environment" : "environments"}`} | ||
| /> | ||
| <StatCard | ||
| label="Services" | ||
| value={String(totals.services)} | ||
| delta={`${totals.applications} apps Β· ${totals.compose} compose Β· ${totals.databases} db`} | ||
| /> | ||
| <StatCard | ||
| label="Deploys / 7d" | ||
| value={deployStats.value} | ||
| delta={deployStats.delta} | ||
| /> | ||
| <StatusListCard | ||
| label="Status" | ||
| items={[ | ||
| { | ||
| dotClass: "bg-emerald-500", | ||
| label: "running", | ||
| count: statusBreakdown.running, | ||
| }, | ||
| { | ||
| dotClass: "bg-red-500", | ||
| label: "errored", | ||
| count: statusBreakdown.error, | ||
| }, | ||
| { | ||
| dotClass: "bg-muted-foreground/40", | ||
| label: "idle", | ||
| count: statusBreakdown.idle, | ||
| }, | ||
| ]} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="rounded-xl border bg-background"> | ||
| <div className="flex items-center justify-between px-5 py-4 border-b"> | ||
| <div className="flex items-center gap-2"> | ||
| <Rocket className="size-4 text-muted-foreground" /> | ||
| <h2 className="text-sm font-semibold">Recent deployments</h2> | ||
| </div> | ||
| {canReadDeployments && ( | ||
| <Link | ||
| href="/dashboard/deployments" | ||
| className="text-xs text-muted-foreground hover:text-foreground transition-colors" | ||
| > | ||
| view all β | ||
| </Link> | ||
| )} | ||
| </div> | ||
| {!canReadDeployments ? ( | ||
| <div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10"> | ||
| <Rocket className="size-8 opacity-40" /> | ||
| <span>You do not have permission to view deployments.</span> | ||
| </div> | ||
| ) : recentDeployments.length === 0 ? ( | ||
| <div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10"> | ||
| <Rocket className="size-8 opacity-40" /> | ||
| <span>No deployments yet.</span> | ||
| </div> | ||
| ) : ( | ||
| <ul className="divide-y"> | ||
| {recentDeployments.map((d) => { | ||
| const info = getServiceInfo(d); | ||
| if (!info) return null; | ||
| const status = (d.status ?? "idle") as DeploymentStatus; | ||
| return ( | ||
| <li key={d.deploymentId}> | ||
| <Link | ||
| href={info.href} | ||
| className="flex items-center gap-4 px-5 py-4 hover:bg-muted/40 transition-colors" | ||
| > | ||
| <span | ||
| className={`size-2 rounded-full shrink-0 ${statusDotClass[status] ?? statusDotClass.idle}`} | ||
| aria-hidden | ||
| /> | ||
| <div className="flex flex-col min-w-0 flex-1"> | ||
| <span className="text-sm truncate">{info.name}</span> | ||
| <span className="text-xs text-muted-foreground truncate"> | ||
| {info.projectName} Β· {info.environment} | ||
| </span> | ||
| </div> | ||
| <span className="text-xs text-muted-foreground w-36 hidden lg:flex items-center justify-end gap-1.5 truncate"> | ||
| <Server className="size-3 shrink-0" /> | ||
| <span className="truncate">{info.serverName}</span> | ||
| </span> | ||
| <span className="text-xs text-muted-foreground w-20 text-right hidden sm:inline"> | ||
| {status} | ||
| </span> | ||
| <span className="text-xs text-muted-foreground w-24 text-right hidden md:inline"> | ||
| {formatDistanceToNow(new Date(d.createdAt), { | ||
| addSuffix: true, | ||
| })} | ||
| </span> | ||
| <span className="text-xs text-muted-foreground hover:text-foreground transition-colors"> | ||
| logs β | ||
| </span> | ||
| </Link> | ||
| </li> | ||
| ); | ||
| })} | ||
| </ul> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </Card> | ||
| </div> | ||
| ); | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
allCentralized.useQuery()fetches the complete deployment history, sorts it in JavaScript, and then discards all but 10 rows. On a busy instance this could transfer a large payload on every 10-second refresh just to display a 10-row list. Consider adding a server-sidelimit/offsetor a dedicatedrecentDeploymentsquery that returns only the N most-recent records.