Merge branch 'canary' into hehehai/feat-server-custom-name

This commit is contained in:
Mauricio Siu
2024-06-08 14:37:51 -06:00
295 changed files with 43807 additions and 3328 deletions

View File

@@ -1,44 +1,53 @@
import { Toaster } from "@/components/ui/sonner";
import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { api } from "@/utils/api";
import type { NextPage } from "next";
import { ThemeProvider } from "next-themes";
import type { AppProps } from "next/app";
import { Inter } from "next/font/google";
import Head from "next/head";
import type { ReactElement, ReactNode } from "react";
const inter = Inter({ subsets: ["latin"] });
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
// session: Session | null;
theme?: string;
getLayout?: (page: ReactElement) => ReactNode;
// session: Session | null;
theme?: string;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
Component: NextPageWithLayout;
};
const MyApp = ({
Component,
pageProps: { ...pageProps },
Component,
pageProps: { ...pageProps },
}: AppPropsWithLayout) => {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<>
<Head>
<title>Dokploy</title>
</Head>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
forcedTheme={Component.theme}
>
<Toaster richColors />
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</>
);
const getLayout = Component.getLayout ?? ((page) => page);
return (
<>
<style jsx global>{`
:root {
--font-inter: ${inter.style.fontFamily};
}
`}</style>
<Head>
<title>Dokploy</title>
</Head>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
forcedTheme={Component.theme}
>
<Toaster richColors />
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</>
);
};
export default api.withTRPC(MyApp);

View File

@@ -1,18 +1,11 @@
import { Head, Html, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head>
<link rel="shortcut icon" href="/icon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;400;500;600;700&display=swap"
rel="stylesheet"
/>
</Head>
<Html lang="en" className="font-sans">
<Head />
<body className="flex h-full flex-col ">
<body className="flex h-full flex-col font-sans">
<Main />
<NextScript />
</body>

View File

@@ -1,57 +1,98 @@
import { Logo } from "@/components/shared/logo";
import { buttonVariants } from "@/components/ui/button";
import type { NextPageContext } from "next";
import Link from "next/link";
interface Props {
statusCode: number;
error?: Error;
}
export default function Custom404({ statusCode }: Props) {
export default function Custom404({ statusCode, error }: Props) {
const displayStatusCode = statusCode || 400;
console.log(error, statusCode);
return (
<div className="h-screen">
<section className="relative z-10 bg-background h-screen items-center justify-center">
<div className="container mx-auto h-screen items-center justify-center flex">
<div className="-mx-4 flex">
<div className="w-full px-4">
<div className="mx-auto max-w-[400px] text-center">
<h2 className="mb-2 text-[50px] font-bold leading-none text-white sm:text-[80px] md:text-[100px]">
{statusCode
? `An error ${statusCode} occurred on server`
: "An error occurred on client"}
</h2>
<h4 className="mb-3 text-[22px] font-semibold leading-tight text-white">
Oops! That page cant be found
</h4>
<p className="mb-8 text-lg text-white">
The page you are looking was not found
</p>
<Link
href="/"
className={buttonVariants({
size: "lg",
})}
>
Go To Home
</Link>
<div className="max-w-[50rem] flex flex-col mx-auto size-full">
<header className="mb-auto flex justify-center z-50 w-full py-4">
<nav className="px-4 sm:px-6 lg:px-8" aria-label="Global">
<Link
href="https://dokploy.com"
target="_blank"
className="flex flex-row items-center gap-2"
>
<Logo />
<span className="font-medium text-sm">Dokploy</span>
</Link>
</nav>
</header>
<main id="content">
<div className="text-center py-10 px-4 sm:px-6 lg:px-8">
<h1 className="block text-7xl font-bold text-primary sm:text-9xl">
{displayStatusCode}
</h1>
{/* <AlertBlock className="max-w-xs mx-auto">
<p className="text-muted-foreground">
Oops, something went wrong.
</p>
<p className="text-muted-foreground">
Sorry, we couldn't find your page.
</p>
</AlertBlock> */}
<p className="mt-3 text-muted-foreground">
{statusCode === 404
? "Sorry, we couldn't find your page."
: "Oops, something went wrong."}
</p>
{error && (
<div className="mt-3 text-red-500">
<p>{error.message}</p>
</div>
)}
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
<Link
href="/dashboard/projects"
className={buttonVariants({
variant: "secondary",
className: "flex flex-row gap-2",
})}
>
<svg
className="flex-shrink-0 size-4"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15 18-6-6 6-6" />
</svg>
Go to homepage
</Link>
</div>
</div>
</div>
</main>
<div className="absolute left-0 top-0 -z-10 flex h-full w-full items-center justify-between space-x-5 md:space-x-8 lg:space-x-14">
<div className="h-full w-1/3 bg-gradient-to-t from-[#FFFFFF14] to-[#C4C4C400]" />
<div className="flex h-full w-1/3">
<div className="h-full w-1/2 bg-gradient-to-b from-[#FFFFFF14] to-[#C4C4C400]" />
<div className="h-full w-1/2 bg-gradient-to-t from-[#FFFFFF14] to-[#C4C4C400]" />
<footer className="mt-auto text-center py-5">
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-sm text-gray-500">
Submit Log in issue on Github
</p>
</div>
<div className="h-full w-1/3 bg-gradient-to-b from-[#FFFFFF14] to-[#C4C4C400]" />
</div>
</section>
</footer>
</div>
</div>
);
}
// @ts-ignore
Error.getInitialProps = ({ res, err }) => {
Error.getInitialProps = ({ res, err, ...rest }: NextPageContext) => {
console.log(err, rest);
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode };
return { statusCode, error: err };
};

View File

@@ -35,7 +35,24 @@ export default async function handler(
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const sourceType = application.sourceType;
if (sourceType === "github") {
if (sourceType === "docker") {
const applicationDockerTag = extractImageTag(application.dockerImage);
const webhookDockerTag = extractImageTagFromRequest(
req.headers,
req.body,
);
if (
applicationDockerTag &&
webhookDockerTag &&
webhookDockerTag !== applicationDockerTag
) {
res.status(301).json({
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
});
return;
}
} else if (sourceType === "github") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== application.branch) {
res.status(301).json({ message: "Branch Not Match" });
@@ -59,6 +76,7 @@ export default async function handler(
applicationId: application.applicationId as string,
titleLog: deploymentTitle,
type: "deploy",
applicationType: "application",
};
await myQueue.add(
"deployments",
@@ -79,7 +97,40 @@ export default async function handler(
res.status(400).json({ message: "Error To Deploy Application", error });
}
}
function extractCommitMessage(headers: any, body: any) {
/**
* Return the last part of the image name, which is the tag
* Example: "my-image" => null
* Example: "my-image:latest" => "latest"
* Example: "my-image:1.0.0" => "1.0.0"
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0"
* @link https://docs.docker.com/reference/cli/docker/image/tag/
*/
function extractImageTag(dockerImage: string | null) {
if (!dockerImage || typeof dockerImage !== "string") {
return null;
}
const tag = dockerImage.split(":").pop();
return tag === dockerImage ? "latest" : tag;
}
/**
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
*/
export const extractImageTagFromRequest = (
headers: any,
body: any,
): string | null => {
if (headers["user-agent"]?.includes("Go-http-client")) {
if (body.push_data && body.repository) {
return body.push_data.tag;
}
}
return null;
};
export const extractCommitMessage = (headers: any, body: any) => {
// GitHub
if (headers["x-github-event"]) {
return body.head_commit ? body.head_commit.message : "NEW COMMIT";
@@ -113,9 +164,9 @@ function extractCommitMessage(headers: any, body: any) {
}
return "NEW CHANGES";
}
};
function extractBranchName(headers: any, body: any) {
export const extractBranchName = (headers: any, body: any) => {
if (headers["x-github-event"] || headers["x-gitea-event"]) {
return body?.ref?.replace("refs/heads/", "");
}
@@ -129,4 +180,4 @@ function extractBranchName(headers: any, body: any) {
}
return null;
}
};

View File

@@ -0,0 +1,83 @@
import { db } from "@/server/db";
import { compose } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import { extractBranchName, extractCommitMessage } from "../[refreshToken]";
import { updateCompose } from "@/server/api/services/compose";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { refreshToken } = req.query;
try {
if (req.headers["x-github-event"] === "ping") {
res.status(200).json({ message: "Ping received, webhook is active" });
return;
}
const composeResult = await db.query.compose.findFirst({
where: eq(compose.refreshToken, refreshToken as string),
with: {
project: true,
},
});
if (!composeResult) {
res.status(404).json({ message: "Compose Not Found" });
return;
}
if (!composeResult?.autoDeploy) {
res.status(400).json({ message: "Compose Not Deployable" });
return;
}
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const sourceType = composeResult.sourceType;
if (sourceType === "github") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== composeResult.branch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "git") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== composeResult.customGitBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
}
try {
await updateCompose(composeResult.composeId, {
composeStatus: "running",
});
const jobData: DeploymentJob = {
composeId: composeResult.composeId as string,
titleLog: deploymentTitle,
type: "deploy",
applicationType: "compose",
};
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
} catch (error) {
res.status(400).json({ message: "Error To Deploy Compose", error });
return;
}
res.status(200).json({ message: "Compose Deployed Succesfully" });
} catch (error) {
console.log(error);
res.status(400).json({ message: "Error To Deploy Compose", error });
}
}

View File

@@ -1,5 +1,7 @@
import { AddApplication } from "@/components/dashboard/project/add-application";
import { AddCompose } from "@/components/dashboard/project/add-compose";
import { AddDatabase } from "@/components/dashboard/project/add-database";
import { AddTemplate } from "@/components/dashboard/project/add-template";
import {
MariadbIcon,
MongodbIcon,
@@ -31,7 +33,7 @@ import type { findProjectById } from "@/server/api/services/project";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { FolderInput, GlobeIcon, PlusIcon } from "lucide-react";
import { CircuitBoard, FolderInput, GlobeIcon, PlusIcon } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -43,7 +45,14 @@ import superjson from "superjson";
export type Services = {
name: string;
type: "mariadb" | "application" | "postgres" | "mysql" | "mongo" | "redis";
type:
| "mariadb"
| "application"
| "postgres"
| "mysql"
| "mongo"
| "redis"
| "compose";
description?: string | null;
id: string;
createdAt: string;
@@ -113,7 +122,24 @@ export const extractServices = (data: Project | undefined) => {
description: item.description,
})) || [];
applications.push(...mysql, ...redis, ...mongo, ...postgres, ...mariadb);
const compose: Services[] =
data?.compose.map((item) => ({
name: item.name,
type: "compose",
id: item.composeId,
createdAt: item.createdAt,
status: item.composeStatus,
description: item.description,
})) || [];
applications.push(
...mysql,
...redis,
...mongo,
...postgres,
...mariadb,
...compose,
);
applications.sort((a, b) => {
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
@@ -144,7 +170,8 @@ const Project = (
data?.mysql?.length === 0 &&
data?.postgres?.length === 0 &&
data?.redis?.length === 0 &&
data?.applications?.length === 0;
data?.applications?.length === 0 &&
data?.compose?.length === 0;
const applications = extractServices(data);
@@ -188,6 +215,8 @@ const Project = (
projectName={data?.name}
/>
<AddDatabase projectId={projectId} projectName={data?.name} />
<AddCompose projectId={projectId} />
<AddTemplate projectId={projectId} />
</DropdownMenuContent>
</DropdownMenu>
)}
@@ -215,7 +244,7 @@ const Project = (
}}
className="group relative cursor-pointer bg-transparent transition-colors hover:bg-card h-fit"
>
<div className="absolute -right-1 -top-1">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={service.status} />
</div>
@@ -252,6 +281,9 @@ const Project = (
{service.type === "application" && (
<GlobeIcon className="h-6 w-6" />
)}
{service.type === "compose" && (
<CircuitBoard className="h-6 w-6" />
)}
</span>
</div>
</CardTitle>

View File

@@ -1,3 +1,4 @@
import { ShowClusterSettings } from "@/components/dashboard/application/advanced/cluster/show-cluster-settings";
import { AddCommand } from "@/components/dashboard/application/advanced/general/add-command";
import { ShowPorts } from "@/components/dashboard/application/advanced/ports/show-port";
import { ShowRedirects } from "@/components/dashboard/application/advanced/redirects/show-redirects";
@@ -13,7 +14,6 @@ import { ShowGeneralApplication } from "@/components/dashboard/application/gener
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { UpdateApplication } from "@/components/dashboard/application/update-application";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import {
@@ -134,7 +134,7 @@ const Service = (
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2">
<UpdateApplication applicationId={applicationId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteApplication applicationId={applicationId} />
@@ -175,6 +175,7 @@ const Service = (
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommand applicationId={applicationId} />
<ShowClusterSettings applicationId={applicationId} />
<ShowApplicationResources applicationId={applicationId} />
<ShowVolumes applicationId={applicationId} />
<ShowRedirects applicationId={applicationId} />

View File

@@ -0,0 +1,245 @@
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { ShowVolumesCompose } from "@/components/dashboard/compose/advanced/show-volumes";
import { DeleteCompose } from "@/components/dashboard/compose/delete-compose";
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show";
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { api } from "@/utils/api";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { CircuitBoard } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useState, type ReactElement } from "react";
import superjson from "superjson";
type TabState =
| "projects"
| "settings"
| "advanced"
| "deployments"
| "monitoring";
const Service = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
) => {
const { composeId, activeTab } = props;
const router = useRouter();
const { projectId } = router.query;
const [tab, setSab] = useState<TabState>(activeTab);
const { data } = api.compose.one.useQuery(
{ composeId },
{
refetchInterval: 5000,
},
);
const { data: auth } = api.auth.get.useQuery();
const { data: user } = api.user.byAuthId.useQuery(
{
authId: auth?.id || "",
},
{
enabled: !!auth?.id && auth?.rol === "user",
},
);
return (
<div className="pb-10">
<div className="flex flex-col gap-4">
<Breadcrumb>
<BreadcrumbItem>
<BreadcrumbLink as={Link} href="/dashboard/projects">
Projects
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbLink
as={Link}
href={`/dashboard/project/${data?.project.projectId}`}
>
{data?.project.name}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<BreadcrumbLink>{data?.name}</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>
<header className="mb-6 flex w-full items-center justify-between max-sm:flex-wrap gap-4">
<div className="flex flex-col justify-between w-fit gap-2">
<div className="flex flex-row items-center gap-2 xl:gap-4 flex-wrap">
<h1 className="flex items-center gap-2 text-xl font-bold lg:text-3xl">
{data?.name}
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
{data?.description}
</p>
)}
</div>
<div className="relative flex flex-row gap-4">
<div className="absolute -right-1 -top-2">
<StatusTooltip status={data?.composeStatus} />
</div>
<CircuitBoard className="h-6 w-6 text-muted-foreground" />
</div>
</header>
</div>
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-6 max-md:overflow-y-scroll justify-start">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteCompose composeId={composeId} />
)}
</div>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironmentCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMonitoringCompose
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogsCompose appName={data?.appName || ""} />
</div>
</TabsContent>
<TabsContent value="deployments">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeploymentsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} />
<ShowVolumesCompose composeId={composeId} />
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default Service;
Service.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{
composeId: string;
activeTab: TabState;
}>,
) {
const { query, params, req, res } = ctx;
const activeTab = query.tab;
const { user, session } = await validateRequest(req, res);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
// Fetch data from external API
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
// Valid project, if not return to initial homepage....
if (typeof params?.composeId === "string") {
try {
await helpers.compose.one.fetch({
composeId: params?.composeId,
});
return {
props: {
trpcState: helpers.dehydrate(),
composeId: params?.composeId,
activeTab: (activeTab || "general") as TabState,
},
};
} catch (error) {
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
},
};
}
}
return {
redirect: {
permanent: false,
destination: "/",
},
};
}

View File

@@ -116,7 +116,7 @@ const Mariadb = (
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2">
<UpdateMariadb mariadbId={mariadbId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteMariadb mariadbId={mariadbId} />

View File

@@ -118,7 +118,7 @@ const Mongo = (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2">
<UpdateMongo mongoId={mongoId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteMongo mongoId={mongoId} />

View File

@@ -117,7 +117,7 @@ const MySql = (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2">
<UpdateMysql mysqlId={mysqlId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteMysql mysqlId={mysqlId} />

View File

@@ -118,7 +118,7 @@ const Postgresql = (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2">
<UpdatePostgres postgresId={postgresId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeletePostgres postgresId={postgresId} />

View File

@@ -116,7 +116,7 @@ const Redis = (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-4">
<div className="flex flex-row gap-2">
<UpdateRedis redisId={redisId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteRedis redisId={redisId} />

View File

@@ -0,0 +1,43 @@
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { validateRequest } from "@/server/auth/auth";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowRegistry />
<ShowNodes />
</div>
);
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return (
<DashboardLayout tab={"settings"}>
<SettingsLayout>{page}</SettingsLayout>
</DashboardLayout>
);
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user || user.rol === "user") {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
return {
props: {},
};
}

View File

@@ -193,7 +193,7 @@ export default function Home({ hasAdmin }: Props) {
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
<Link
className="hover:underline text-muted-foreground"
href="https://docs.dokploy.com/reset-password"
href="https://docs.dokploy.com/get-started/reset-password"
target="_blank"
>
Lost your password?